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;