僕がモデリング関数と呼んでいる概念の話をしたい。
DBから落としてきたデータをもとにビジネスロジックを書くと、関数の引数が増えすぎてうざくなる経験はないだろうか。
例えばTwitterを作っているとして、AさんがBさんをフォローしているか判定する関数はこうだ。
(followsはFollowテーブルのレコードの配列だ)
if(isFollow(toUserID, fromUserID, follows)){
// 処理
}
投稿数が5以上のユーザーをフィルターするのはこうだ。
users = filterPostCount(users, posts, 5);
どうも汚い。
オブジェクト指向言語で書く以上こういう風に書きたい。
if(user.isFollow(toUserID)){
// 処理
}
users = users.filterPostCount(5);
これを実現するためにはそもそも生データ用のクラスとビジネスロジックで使う用のクラスを分けるといい。僕は前者をEntity、後者をModelと呼び、EntityをModelに変換する関数をmodeling関数と呼んでいる。
EntityはDBのスキーマと1:1で対応し、ORMがインスタンスを作ってくれるものだ。
// エンティティ
class UserEntity {
String userID , name;
}
class FollowEntity {
String to , from;
}
// モデル
class UserModel {
String userID , name;
List<FollowEntity> follows;
bool isFollowTo(String to) {
return follows.where((f ) => f.to == to).isNotEmpty;
}
}
// モデリング関数
List<UserModel> modeling(List<UserEntity> userEntity , List<FollowEntity> follows ){
// ここにいろんなフィルターとかマップとか
}
これで目的が達成できた。
欲を言えばEntityとModelを一つのオブジェクトツリーにぶら下げることで、全てのmodeling関数を共通化することができる。そうすれば、どんな時もとりあえずEntityを取得して一種類のmodeling関数にかければいいということになりわかりやすい。
class Entities {
List<UserEntity> users;
List<FollowEntity> follows;
// その他いろんなエンティティ
}
class Model {
List<UserModel> users;
// その他いろんなモデル
}
// モデリング関数
List<Model> modeling(Entities entities ){
// ここにいろんなフィルターとかマップとか
}
この方法のメリットは「そのオブジェクトに関することは、そいつに聞けばなんでもわかる」状態になりコードが綺麗になることだ。
デメリットは、そのビジネスロジックに必要のないところまでモデリングするためリソースが無駄になることだ。
注意点は、Modelに生やすメソッドはそのModelの状態を書き換えてはいけないということだ。単に聞かれたことに答えるだけの関数を書こう。
このような反論が想定される。クラス自体を分けずにEntityにメソッドを生やすだけではダメなのか。それをしようとするといつどのタイミングで関連するEntityをそのEntityに配ればいいのかわからないからダメだ。
今回はサーバーサイドを例に説明したが、本質的にデスクトップアプリでもWebアプリでも使えるテクニックだ。僕はReduxでmapStateToPropsとReducerの先頭でStateをModelに変換している。
考え方の背景を説明しよう。データを保持するということとそのデータをもとに何かを判断するということでは、求められるデータ構造が異なる。
前者は重複がなく、極力1操作で状態を更新でき、矛盾しづらいデータ構造だ。
後者はそのオブジェクトに関することはなんでも知っている方がよく、データの重複は問題ではない。Twitterのフォロワー・フォロイーの話なら、AさんオブジェクトがBさんをフォローしているという情報を持っておりBさんオブジェクトもAさんにフォローされているという情報を持っていてもかまわない。
f(保持用のデータ) = 判断用のデータ
の関係になっており、判断用のデータは毎回保持用のデータから作成されそして使い捨てになっているといい。
Active Recordとかの話は長くなりそうなので省略した