DEV Community

Ron Gonzalez Lobo
Ron Gonzalez Lobo

Posted on

Dart Quick Tip: Lookup Tables

Hey there,

While reading about refactoring to collections in another programming language recently, I was curious how the concept could be applied in Dart.

Consider the following example which is taken from a recently published cqrs library:

part of 'cqrs_example.dart';

///
/// AccountAggregate, a unit for data manipulation and management of
/// consistency(*1) using the account model as aggregate root.
///
class AccountAggregate extends Aggregate<Account> {
  final String name = 'account';

  @override
  Account initializeModel() => Account();

  ///
  /// Applies domain events to the account domain model emitted by the commands
  ///
  @override
  Future<void> apply(Account model, DomainEvent event) async {
    if (event is AccountCreatedEvent) {
      model.id = event.id;
      model.owner = event.owner;
      model.amount = 0.0;
    } else if (event is DepositPerformedEvent) {
      model.amount += event.amount;
    } else if (event is WithdrawalPerformedEvent) {
      model.amount -= event.amount;
    } else {
      throw Exception("Unknown event!");
    }
  }

  ///
  /// Handles commands to the account domain model
  ///
  @override
  Future<void> handleCommand(
      Command cmd, Account model, CommandOutput out) async {
    if (cmd is CreateAccountCmd) {
      if (model.id != null) {
        out.setError("Model with id ${cmd.modelId} already exists!");
        return;
      }
      out.addEvent(AccountCreatedEvent(id: cmd.modelId, owner: cmd.owner));
    } else if (cmd is DepositCmd) {
      out.addEvent(DepositPerformedEvent(id: cmd.modelId, amount: cmd.amount));
    } else if (cmd is WithdrawCmd) {
      if (model.amount < cmd.amount) {
        out.setError("Not enough balance!");
        return;
      }
      out.addEvent(
          WithdrawalPerformedEvent(id: cmd.modelId, amount: cmd.amount));
      return;
    } else {
      throw UnsupportedError(cmd.runtimeType.toString());
    }
  }
}

The class AccountAggregate exposes two public methods to handle commands and apply domain events. Both methods heavily rely on conditional checks to perform the desired business logic.
Imagine we want to add more commands and events in the future resulting in a bigger and more unreadable if/else trunk. How can we beautify this?

Lookup tables to the rescue:

part of 'cqrs_example.dart';

class AccountAggregate extends Aggregate<Account> {
  final String name = 'account';

  final Map<String, Function> _events = {
    "$AccountCreatedEvent": _applyAccountCreated,
    "$DepositPerformedEvent": _applyDepositPerformed,
    "$WithdrawalPerformedEvent": _applyWithdrawalPerformed,
  };

  final Map<String, Function> _commands = {
    "$CreateAccountCmd": _handleCreateAccount,
    "$DepositCmd": _handleDeposit,
    "$WithdrawCmd": _handleWithdraw,
  };

  @override
  Account initializeModel() => Account();

  @override
  Future<void> apply(Account model, DomainEvent event) async =>
      _events.containsKey(event.runtimeType.toString())
          ? await Function.apply(
              _events[event.runtimeType.toString()], [model, event])
          : throw Exception("Unknown event!");

  @override
  Future<void> handleCommand(
          Command cmd, Account model, CommandOutput out) async =>
      _commands.containsKey(cmd.runtimeType.toString())
          ? await Function.apply(
              _commands[cmd.runtimeType.toString()], [cmd, model, out])
          : throw UnsupportedError(cmd.runtimeType.toString());

  static Future<void> _handleCreateAccount(
      CreateAccountCmd cmd, Account model, CommandOutput out) async {
    if (model.id != null) {
      out.setError("Model with id ${cmd.modelId} already exists!");
      return;
    }
    out.addEvent(AccountCreatedEvent(id: cmd.modelId, owner: cmd.owner));
  }

  static Future<void> _handleDeposit(
      DepositCmd cmd, Account model, CommandOutput out) async {
    out.addEvent(DepositPerformedEvent(id: cmd.modelId, amount: cmd.amount));
  }

  static Future<void> _handleWithdraw(
      WithdrawCmd cmd, Account model, CommandOutput out) async {
    if (model.amount < cmd.amount) {
      out.setError("Not enough balance!");
      return;
    }
    out.addEvent(WithdrawalPerformedEvent(id: cmd.modelId, amount: cmd.amount));
  }

  static Future<void> _applyAccountCreated(
      Account model, AccountCreatedEvent event) async {
    model.id = event.id;
    model.owner = event.owner;
    model.amount = 0.0;
  }

  static Future<void> _applyDepositPerformed(
      Account model, DepositPerformedEvent event) async {
    model.amount += event.amount;
  }

  static Future<void> _applyWithdrawalPerformed(
      Account model, WithdrawalPerformedEvent event) async {
    model.amount -= event.amount;
  }
}

In the refactored version two maps are introduced for each possible commands and the corresponding events, using a separate method for a specific command to be handled and event to be applied.
This will not only make it easier to add new commands and events, we are also able to mock and test every command and event method on its own.

Feedback and thoughts appreciated :)

P.S.: Thanks Ravi Teja Gudapati for the cqrs library!

Links

(*1) NoSQL Distilled: A Brief Guide to the Emerging World of Polyglot Persistence

Top comments (2)

Collapse
 
creativ_bracket profile image
Jermaine

Thanks Ron. Would be helpful to have some comments in the snippet to know what's happening, possibly explain CQRS.

Collapse
 
ronlobo profile image
Ron Gonzalez Lobo

Good catch, will add some explanation comments to the snippet shortly.
To get a better understanding about CQRS itself, please checkout the article which was written by Martin Fowler.