いや~、一時期どこでも「DDD」の話題が出たり、
また、実際に現場で導入していたりということで、
ちょっと、考えてみますかねと思いまして。
DDDといっても、範囲が広すぎるので、今回はタイトル付近の話ですが。
まずは、参考元だったり、引用元だったりを。
それぞれの記事の「日時」が注目でしょうか。
やってる所は、昔からやってるでしょうからね。
DAOの作成単位について(2006-04-13)
https://qa.atmarkit.co.jp/q/564
ドメインオブジェクトとテーブルのマッピング(2009-11-27)
http://masuda220.jugem.jp/?eid=318
[ドメイン駆動設計] ウェブアプリケーションの構造について(2010-09-27)
http://d.hatena.ne.jp/j5ik2o/20100927/1285609764
ドメイン駆動設計のリポジトリパターンをプロジェクトへ持ち込む時の話(2014-6-17)
http://phpmentors.jp/post/88996330583/ddd-repository
DDDにおいてリポジトリとDBのトランザクションは切り離せないのか?(2015-09-14)
http://pospome.hatenablog.com/entry/20150914/1442210241
DDDと高負荷サービス(というか I/Oアクセス)は相性が悪いのか?(2016-06-28)
http://pospome.hatenablog.com/entry/20160628/1467121942
やはりお前たちのRepositoryは間違っている(2018-05-21)
https://qiita.com/mikesorae/items/ff8192fb9cf106262dbf
オブジェクト指向と10年戦ってわかったこと(2018-08-05)
https://qiita.com/tutinoco/items/6952b01e5fc38914ec4e
あとは、↓にソース(の一部)が出てきますが、
そのソースの全容は、以下に入っています。
「草案」だから、まだ途中ですけどね。
other/model_test at master · kitoku-magic/other · GitHub
さて、大きく分けて、3本立てでお送りしたいと思います。
1.色々なIO処理をまとめる
いや、これはモデルを物凄く抽象化して考えた時に、
「ある所から、データを取得する」「ある所に、データを保存する」となり、
その、ある所に、以下の様なストレージが入るんじゃないの?と。
主なストレージ:
RDBMS
KVS
ファイル(HDD)
メモリ
S3
例えば、経験は無いですが、プロジェクト初期において、
「いや~、データを何処から取って来るかは、RDBMSとは限らなくて、まだ決まってないんだよね」
的な話が出ても、別におかしくはないわけで。
なので、以下の様なクラス設計も、あり得るのではないかと。
interface storage_handler { // 該当のストレージからデータを取得する public function get(); // 該当のストレージにデータを保存する public function set(); }
// 汎用RDBMS abstract class rdbms_storage_handler { // トランザクションを開始する abstract public function begin(); // トランザクションをコミットする abstract public function commit(); }
// MySQL版 class mysql_storage_handler extends rdbms_storage_handler implements storage_handler { public function get() { } public function set() { } public function begin() { } public function commit() { } }
// PostgreSQL版 class postgresql_storage_handler extends rdbms_storage_handler implements storage_handler { public function get() { } public function set() { } public function begin() { } public function commit() { } }
// 汎用KVS abstract class kvs_storage_handler { // 独自のメソッドが必要なら書く }
// Memcache版 class memcache_storage_handler extends kvs_storage_handler implements storage_handler { public function get() { } public function set() { } }
// Redis版 class redis_storage_handler extends kvs_storage_handler implements storage_handler { public function get() { } public function set() { } }
ファイル(HDD)・メモリ・S3は省略。
と、こんな感じのクラス構成で。
で、処理の流れとしては、以下の様な感じで。
コントローラー(各処理の開始の起点。サービスを呼び、最後は画面を表示する)
↓
サービス(業務ロジックを書く)
↓
モデル(各ストレージに対する処理を書く)
controller内で
{ $service = new {サービス名}(new mysql_storage_handler()); $service->exec(); }
service内で
{ private $storage_handler; public function __construct(storage_handler $storage_handler) { $this->storage_handler = $storage_handler; } public function exec() { // ここで、上記のstorage_handlerを使ってモデルに対して操作を行う } }
みたく書くと、サービスのテストケースを書く時に、
コンストラクタの引数を変えれば、ストレージが変えれますよと。
要は、以下の記事の話とか。
例えばDBコネクションやストレージのパス等はReposiotoryのインターフェースからは隠蔽され、Repositoryのユーザは永続化ストレージが何であるか(例えばMySQLやRedis等)を意識することなく保存や検索の操作を行うことができるようになります。これによりRepositoryを利用するロジックは業務的な操作に集中できるようになる他、データベースの移行等の永続化層の変更が発生した際にロジックへの影響を切り離すことができるようになります。
ええ、ただ「実際に変われば」ですけどね。
なので、確率的な話と、
あと、変わった時期が、極端ですが「プロジェクト初期」なのか「納期間近」なのか。
いや、上記の通り、プロジェクト初期なら全然アリでしょうけど。
でも、これをやる動機が、
「ソースの作り的に変更に強いから、仕様は決めなくて良い」的な話だったら、
やめた方が良いですね。
変わったのが、これ「だけ」だったら、そこまでコストかからないかもしれないですけど、
こういうのは、雪崩の様に、押し寄せたりしますし。
あと、昔から思っていたんですが、
「仕様が決まらない状態」って、「無駄コード」が多くなるんですよね。
まさに、↑のソースがそうなるかも的なんですけど。
なので、別に上のコードを書いちゃダメとは思わないですけど、
それはそれとして、「仕様は決めましょう」&「納期直前にMySQL→Redis(全然違うケース)に」とか言われたら、
その分のお金と時間も頂きましょうって所じゃないでしょうかね。
これは、↓の話でも同じですけどね。
2.アクセスするリポジトリの単位
ある単位(ここがドメインか?)にまとめてリポジトリを作るか、テーブル単位で作るか?ってのが、
以下でも書かれていました。
じゃあ、どんな感じなのか、実際書いてみると、
・ある単位毎
interface order_repository { // 商品を注文する public function order(); // 商品のステータスを変更する(発送済みにしたり、キャンセルしたり) public function change_order_status($order_id); // 注文IDから注文した商品を取得する public function get_item_by_order_id($order_id); // 注文した全ての商品を取得する(自分の注文に限定した方が良いでしょうが) public function get_all_item(); }
// これもインタフェース実装しても良いけど。あとストレージはDBに固定している class base_repository { private $db_storage_handler; public function __construct(storage_handler $db_storage_handler) { $this->db_storage_handler = $db_storage_handler; } // 継承先のクラスからしかアクセスさせない protected function select() { // SELECT文を実行する // get()の中に、SQLを実行する処理を書く $this->db_storage_handler->get(); } // 継承先のクラスからしかアクセスさせない protected function insert() { // INSERT文を実行する } // 継承先のクラスからしかアクセスさせない protected function update() { // UPDATE文を実行する } // 継承先のクラスからしかアクセスさせない protected function delete() { // DELETE文を実行する } }
// order関連のデータ操作を行うリポジトリ class order_repository_impl extends base_repository implements order_repository { public function order() { // orderテーブルとorder_itemテーブルにレコードを追加する $this->insert(); } public function change_order_status($order_id) { // orderテーブルのstatusを変更する $this->update(); } public function get_item_by_order_id($order_id) { // order_itemテーブルからorder_idに該当するレコードを取得する $this->select(); } public function get_all_item() { // order_itemテーブルから全てのレコードを取得する(ユーザーIDを指定する方が良いでしょうが) $this->select(); } }
・テーブル単位毎
interface order_repository { // 商品を注文する public function order(); // 商品のステータスを変更する(発送済みにしたり、キャンセルしたり) public function change_order_status($order_id); }
interface order_item_repository { // 商品を注文する public function order_item(); // 注文IDから注文した商品を取得する public function get_item_by_order_id($order_id); // 注文した全ての商品を取得する(自分の注文に限定した方が良いでしょうが) public function get_all_item(); }
base_repositoryは、おそらく上記と同じなので省略。
// orderテーブルのデータ操作を行うリポジトリ class order_repository_impl extends base_repository implements order_repository { public function order() { // orderテーブルにレコードを追加する $this->insert(); } public function change_order_status($order_id) { // orderテーブルのstatusを変更する $this->update(); } }
// order_itemテーブルのデータ操作を行うリポジトリ class order_item_repository_impl extends base_repository implements order_item_repository { public function order_item() { // order_itemテーブルにレコードを追加する $this->insert(); } public function get_item_by_order_id($order_id) { // order_itemテーブルからorder_idに該当するレコードを取得する $this->select(); } public function get_all_item() { // order_itemテーブルから全てのレコードを取得する(ユーザーIDを指定する方が良いでしょうが) $this->select(); } }
ここは、以下の意見が、一番しっくりきたんですけどね。
https://qa.atmarkit.co.jp/q/564/a/3995/revisions
だから、テーブル単位の方がしっくりきます。
勿論、デメリット(↑のリンク先でも書かれていますが)もあるんですけど、
特に、気にならないレベルかなと思いましたし。
で、呼び出し側からは、これはINSERTの場合ですが、
controller内で
{ // DBに限定してますが $db_storage_handler = new mysql_storage_handler(); $service = new {サービス名}( new order_repository_impl($db_storage_handler), new order_item_repository_impl($db_storage_handler) ); $service->exec(); }
service内で
{ private $order_repository; private $order_item_repository; public function __construct(order_repository $order_repository, order_item_repository $order_item_repository) { $this->order_repository = $order_repository; $this->order_item_repository = $order_item_repository; } public function exec() { $this->order_repository->begin(); try { $this->order_repository->order(); $this->order_item_repository->order_item(); $this->order_repository->commit(); } catch (Exception $e) { $this->order_repository->rollback(); } } }
となるかなと。
いやいや、それだと「$this->order_item_repository->order_item();」を忘れたら(order()と必ず一緒に行う前提)、
バグるじゃないかと。
確かにそうなので、じゃあ以下かなと。
// orderテーブルのデータ操作を行うリポジトリ class order_repository_impl extends base_repository implements order_repository { private $order_item_repository; public function __construct(order_item_repository $order_item_repository) { $this->order_item_repository = $order_item_repository; } public function order() { // orderテーブルにレコードを追加する $this->insert(); // order_itemテーブルにレコードを追加する $this->order_item_repository->insert(); } // 以下、省略 }
そして、controller内で
{ $db_storage_handler = new mysql_storage_handler(); $service = new test_service( // ちょっと歪かなとは思うけど new order_repository_impl($db_storage_handler, new order_item_repository_impl($db_storage_handler)) // ↓が元コード // new order_item_repository_impl($db_storage_handler) ); $service->exec(); }
service内で
{ private $order_repository; // private $order_item_repository; // public function __construct(order_repository $order_repository, order_item_repository $order_item_repository) public function __construct(order_repository $order_repository) { $this->order_repository = $order_repository; // $this->order_item_repository = $order_item_repository; } public function exec() { $this->order_repository->begin(); try { $this->order_repository->order(); // $this->order_item_repository->order_item(); $this->order_repository->commit(); } catch (Exception $e) { $this->order_repository->rollback(); } } }
で、いけるんじゃないかと。
じゃあ、テーブル単位だと、JOINがあるSELECTはどうするのか?
class order_repository_impl extends base_repository implements order_repository { private $order_item_repository; public function __construct(storage_handler $db_storage_handler, order_item_repository $order_item_repository) { parent::__construct($db_storage_handler, 'order', array('order_id'), array('order_item' . parent::ENTITY_CLASS_SUFFIX => null)); $this->order_item_repository = $order_item_repository; } ・・・省略 // 注文IDに該当する商品名を取得する public function get_order_item_name($order_id) { $db_storage_handler = $this->get_db_storage_handler(); $db_storage_handler->set_columns('oi.item_name'); $db_storage_handler->set_from('`order` o INNER JOIN `order_item` oi ON o.order_id = oi.order_id'); $db_storage_handler->set_where('o.order_id = :order_id'); $db_storage_handler->set_bind_params(array( array( 'name' => ':order_id', 'value' => $order_id, 'data_type' => PDO::PARAM_INT, ) )); $result = $this->select(); return $result; } }
class base_repository { // 継承先のクラスからしかアクセスさせない protected function select() { // SELECT文を実行する // get()の中に、SQLを実行する処理を書く return $this->db_storage_handler->get(); } }
class mysql_storage_handler extends rdbms_storage_handler implements storage_handler { public function get() { $sql = 'SELECT ' . $this->columns . ' FROM ' . $this->from; if ('' !== $this->where) { $sql .= ' WHERE ' . $this->where; } // 以下、GROUP BYなどが続く $handle_instance = $this->get_handle_instance(); $this->statement = $handle_instance->prepare($sql); foreach ($this->bind_params as $bind_param) { $this->statement->bindValue($bind_param['name'], $bind_param['value'], $bind_param['data_type']); } $result = $this->statement->execute(); return $result; } }
で、問題無し。
で、サービスからは、
class test_service { public function exec() { $order_id = 1; $result = $this->order_repository->get_order_item_name($order_id); var_dump($result); } }
という感じで。
「JOINの部分など」を直書きするってのがどうかって所なんでしょうけどね。
↑の「ある単位」毎にってのも、「業務分析」したら、
おそらくある単位が出来てくるとは思いますけど、
これに限らず、「費用対効果」ってのがあるかなと。
保守性を重視ってのが、勿論間違ってるとは思わないけど、
その保守性が活かされる前に、サービスが終了するとか。
「中長期を気にし過ぎて、逆に短期の方がおろそかになった」と言い換えても良いかと思いますが。
逆のケースを「山程」知っているので、そういう発想が出てくるのもアリなんですけど、
「短期が存在しないと、中長期は絶対に存在しない」わけですし。
3.戻り値の情報の持つ単位(テーブル単位?)
ここも、DDDだと「集約」とかがあり、悩ましい所ですね。
で、これはこれで、何度も書いている通り、別に何かがおかしいわけではないけど、でも「費用対効果」が。
なので、以下の様な感じはどうなのかと。
あと、いわゆるGeneration Gapパターンを使ってます。
// エンティティの基底クラス abstract class entity { public function is_property_exists($field_name) { return array_key_exists($field_name, $this->get_table_columns()); } abstract public function get_table_columns(); }
// これは自動生成されるクラス class order_entity_base extends entity { private $order_id; private $order_status; // order_id public function get_order_id() { return $this->order_id; } public function set_order_id($order_id) { $this->order_id = $order_id; } // order_status public function get_order_status() { return $this->order_status; } public function set_order_status($order_status) { $this->order_status = $order_status; } public function get_table_columns() { return array( 'order_id' => null, 'order_status' => null, ); } }
// これも自動生成されるクラス class order_item_entity_base extends entity { private $order_item_id; private $order_id; private $item_name; // order_item_id public function get_order_item_id() { return $this->order_item_id; } public function set_order_item_id($order_item_id) { $this->order_item_id = $order_item_id; } // order_id public function get_order_id() { return $this->order_id; } public function set_order_id($order_id) { $this->order_id = $order_id; } // item_name public function get_item_name() { return $this->item_name; } public function set_item_name($item_name) { $this->item_name = $item_name; } public function get_table_columns() { return array( 'order_item_id' => null, 'order_id' => null, 'item_name' => null, ); } }
// ここを、自分で実装する class order_entity extends order_entity_base { private $order_item_entities; // order_item_entities public function get_order_item_entities() { return $this->order_item_entities; } public function set_order_item_entities($order_item_entities) { $this->order_item_entities = $order_item_entities; } public function get_order_item_entity($index) { return $this->order_item_entities[$index]; } public function add_order_item_entity(order_item_entity_base $order_item_entity) { $this->order_item_entities[] = $order_item_entity; } }
// 何もないけど(笑) class order_item_entity extends order_item_entity_base { }
で、さっきのJOINでSELECTしたデータを取ってみましょう。
class order_repository_impl extends base_repository implements order_repository { private $order_item_repository; public function __construct(storage_handler $db_storage_handler, order_item_repository $order_item_repository) { // 第2引数は、テーブル名。第3引数は、主キー(複合に対応する為、配列)。第4引数は、関連するエンティティクラス名(文字列) parent::__construct($db_storage_handler, 'order', array('order_id'), array('order_item' . parent::ENTITY_CLASS_SUFFIX => null)); $this->order_item_repository = $order_item_repository; } public function get_order_item_name($order_id) { // 全件取る様に変えました $db_storage_handler = $this->get_db_storage_handler(); $db_storage_handler->set_columns('*'); $db_storage_handler->set_from('`order` o INNER JOIN `order_item` oi ON o.order_id = oi.order_id'); $db_storage_handler->set_where(''); $db_storage_handler->set_bind_params(array( /*array( 'name' => ':order_id', 'value' => $order_id, 'data_type' => PDO::PARAM_INT, )*/ )); $result = $this->select(); $entities = array(); if (true === $result) { $entities = $this->fetch_all_associated_entity(); } return $entities; } }
で、この「$this->fetch_all_associated_entity()」って所は、
class base_repository { const ENTITY_CLASS_SUFFIX = '_entity'; private $db_storage_handler; private $table_name; private $primary_keys; private $associated_entities; public function __construct(storage_handler $db_storage_handler, $table_name, $primary_keys, $associated_entities) { $this->db_storage_handler = $db_storage_handler; $this->table_name = $table_name; $this->primary_keys = $primary_keys; $this->associated_entities = $associated_entities; } protected function fetch_all_associated_entity() { $this->db_storage_handler->set_repository_class($this); $this->db_storage_handler->set_entity_class_name($this->table_name . self::ENTITY_CLASS_SUFFIX); // 第2引数にfalseを設定すると、主キーが同じレコードでも、別の配列の要素となる //return $this->db_storage_handler->fetch_all_associated_entity($this->get_associated_entities(), false); return $this->db_storage_handler->fetch_all_associated_entity($this->get_associated_entities()); } }
class mysql_storage_handler extends rdbms_storage_handler implements storage_handler { const ENTITY_CLASS_DIRECTORY = '../../model/entity/'; public function fetch_all_associated_entity($associated_entities, $unique_primary_key_data = true) { $entities = array(); if (0 === count($associated_entities)) { return $entities; } if (null !== $this->statement) { // TODO: 当然、存在チェックもする require_once(dirname(__FILE__) . DIRECTORY_SEPARATOR . self::ENTITY_CLASS_DIRECTORY . $this->entity_class_name . '.php'); $main_entity = null; while (false !== ($row = $this->statement->fetch(PDO::FETCH_ASSOC))) { $entity_created = false; // 主キーが、既に設定済みのエンティティがある時に、インスタンスを再生成しない為の処理 if (true === $unique_primary_key_data && null !== $main_entity) { $row_values = array(); $entity_values = array(); $main_entity_index = array_search($main_entity, $entities, true); $primary_keys = $this->repository_class->get_primary_keys(); foreach ($primary_keys as $primary_key) { if (true === isset($row[$primary_key])) { $row_values[] = $row[$primary_key]; if (false !== $main_entity_index) { $method_name = 'get_' . $primary_key; $entity_values[] = call_user_func_array(array($entities[$main_entity_index], $method_name), array()); } } } // 取得したカラムに、全ての主キーが含まれている && // 既に設定済みのエンティティの全ての主キーの値に、nullが含まれていない && // 取得したカラムの主キーの値と、既に設定済みのエンティティの主キーの値が同じ if (count($primary_keys) === count($row_values) && false === in_array(null, $entity_values, true) && $entity_values === $row_values) { $entity_created = true; } } if (false === $entity_created) { $main_entity = new $this->entity_class_name; } // 関連するエンティティにデータを入れていく foreach ($associated_entities as $associated_entity_class_name => $value) { require_once(dirname(__FILE__) . DIRECTORY_SEPARATOR . self::ENTITY_CLASS_DIRECTORY . $associated_entity_class_name . '.php'); $associated_entity_class = new $associated_entity_class_name; foreach ($row as $column_name => $column_value) { // 関連するエンティティのフィールドに、取得したカラムが存在するか? if (true === $associated_entity_class->is_property_exists($column_name)) { $method_name = 'set_' . $column_name; call_user_func_array(array($associated_entity_class, $method_name), array($column_value)); } if (false === $entity_created && true === $main_entity->is_property_exists($column_name)) { $method_name = 'set_' . $column_name; call_user_func_array(array($main_entity, $method_name), array($column_value)); } } // 関連するエンティティに入ったデータを、メインのエンティティの配列に入れていく $method_name = 'add_' . $associated_entity_class_name; call_user_func_array(array($main_entity, $method_name), array($associated_entity_class)); } if (false === $entity_created) { $entities[] = $main_entity; } } } return $entities; } }
で、最後にサービス。
class test_service { public function exec() { $order_id = 1; $result = $this->order_repository->get_order_item_name($order_id); var_dump($result); } }
このコードで、以下のテーブルの状態の時に。
mysql> select * from `order`; +----------+--------------+ | order_id | order_status | +----------+--------------+ | 1 | 1 | | 2 | 1 | +----------+--------------+ 2 rows in set (0.00 sec)
mysql> select * from `order_item`; +---------------+----------+---------------------------------+ | order_item_id | order_id | item_name | +---------------+----------+---------------------------------+ | 1 | 1 | order_item::item_nameテスト1 | | 2 | 1 | order_item::item_nameテスト2 | | 3 | 2 | order_item::item_nameテスト3 | | 4 | 2 | order_item::item_nameテスト4 | | 5 | 2 | order_item::item_nameテスト5 | +---------------+----------+---------------------------------+ 5 rows in set (0.00 sec)
以下の様な戻り値になりますと。
array(2) { [0]=> object(order_entity)#8 (3) { ["order_item_entities:private"]=> array(2) { [0]=> object(order_item_entity)#9 (3) { ["order_item_id:private"]=> string(1) "1" ["order_id:private"]=> string(1) "1" ["item_name:private"]=> string(31) "order_item::item_nameテスト1" } [1]=> object(order_item_entity)#10 (3) { ["order_item_id:private"]=> string(1) "2" ["order_id:private"]=> string(1) "1" ["item_name:private"]=> string(31) "order_item::item_nameテスト2" } } ["order_id:private"]=> string(1) "1" ["order_status:private"]=> string(1) "1" } [1]=> object(order_entity)#11 (3) { ["order_item_entities:private"]=> array(3) { [0]=> object(order_item_entity)#12 (3) { ["order_item_id:private"]=> string(1) "3" ["order_id:private"]=> string(1) "2" ["item_name:private"]=> string(31) "order_item::item_nameテスト3" } [1]=> object(order_item_entity)#13 (3) { ["order_item_id:private"]=> string(1) "4" ["order_id:private"]=> string(1) "2" ["item_name:private"]=> string(31) "order_item::item_nameテスト4" } [2]=> object(order_item_entity)#14 (3) { ["order_item_id:private"]=> string(1) "5" ["order_id:private"]=> string(1) "2" ["item_name:private"]=> string(31) "order_item::item_nameテスト5" } } ["order_id:private"]=> string(1) "2" ["order_status:private"]=> string(1) "1" } }
まだINSERT・UPDATE・DELETE等もありますが、といっても実装出来ない感じはしないですけどね。
あと、これだと、エンティティに限らず「子クラスをいじっている分(各機能を実装している時)」には、
そんなに「複雑」じゃないと思うんですよ、「集約」に比べれば。
といっても、その複雑ってのが、人によってマチマチと分かるのが、以下なのですが。
ウェブアプリケーションの構造について - じゅんいち☆かとうの技術日誌
コメント欄の方です。
「予算のあるプロジェクト」ってのがポイントかなぁと。
気持ちはわかるのですが、こんなFATな設計を実装できるほど予算のあるプロジェクトなんて今どきないんですよね…
そんな時代だし、設計思想や言語だってどんどん変わって行っちゃうからこそ、設計的にも言語的にもライトなものが全盛なんじゃないかと思うのですが、いかがでしょうか。もうすこし現実的な「現代版ドメイン駆動設計」を披露していただけると嬉しいです。
でも、予算を取るのって、当然「成功の確度」が高くないと無理で、
で、昨今やる前から、その辺がわかるかっていうと・・・
だから、スタートアップなんかは、間違いなく合わないと思いますね。
逆に、実績があるならアリなわけですが、その場合は当然「時間に余裕がある」はず(何故かないってのも見ますが)で、
DDDのデメリット部分も緩和する事が出来るんじゃないかなと。
逆に、肯定的な意見もあり、
「実は FAT ってコードだけでなく設計やマネジメントなど全般的に考えないといけないテーマかもしれませんね。」
何を持って FAT なのかってことだと思うので、人によっては FAT に見えるのは理解できます。
でもその FAT って部分的にトラウマ(色眼鏡)が含まれていることってありえませんか?個人的には FAT には見えないですね。私の理解では、個別のソースはかなりすっきりすると思います。
実は FAT ってコードだけでなく設計やマネジメントなど全般的に考えないといけないテーマかもしれませんね。
どこかにゆがみがあるから、FAT に見えてしまうと思います。
ええ、そうじゃないですかね、まさに↑がその辺の話かなと。
あと、クラスが多いから、IDEがないとキツイとかも考える必要があるかなと。
あと、「でもその FAT って部分的にトラウマ(色眼鏡)が含まれていることってありえませんか?」って所。
うん、EJB(幸い、業務で触ることは無かったですが)とかね(苦笑)
あと、気になったコメントは、
う~ん、テーブルは、正規化目的じゃないなら、細かくしない方が良いと思いますけどね。
UserTableにユーザプロファイル情報を含めることはできます。
しかし、カラムの数が多い場合は目的の情報を探すのが困難になるので、
個人情報に関連する情報はプロファイルという別のテーブルに切り出した方が管理しやすいというモチベーションですね。
以下に、その辺が言及されてましたが。
ドメインオブジェクトとテーブルのマッピング | システム設計日記
DDDと高負荷サービス(というか I/Oアクセス)は相性が悪いのか? - pospomeのプログラミング日記
あと、クラス数が増えるって事については、以下の観点もあるかなと思いますね。
ドキュメントが充実(ここも時間がかかる)していればアリかとは思いますけど。
マジックナンバー7の誤解 : みんな、他人の頭を過大評価しすぎているんじゃないだろうか。それって不幸だ。 - たたたた。
あと、乱用って意味だと、以下かなぁと。
「全部」のswitch文にstateパターンを適用するの?的な。
switch文を使ってはいけない: Architect Note
いや、stateパターン、以下とか、むしろ好きですけどね(笑)
https://github.com/kitoku-magic/final_magic/blob/master/view.php#L141
あと、複雑なSELECT文についてって所だと、
恐らく、ユーザのIDをキーに購入履歴テーブルをジョインし、購入日で絞り込んだ件数をcountするクエリをORMで頑張って実現しようとするでしょう。
これらのクエリは大抵プログラム的にもメンテナンスしづらいものとなり、更にユーザ一覧画面などで複数件実行するとパフォーマンス上の問題を引き起こしたりします。
↓
2) CQS(Command Query Separation)を適用してクエリを切り出す
上の問題に対して、下を適用すれば解決するって所に、因果関係があるのか?という。
別のクラスに切り出しただけでは?と。
なんか、「複雑化を排除しようとしたら、また別の形で複雑化した」感がありますね。
「複雑なものを単純に」って響きは良いですが、出来たら天才クラスだと思うので、
「複雑なものは複雑」で良いと思ってるんですけどね。
あと、オブジェクト指向全般としては、以下が大切かなと。
「どこが変わりそうか?」ってのは、「経験値」が問われそうな所だと思いますけどね。
今回例に取り上げたのは、ファーストフードのシステムなので「食品以外のものを販売する」というような変更が発生することは、あまり考えられません。「もしかしたらガソリンやタイヤを販売するかもしれないじゃないか!可能性はゼロではないのだから柔軟に対応できるように設計するべきだ!」と思うかもしれません。しかし、将来的に変更のない箇所を無駄に柔軟に設計することは過剰実装であり、プロジェクトの設計を複雑なものに変えてしまうため、行わない方が良いこともあるのです。
なので、「そう簡単に白黒付けられる話ではない(白黒付けるのは否定的じゃないですよ)」と思ってますけどね。
これで終わりです。
あと、GitHubにも上がってますが、UMLツールを探していました。
Miracle Jungle.: フリーのUMLツールをいくつか試してみた
katapedia/リバースエンジニアリングツール.md at master · yutakatay/katapedia · GitHub
「umbrello」が良いんじゃないかと思ったんですが、継承先が複数ある時に、何故か線が引けないんですよね。
BOUMLの方が良いのかもしれない。
あと、まだあのソースは途中なので、完成したら再度告知すると思います(笑)
あと、全然関係ないですが、椎名林檎のアイデンティティ http://j-lyric.net/artist/a00450a/l000c60.html 流しながら書いてました(笑)