PROJECT: CoinBook


Overview

CoinBook is a desktop accounting application written in Java. It is targeted at cryptocurrency traders and enthusiasts, and allows them to keep track of the coins they hold, obtain price data and analytics, and read the latest news relevant to them in the same place. Primary interaction is through a CLI, and a GUI built with JavaFX.

Summary of contributions

  • Major enhancement: Added user-set notification rules

    • What it does: Allows the user to set rules regarding any property of a coin, e.g. price, profitability, etc., which trigger notifications if the conditions are met upon a price data update.

    • Justification: This feature informs the user immediately and draws their attention to the updates that is most relevant to them. Users can set notifications as suits their needs and focus on what’s important.

    • Highlights: This enhancement requires various backend systems that were implemented by other team members and close coordination of the interfaces and behaviour was challenging but crucial.
      An Event-Driven Design drives the feature and the existing Command pattern was adapted and combined with another design pattern to define custom behaviours upon rule triggers (although it is only used for creating pop-up notifications now).

  • Proposed extensions: The notifications features was designed with extensibility in mind. It could be upgraded in the future to handle other types of trigger-action pairs, encapsulated by the Rule objects. Examples: custom macros, post updates to backup server, generate suggestions from analytics.

  • Minor enhancement: Added a charts panel to the GUI for visualisation of historical price data, which can be extended in the future to display other analytics e.g. candlestick graphs.

  • Code contributed: [Functional code] [Test code]

  • Other contributions:

    • Project management:

      • Managed releases v1.2 - v1.4 (3 releases) on GitHub

      • Managed issue tracker and milestone planning

      • Recorded issues raised in team meetings into tracker

    • Changes to existing features:

      • Refactored many minor parts of the codebase to suit our new product, a good part of which was redefining existing interfaces: (Pull requests) #87, #98, #106, #108, #118

      • Made various minor visual changes to the GUI: #263

    • Bug squashing:

    • Documentation:

      • Edited layout and language of the Developer and User Guides: #115

      • Updated diagrams in the Developer Guide: #187

    • Community:

      • PRs reviewed (with non-trivial review comments): #34, #209

      • Reported bugs and suggestions for other teams in the class (examples: 1, 2, 3, 4)

    • Tools:

      • Used PlantUML for generating diagrams for the Developer Guide

      • Used AsciiDoctor PDF to render better-looking documentation over printing HTML pages from Chrome

Contributions to the User Guide

Given below are sections I contributed to the User Guide. They showcase my ability to write documentation targeting end-users.

Set up notifications notify | n [Since v1.5rc]

Format
notify CONDITION

Sets a condition that triggers a popup notification whenever the condition matches the new data for a coin after a price update. The condition query mostly follows the same format as that used in find, with the following additional options:

Notification Options Format
  • You can put + or - before specifying any of the following amounts to test its change instead of its absolute value:

    • p/PRICE: Current price, in dollars, of the coin

    • w/WORTH: How much, in dollars, the current amount held is worth at the current price

  • For example:

    • p/+1000: Current price rose $1000

    • p/->500: Current price fell more than $500

You can click on the notification pop-up to jump quickly to the coin account that triggered it.

Examples
notify h/>0

Notify when the amount held in an account is more than 0 after the update. This always triggers and hence is useless on its own, but can be combined with other conditions to restrict notifications to a smaller set of accounts

notify c/BTC w/=50

Notify when the amount worth in dollars of the BTC account is $50

notify w/+>1000 c/ETH

Notify when the amount worth in dollars of the ETH account rises by more than $1000

List added notifications listnotifs | ln [Since v1.5]

Format
listnotifs

Opens the notification list window.

Contributions to the Developer Guide

Given below are sections I contributed to the Developer Guide. They showcase my ability to write technical documentation and the technical depth of my contributions to the project.

User-Set Notifications

Current Implementation

The notification system is facilitated by a RuleBook, which is located within the Model component as part of the App data. RuleBook holds a set of rules which define conditions to trigger some predefined action when met. A RuleChecker in Logic does this work of checking rules and executing the associated actions.

Let us walk through the implementation of notifications by considering a typical scenario involving this feature. Suppose the user wants to keep track of a certain coin’s price, say BTC.

The user adds a new notification using NotifyCommand, e.g. notify c/BTC p/>15000, which sets a new notification to be triggered for when the price of BTC crosses $15000. The corresponding rule is added to the RuleBook.

Later on, the user may add other notifications. So now, there are a list of different rules stored in CoinBook. When the price data is synced with latest data from the web, whether from the regular update or triggered by the user with the sync command, a CoinChangedEvent is sent out for each updated coin.

RuleChecker catches these events, and checks against the RuleBook. If any match, the corresponding action is executed. Here, a notification pops up to alert the user.

The diagram below (Fig. 23a,23b) summarises these interactions:

NotificationsSequenceDiagram1
NotificationsSequenceDiagram2
Figure 1. Sequence Diagram for Notifications

Advanced Details and Proposed Extensions

In order to allow for future extensions, the rule system has been designed to separate, as much as possible, the construction of the rule and its later behaviour.

Structure of the Rule Class

The Rule class looks like the following:

public class Rule<T> {                    (5)

    public final Predicate<T> condition;
    public final ActionCommand<T> action; (1)

    public final RuleType type;           (2)
    public final String description;

    protected Rule(String description, RuleType type,
                   ActionParser<T> actionParser,
                   ConditionParser<T> conditionParser) {     (4)
        this.description = description;
        this.type = type;
        this.action = actionParser.parse(description);
        this.condition = conditionParser.parse(description);
    }

    @Override
    public String toString() {
        return String.format(RULE_FORMAT_STRING, type, description);   (3)
    }

    @FunctionalInterface
    protected interface ActionParser<T> {
        ActionCommand<T> parse(String args);
    }

    @FunctionalInterface
    protected interface ConditionParser<T> {
        Predicate<T> parse(String args) throws IllegalValueException;
    }

    [...]
}

The above snippet demonstrates several noteworthy points:

1 Each Rule object is a condition-action pair
2 It is completely described by its type and description
3 By storing the above two values as strings, the exact object can be fully reconstructed after restarting
4 The constructor has protected access so that this base Rule class must be extended as new rule types to be used from outside
5 The class is generic, and parameterized by the type of the object it is a condition upon, e.g. notification rules test against Coin objects

This structure follows very closely the Strategy design pattern as described in the "Gang of Four" book. It leads to the following class structure, as used above (see Fig. 24):

NotificationClassDiagram
Figure 2. Class Diagram for Rule System

An example concrete implementation is the NotificationRule class used for notifications:

public class NotificationRule extends Rule<Coin> {

    private static final ActionParser<Coin>
    parseAction = SpawnNotificationCommand::new;

    private static final ConditionParser<Coin>
    parseCondition = NotifyCommandParser::parseNotifyCondition;

    public NotificationRule(String value) {
        super(value, RuleType.NOTIFICATION, parseAction, parseCondition);
    }
}

The notification command fills in the abstract actionParser and conditionParser with functions implemented in various places, but which follow the template FunctionalInterface shown above.

conditionParser takes in the description string and returns a Predicate<T> object, which can be tested against an object of type T. This results in a true or false output depending on whether that object satisfies the condition. NotificationRule uses the same ConditionParser as in the find command, explained in the previous section.

actionParser takes in the same description string and returns an ActionCommand, which is just:

public abstract class ActionCommand<T> extends Command {
    public abstract void setExtraData(T data, BaseEvent event);
}

It adds a method atop Command objects, to receive extra data which is only known after matching the rule. An easy example is the object that triggered the rule itself, which cannot be known until it is actually tested, and that only happens after the Rule has been constructed. This is the first extra data. You shall see the reason for the second one (event) later.

Rule Checking System

The rule checker is implemented via the event-driven approach described in the previous chapter. The rule checker looks something like the below (simplified from actual):

public class RuleChecker {

    private final RuleBook rules;

    @Subscribe
    public void handleCoinChangedEvent(CoinChangedEvent cce) {
        for (Rule r : rules.getRuleList()) {
            r.action.setExtraData(cce.data, cce);

            switch (r.type) {
            case NOTIFICATION:
                checkAndFire(r, cce.data);
                break;
            [...more types in the future]
            }
        }
    }

    private static <T> void checkAndFire(Rule<T> rule, T data) {
        if (rule.condition.test(data)) {
            rule.action.execute();
        }
    }
}

An instance of RuleChecker lives inside LogicManager, at the same level of architecture as the UndoRedoStack or the CommandHistory (see here). The rule testing procedure is initiated by an event, usually capturing information about a change in the state of some objects of interest.

RuleChecker, upon capturing this event, begins looping through the list of rules, testing each one’s condition against the event data, executing the attached action if necessary. The extra data is set with the potentially matching object, as well as the event itself as alluded to above. These should consist of all necessary context for the action’s execution.

A concrete example from NotificationRule:

public class SpawnNotificationCommand extends ActionCommand<Coin> {

    private final String message;
    private Coin jumpTo;
    private Index index;

    public SpawnNotificationCommand(String message) {
        this.message = message;
    }

    @Override
    public void setExtraData(Coin data, BaseEvent event) {
        jumpTo = data;
        index = ((CoinChangedEvent) event).index;
    }

    @Override
    public CommandResult execute() {
        EventsCenter.getInstance().post(new ShowNotificationRequestEvent(
            message, index, jumpTo.getCode().toString()));
        return new CommandResult("");
    }
   }
}

This sends yet another event, ShowNotificationRequestEvent, along with some data about the coin that matched the notification rule. The event is then captured by the UI component, which responds by creating a pop-up notification to show to the user.

Proposed Extensions

With the above in mind, you should be able to see that it is relatively easy to add other kinds of rules to CoinBook. For example:

  • Scheduled tasks

    • Condition: Time-based

    • Action: User-set, e.g. run the sync command, export data to file and mail, etc.

  • Webhook rules

    • Condition: Similar to notifications

    • Action: Instead of just popping-up a notification, post some preformatted data to a URL

  • Associated actions

    • Condition: Action made upon some coin account

    • Action: Another action which depends on the first is generated and executed upon predefined targets

Design Considerations

Aspect: Implementation of rules
  • Alternative 1 (current choice): Make them generalized trigger-action pairs

    • Pros: Later extension is easy to make by simply defining new condition and action parsers along with associated input.

    • Cons: This design pattern (Strategy pattern) is more advanced and may be hard for new developers to understand. Code readability may also suffer.

  • Alternative 2: Make only a single NotificationRule type

    • Pros: This is quicker to implement as the behaviour is hard-coded.

    • Cons: It will be more difficult to extend rules to handle other events in the future.

RuleBook is made general-purpose and it can hold other types of rules for future extensions, e.g. Automated Task Rules, etc.
Aspect: Type of notification to use
  • Alternative 1: Use platform-side notifications, e.g. system tray

    • Pros: The user can integrate these into their own workflow, such as setting other programs to listen in on system notifications and forward them to their other device, generate emails, take actions, etc. The user has some control over how notifications look and behave.

    • Cons: They may not work on every platform; they are heavily dependent on implementation of Java features. For example, some distributions of Linux may not include system trays.

  • Alternative 2 (current choice): Use application-side notifications.

    • Pros: This will only require the same framework which displays the App window itself, so is guaranteed to work alongside the App.

    • Cons: There will be less flexibility in customisation and availability of integration into user’s preferred workflow.

Aspect: Undo-ability of NotifyCommand
  • Alternative 1 (current choice): Leave it non-undoable

    • Pros: Notification rule data can be kept separate from coin data.
      The basic functionality of RuleBook is implemented inside Model which deals with data, but the rules are instantiated only in the Notifications component itself. A rule manager window will be available for editing or deleting existing notifications.

    • Cons: The UI will be less intuitive as users have to manage coins and rules in slightly different manners.

  • Alternative 2: Make it an UndoableCommand just like add, edit, etc.

    • Pros: This offers an intuitive, single interface for similar operations.

    • Cons: This would increase the coupling between the coin data and the rule data parts of Model, since the current implementation of UndoableCommand requires saving the state of Model, which is a wrapper for just the coin data.
      We want to keep Model as an interface for just the coin data itself.