2
\$\begingroup\$

Below is a simple Trade class with minimal business logic: primarily a method to get market value associated with the trade. But the trade needs the following pieces of core functionality, orthogonal to any business logic:

  • Need to be serializable to/from JSON (toJson/fromJson)
  • Need to be sorted in lists by date (compareTo)
  • Need to be copyable (copy with TradeBuilder to make copy more useful)
  • Need to be stored as key in map (hashCode and operator==)

Notes:

  • The Date class is just a wrapper that wraps DateTime
  • ebisu_utils provides very basic support for JSON serialization
  • The code was code generated - and given the boilerplate it is understandable why

In this context trades do not have unique id's, so storing them as keys in a map may be suspect. While it is possible to have two trades with all the same fields actually be unique - ignore that for this purpose and assume it can not happen.

I believe the functionality works fine as is. But, here are some questions:

  • What are better approaches for any of these functions?

  • With reflection could some/much of this boilerplate code could be removed?

    • If so - can it be done with no runtime cost for both Dart and transpiled JavaScript?
  • What are the performance considerations of these implementations?

  • I think there are not ways to guarantee true immutability in Dart objects. Making all fields final is a signal that trades are intended/effectively immutable. Are there many holes in this?

  • Is the TradeBuilder overkill? It is there because of the quasi-immutability aspect of Trade. So if you wanted to unwind a buy, creating a comparable sell would be:

    final sell = (new TradeBuilder.copyFrom(buy)
    ..tradeType = SELL).buildInstance();
    

Finally, in terms of clutter, would it be better to move much/some of this code out of the class and into a separate set of related functions within the library?

class Trade implements Comparable<Trade> {

  const Trade(this.date, this.symbol, this.tradeType, this.quantity, this.price);

  bool operator==(Trade other) =>
    identical(this, other) ||
        date == other.date &&
      symbol == other.symbol &&
      tradeType == other.tradeType &&
      quantity == other.quantity &&
      price == other.price;

  int get hashCode {
    int result = 17;
    final int prime = 23;
    result = result*prime + date.hashCode;
    result = result*prime + symbol.hashCode;
    result = result*prime + tradeType.hashCode;
    result = result*prime + quantity.hashCode;
    result = result*prime + price.hashCode;
    return result;
  }

  int compareTo(Trade other) {
    int result = 0;
    ((result = date.compareTo(other.date)) == 0) &&
    ((result = symbol.compareTo(other.symbol)) == 0) &&
    ((result = tradeType.compareTo(other.tradeType)) == 0) &&
    ((result = quantity.compareTo(other.quantity)) == 0) &&
    ((result = price.compareTo(other.price)) == 0);
    return result;
  }

  copy() => new Trade._copy(this);
  final Date date;
  final String symbol;
  final TradeType tradeType;
  final double quantity;
  final double price;

  // custom <class Trade>

  get signedQuantity => tradeType == BUY? quantity : -quantity;

  get marketValue => quantity * price;

  toString() => '($_symbolTxt$_buyOrSellTxt $quantity@$price $date)';

  get _symbolTxt => symbol == null? '' : '$symbol: ';
  get _buyOrSellTxt => tradeType == BUY? 'B' : 'S';

  // end <class Trade>

  Map toJson() {
    return {
      "date": ebisu_utils.toJson(date),
      "symbol": ebisu_utils.toJson(symbol),
      "tradeType": ebisu_utils.toJson(tradeType),
      "quantity": ebisu_utils.toJson(quantity),
      "price": ebisu_utils.toJson(price),
    };
  }

  static Trade fromJson(Object json) {
    if(json == null) return null;
    if(json is String) {
      json = convert.JSON.decode(json);
    }
    assert(json is Map);
    return new Trade._fromJsonMapImpl(json);
  }

  Trade._fromJsonMapImpl(Map jsonMap) :
    date = Date.fromJson(jsonMap["date"]),
    symbol = jsonMap["symbol"],
    tradeType = TradeType.fromJson(jsonMap["tradeType"]),
    quantity = jsonMap["quantity"],
    price = jsonMap["price"];

  Trade._copy(Trade other) :
    date = other.date,
    symbol = other.symbol,
    tradeType = other.tradeType == null? null : other.tradeType.copy(),
    quantity = other.quantity,
    price = other.price;

}

class TradeBuilder {

  TradeBuilder();

  Date date;
  String symbol;
  TradeType tradeType;
  double quantity;
  double price;

  // custom <class TradeBuilder>
  // end <class TradeBuilder>
  Trade buildInstance() => new Trade(
    date, symbol, tradeType, quantity, price);

  factory TradeBuilder.copyFrom(Trade _) =>
    new TradeBuilder._copyImpl(_.copy());

  TradeBuilder._copyImpl(Trade _) :
    date = _.date,
    symbol = _.symbol,
    tradeType = _.tradeType,
    quantity = _.quantity,
    price = _.price;
}

class TradeType implements Comparable<TradeType> {
  static const BUY = const TradeType._(0);
  static const SELL = const TradeType._(1);

  static get values => [
    BUY,
    SELL
  ];

  final int value;

  int get hashCode => value;

  const TradeType._(this.value);

  copy() => this;

  int compareTo(TradeType other) => value.compareTo(other.value);

  String toString() {
    switch(this) {
      case BUY: return "Buy";
      case SELL: return "Sell";
    }
    return null;
  }

  static TradeType fromString(String s) {
    if(s == null) return null;
    switch(s) {
      case "Buy": return BUY;
      case "Sell": return SELL;
      default: return null;
    }
  }

  int toJson() => value;
  static TradeType fromJson(int v) {
    return v==null? null : values[v];
  }

}

const BUY = TradeType.BUY;
const SELL = TradeType.SELL;
\$\endgroup\$
1
  • \$\begingroup\$ Alex Tatumizer pointed out an issue with hashCode. It needs to apply mask to prevent going beyond smi. const MASK=(1<<31)-1; result = ((result*prime)&MASK + date.hashCode)&MASK; This is tedious - so a better solution is to use pub package quiver to help generate the hash. \$\endgroup\$ Commented Apr 30, 2014 at 11:47

1 Answer 1

1
\$\begingroup\$

Here are some first pass thoughts:

Initial thoughts...

  • Your whitespace isnt consistent with the dartstyle guide.

  • reflection probably is overkill here and has significant binary size implications when complied to JS.

  • Trade wont benefit from a const constructor as I suspect you will encounter new trades during the life of the app and I believe that const constructor executions must be known at compile time. (See: https://stackoverflow.com/questions/21744677/dartlang-const-constructor-how-is-it-different-to-regular-constructor)

  • final is a strong immutability guarantee - though keep in mind once compiled to js it wont protect you 100% from determined malicious code on the same page

  • Move your fields to the top of your class

  • Put your constructors one after another

  • your getters dont appear to have return types whereas most of your other methods do, was that intentional? Dart style guide would suggest that public methods should declare return types.

  • Your Trade.toString format may be an issue when debugging because if either of the the fields are empty you may not be obvious. If you need this particular format for something else to consume then you are stuck. Normally I like to label each field so it is unambiguous.

  • Map.toJson should use => since it is a single line return function. Also, why dont you just call ebisu_utils.toJson(this) ?

  • I would use constants to define these fields so you dont have to repeat them everywhere.

  • the fromJson constructor should probably be a named constructor or at least a factory constructor

  • TradeBuilder seems like overkill when you could just use a factory constructor on Trade with optional params

  • your trade type enum should include a name field as well as the value/ordinal so that you dont have to switch in the toString

  • I dont think you need to copy a enum, they are considered constants.

  • I dont understand why you are exporting BUY and SELL as consts when the are already enums

\$\endgroup\$
1
  • \$\begingroup\$ Regarding 'constructors one after another', private constructors are toward the bottom. If public fields go at top is recommended I'll change. A const ctor can be used with new (dynamic) or const (compile time) - so is more flexible? Not understanding using 'constants to not repeat fields' bit. Enum needs copy because the generator creating Trade does not know TradeType is an Enum and special so it copies like it would non-primitives - so therefore uniformity. BUY/SELL exported so can be referenced without type. I will work on your other suggestions. \$\endgroup\$ Commented Apr 30, 2014 at 11:37

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.