- はじめに
- データとロジックを別のクラスに分けることがわかりにくさを生む
- データとロジックを一体にして業務ロジックを整理する
- 三層の関心事と業務ロジックの分離を徹底する
- 参考
--
https://www.amazon.co.jp/dp/B073GSDBGT/
--
- データとロジックを別のクラスに分けることがわかりにくさを生む
- データとロジックを一体にして業務ロジックを整理する
- 三層の関心事と業務ロジックの分離を徹底する
--
- 本の内容ほぼそのまま沿って進めます
- 今までの勉強会でドメインオブジェクトについてはなんとなくわかったと思う
- が、このままでは実際に使う事はないよね?
- 利点であったり、どうやって使っていくかその辺りの内容を説明する
--
- ソフトウェアアーキテクチャパターン
- プレゼンテーション層
- ユーザインタフェース
- アプリケーション層
- ビジネスロジック
- データソース層
- データの入出力
--
- 分析業務での適用は難しい?
- 個人的には全然適用可能だとは思っているが、全然別の話になりそう
- 興味のある方は調べると良いと思います
- → データとロジックを別のクラスに分けることがわかりにくさを生む
- データとロジックを一体にして業務ロジックを整理する
- 三層の関心事と業務ロジックの分離を徹底する
--
- 機能クラス
- 注文登録クラス
- 注文内容の確認
- 注文内容の記録
- 受注の通知
- 注文登録クラス
- データクラス
- 注文データ
- 商品
- 数量
- 届け先
- 注文データ
--
- Data Transfer Object(DTO)
- Entity クラス
- From クラス
- JavaBeans
※Java 界隈だと一般的
--
- ちょっと考えてみる
- 注文登録をする機能を作る
- 引数である注文データはどういう型で渡される?
--
https://docs.python.org/ja/3/tutorial/datastructures.html
--
- とりあえず dict に入れる
- 1 つの dict に色んなものが入っている
- 1 つの dict を使いまわす
--
- dict に何が入っているかわからない
- Key のタイポ
- 型がわからない
--
- 変更箇所を特定するために広い範囲を調べる
- 1 つの変更に対してあちこちの修正が必要
- 変更の副作用の確認が大変
--
- 同じ業務ロジックがあちこちに重複して書かれる
- どこに業務ロジックが書いてあるか見通しが悪くなる
※dataclass とは異なります
--
- データを同じように処理したい箇所が複数あれば処理は重複する
※詳細は割愛
--
- common や util
--
- 共通関数であっても完全に同じでない事はよくある
- フィルタリングしたい
- Key が異なる
- ユニークではない
- などなど
--
--
- 引数にフラグやオプションを追加
- ↑ を理解するのが困難
- 結局どれを設定すれば良いかわからない
- 使われない
- 同じようなロジックがあちこちに書かれる
--
- 数が多すぎる
- 違いがわからない
- 使われない
- 同じようなロジックがあちこちに書かれる
--
- util にある事を知らない
- 自分から util を見に行かない
- util だけ見ても良くわからないので頭に残らない
- データとロジックを別のクラスに分けることがわかりにくさを生む
- → データとロジックを一体にして業務ロジックを整理する
- 三層の関心事と業務ロジックの分離を徹底する
--
- データと処理を 1 つのクラスにまとめる
- データは何らかの処理で使用するためにある
- データと処理を同じ場所に置いておくことで処理の重複がなくなる
- 使う側に処理を書く必要がなくなるため
- → 使う側のコードがシンプルになる
--
- データを使う側のコードがシンプルになるように設計する事
--
- メソッドをロジックの置き場所にする
- ロジックをデータを持つクラスに移動する
- 使う側のクラスにロジックを書き始めたら設計を見直す
- メソッドを短くしてロジックの移動をやりやすくする
- メソッドでは必ずインスタンス変数を使う
- クラスが肥大化したら小さく分ける
- パッケージを使ってクラスを整理する
--
xxx_param = {
data_type = 'user_attribute',
...
}
from dataclasses import dataclass
@dataclass
class XxxParam:
data_type: str
--
xxx_param = XxxParam(data_type='user_attribute')
user_attribute_path = f's3://backet/{xxx_param.data_type}/'
user_attribute_df = spark.read.parquet(user_attribute_path)
※結局あちこちで同じコードが書かれる
--
from dataclasses import dataclass
@dataclass
class XxxParam:
data_type: str
@property
def dataframe(self):
return spark.read.parquet(f's3://backet/{self.data_type}/')
xxx_param = XxxParam(data_type='user_attribute')
user_attribute_df = xxx_param.dataframe
--
- インスタンス変数を使って加工や計算を行う
- インスタンス変数を返すだけでは意味が薄い
--
- まずは dataclass に移行
- dict から脱却
--
- データを取得するという事はそれを使って処理をする
- その処理をクラスに移動する
--
- データを持つ側にロジックが増える
- データを使っていた側からロジックが減る
- 使う側はデータを取得するのではなく、処理結果を受け取るようになる
--
- データを持つクラスに業務ロジックを集めることが、コード重複や散在を防ぐオブジェクト指向の基本
- 集める事で重複がなくなる
- 重複がなくなると変更の対象個所をそのクラスに限定できる
--
- 処理が正しく行われるのであれば、ロジックはどこに書いても良いわけではない
- ロジックが書かれる適切な場所を決めるのが設計
- データを持ち、データを使った判断/加工/計算ロジックを書くのがオブジェクト指向の設計の基本
- 結果的に便利な部品となる
--
- 問題はとりあえず動くようになった後
- データを持つ側にロジックを移動し設計改善を続けていく
- 変更容易性を大きく左右する
--
- データを取得してそのデータを使って判断/加工/計算ロジックを書き始めたら 何か変だ と考える
- ロジックを書く場所が変だと思ったら TODO コメントを残す
--
- 最初から良い設計はみつからない
- ロジックの置き場所や、クラス名、関数名の改善を続ける必要がある
--
- 小さく分けて独立させる
- そのクラスにふさわしくないコード群を発見しやすくなる
--
class A:
def total(unit_price: int, quantity: int):
return unit_price * quantity
※引数だけを使用しているため class に属している意味がない
--
- インスタンス変数を使っていない関数はそのクラスにある理由がない
- 後から見た時にこの関数がどこにあるかわからなくなる
--
- 一旦、関数を使用する側に移動
- 引数で渡されるデータを持つ側に移動する事を検討
--
- 特にインスタンス変数が多い場合
- 大きくなると見通しが悪くなる
- 変更時の副作用の検証範囲が広がる
--
- インスタンス変数とメソッドの関係を調査
- メソッドがどのインスタンス変数を使用しているか
- メソッドをグループ化
- グループ化した変数群を別クラスに切り出す
--
from dataclasses import dataclass
@dataclass(frozen=True)
class Customer:
first_name: str
last_name: str
postal_code :str
city: str
address: str
telephone: str
mail_address: str
telephone_not_preferred: bool
@property
def full_name(self):
return f'{self.first_name} {self.last_name}'
... # 色々なメソッド
--
customer = Customer(
first_name='yamada',
last_name='taro',
postal_code='1234567',
city='tokyo',
address='',
telephone='123456789',
mail_address='info@example.com',
telephone_not_preferred=True,
)
customer.full_name
--
from dataclasses import dataclass
@dataclass(frozen=True)
class PersonName:
first_name: str
last_name: str
@property
def full_name(self):
return f'{self.first_name} {self.last_name}'
--
person_name = PersonName(first_name='yamada', last_name='taro')
person_name.full_name
--
同じように Address と ContactMethod に分離する
--
from dataclasses import dataclass
@dataclass(frozen=True)
class Customer:
person_name: PersonName
address: Address
contact_method: ContactMethod
--
- データとロジックが密接に関係したオブジェクト
- 独立性が高く再利用しやすい
- 意図が明確で使いやすい
- クラス内部の変更が他のクラスに影響しにくい
--
- 関連性が強いデータとロジックだけを集めたクラスを凝集度が高いと言う
- 凝集度が高い=疎結合
--
- 変数とメソッドの関係に注目してクラスを作るとクラスが増える
- クラスが増えると見つけにくくなる
- パッケージを使用して整理する
--
- 関連性の強いクラスを集める
- クラス数が少なくても長いパッケージになりそうであれば階層化する
- 階層が深い、1 パッケージに 1 クラスとなっても問題ない
--
- 開発初期から適切な設計はできない
- ドメイン知識が少ないため
- パッケージ単位でのコードの整理を地道に続ける必要がある
- データとロジックを別のクラスに分けることがわかりにくさを生む
- データとロジックを一体にして業務ロジックを整理する
- → 三層の関心事と業務ロジックの分離を徹底する
--
- データを使って判断/加工/計算を行う事こそ業務ロジック
- 業務ロジックがどこに書いてあるかわかる
- 業務ロジックの変更が容易であること
- データとロジックを 1 つにまとめたオブジェクトをドメインオブジェクトと呼ぶ
- ドメインオブジェクトは小さな単位に分けて整理
--
- 受注日と今日の日付から受注日の妥当性を判断するロジック
- 単価と数量から合計価格を計算するロジック
- 数値データの価格を千円単位の文字列表記に加工するロジック
--
- 注文は需要な関心事なので早い段階でクラス化の候補となる
- ロジックの整理の単位としては大きすぎる
--
- 商品、数量、金額、納期、届け先、請求先など
- 小さな単位でドメインオブジェクトを作っていく
- 小さなドメインオブジェクトを組み合わせて注文オブジェクトを作っていく
※4 章(次回)に詳細があるはず!
--
- パッケージを使って整理する
- 単純にグルーピングするのではなく参照関係を考慮する
- 相互に関係を持つのではなく参照関係を考える
--
- 入金
- 請求
- 出荷
- 注文
- 顧客
- 商品
- 顧客パッケージから注文パッケージを参照しない
- 注文パッケージは顧客と商品パッケージを参照する
--
- どのパッケージに置くべきか
- どのパッケージの内容を参照して良いか
--
- 対象領域(ドメイン)をオブジェクトのモデルとして整理したもの
- 業務全体がどのような関心毎で成り立っているかを理解可能
--
- 各層からドメインモデルを使用する事でロジックが各層に散らばる事はない
- ロジックが重複しない
- 各層の構造がシンプルになる
※詳細は割愛
--
- データと処理を分けると変更が大変になる
- データのみを保持し使いまわすと処理の重複が増える
- コードの見通しを良くするにはデータと処理を一体にする設計を徹底する
- データと処理は近くにまとめる(ドメインオブジェクト)
- ドメインオブジェクトに処理を集める
- 業務的な判断/加工/計算ロジックをドメインモデルに任せる事でシンプルな構造になる
- 現場で役立つシステム設計の原則
- 作者による本への質問や回答
- 書評のまとめ
- [詳解] Python の dataclasses