概要
「良いコード/悪いコードで学ぶ設計入門」を読んで大事そうなところをメモしておく。
書籍中で書かれているコードを一部Pythonに変換してみる。
クラス設計(第3章)
- インスタンス変数とメソッドの両方をクラス内で作成する
- インスタンスを生成する。基本的にクラスメソッドを使わない
- インスタンス生成時に不正値をチェックする。不正値の場合、例外をスローする
- メソッドの先頭に定義する処理対象外条件をガード節と呼ぶ
class StorePoint(object): def __init__(self, point: int): if point < 0: raise ValueError self.__point = point
- インスタンス変数をイミュータブルにし、再代入を禁ずる(Pythonだと、
self.__variable
のようにアンダースコア2つで変更不可を明示する) - インスタンス変数を変更せずに、新しいインスタンスを生成して
return
で返す
class StorePoint(object): def __init__(self, point: int): self.__point = point def add(self, additional_point: int): point = self.__point + additional_point return StorePoint(point) def show(self): print("あなたのポイントは ", self.__point, " ptです!")
- パフォーマンスを重視するケースでは例外的にインスタンス変数への再代入(可変)を認める
class StorePoint(object): def __init__(self, point: int): self.__point = point def add(self, additional_point: int): self.__point += additional_point def show(self): print("あなたのポイントは ", self.__point, " ptです!")
int
やstr
などのプリミティブ型を使用せずに、独自のクラス(値オブジェクト)を作成し引数に渡すことで、引数の渡し間違いを防ぐ
class StorePoint(object): def __init__(self, point: int): self.__point = point def add(self, store_point_class: cls): point = self.__point + store_point_class.__point return StorePoint(point) def show(self): print("あなたのポイントは ", self.__point, " ptです!")
関数(メソッド)の主作用と副作用
主作用
- 関数が値を返す
副作用:
- 状態変更をすること
- ※ 関数の外にある状態を変更すること。例:インスタンス変数、引数
- ※ 関数内のローカル変数の変更は副作用とは言えない
影響を限定しよう
- インスタンス変数の値を変えてしまうと、結果の予測が難しく、保守が大変になる
- データ(状態)を引数で受け取り、状態を変更せずに、値を関数の戻り値として返すのが理想的
staticメソッド(クラスメソッド)
- staticメソッドはインスタンス変数を使えないので、データとデータ操作のロジックが乖離するので、低凝集になってしまう
class TestClass(object): @classmethod def TestMethod(): pass
- ログ出力用メソッドや、フォーマット変換用メソッドなど凝集度に無関係なものに使う
- あるいは、ファクトリメソッドとしてstaticメソッドを使う
共通処理クラス(Common, Util)に気をつける
- CommonやUtilというクラスに共通処理用のメソッドを置きがち
- データとロジックがセットになっていないので低凝集になりがち
デメテルの法則
- 利用するオブジェクトの内部を知るべきではない
尋ねるな、命じろ
- ほかのオブジェクトの内部状態(つまり変数)を尋ねたり、その状態に応じて呼び出し側が判断したりするのではなく、呼び出し側はただメソッドで命ずるだけで、命令された側で適切な判断や制御するよう設計します。
- getter/setter(ゲッター/セッター)を頻繁に使っている場合は要注意
早期return
- 条件分岐のネストを解消する
- 条件を反転させて、returnを返してしまう
- 条件分岐が追加になっても、容易に追加が可能
def not_early_return(a, b, c): if a >= 0: if b >= 100: if c >= 300: pass return a + b + c def early_return(a, b, c): if a < 0: return if b < 100: return if c < 300: return return a + b + c
インターフェース
- クラス判定用の分岐を書かずに、
show_area()
で面積を計算して表示できる
class Circle(Shape): def __init__(self, radius): if radius < 0: raise ValueError self.radius = radius def area(self): return self.radius * self.radius * 3.14 class Rectangle(Shape): def __init__(self, width, height): if width < 0: raise ValueError if height < 0: raise ValueError self.width = width self.height = height def area(self): return self.width * self.height def show_area(shape: Shape): print(shape.area()) show_area(Circle(2)) show_area(Rectangle(2,3))
継承より委譲
- 継承はかなり注意して扱わないと、すぐに密結合に陥ります。
- まずお伝えしたいのは、継承はよっぽど注意して扱わないと危険、継承は推奨しませんというのが本書のスタンスです。
- 継承は、オブジェクト指向言語の入門書の多くで紹介されるしくみです。
- 入門書に記載があるとカジュアルに使ってしまいがちです。
- しかし、熟練エンジニアのコミュニティなどでは、継承に対して疑問視や危険視する見方があります。
- 継承関係にあるクラスどうしでは、サブクラスはスーパークラスの構造にひどく依存します(スーパークラス依存)。
- サブクラスは、スーパークラスの構造をいちいち気にしなければなりません。
- スーパークラスの動向によっぽど注意していないと、スーパークラスの変更によりバグ化してしまうのです。
- 委譲とはインスタンス変数に他のクラスのインスタンスを代入し、メソッドで代入したインスタンスのメソッドを呼び出すこと。
- コードは↓の「2. インスタンス(委譲)によるAdapter」を参照すること
デッドコード、到達不能コード
- 条件分岐で、どんな条件であっても実行されないコード
- 可読性を低下させる。将来バグになる可能性がある
- IDEの静的解析機能でデッドコードを検出する機能があるので、それを使うと良い
マジックナンバー
- 説明のない数値
- 修正漏れがあるとバグになる
- 定数として定義するようにする
nullを返さない、渡さない
- nullを変数に代入しない
- nullをメソッドの戻り値にしない
例外を握り潰さない
- try-catchで例外をキャッチして、何も処理をしないと、エラーを検知するすべがなくなってしまう
- どこに問題があるのかわからなくなる
関心事にふさわしい命名
- 「商品」ではなく、注文品、予約品、発送品といったように関心に合わせた命名を行う
- 「商品」だと色んなところで使われるクラスになってしまい、巨大なクラスになってしまう
- DataやInfoといったクラス名はつけない
- Manager、Processor、Controllerといったクラス名は巨大化しがちなのっで要注意
コマンドとクエリの分離
- 状態の変更(コマンド)とクエリ(状態を返す)は分離する方が使いやすい
メソッドは可能な限り動詞 1語にする
- メソッドは可能な限り動詞 1語にする。例:add
本書で登場する設計パターン
以下の設計パターンがどういったものなのか、概要を記憶しておくようにする。
完全コンストラクタ
値オブジェクト
- 値をクラスとして表現する設計パターン
- 金額、日付、電話番号など様々な値を扱う
- さらに、値を扱うためのロジックをメソッドとして備える
- 値オブジェクトと完全コンストラクタはほぼセットで使う。オブジェクト指向設計の基本形
ファクトリメソッド
- インスタンス初期化(生成)のロジックが複数ある場合は、ファクトリメソッドで初期化処理の分散を防ぐ
- 以下のコードだと、initでインスタンスを生成できてしまうので、もう少し工夫が必要
- ↓を参考にしてみたらできそう。ただ、もっとうまいやり方を見つけたい
- https://qiita.com/17ec084/items/1bad1d79428c6008e66b
class PointClass(object): ''' お客様にpointを付与するクラス ''' __instance = None __MIN = 0 __STANDART_JOINING_CAMPAIGN_POINT = 0 __PREMIUM_JOINING_CAMPAIGN_POINT = 5000 def __init__(self, point): if point < self.__MIN: raise ValueError self.point = point # 標準会員向け入会ギフトポイント @classmethod def standard_joining_point(cls): initial_point = cls.__STANDART_JOINING_CAMPAIGN_POINT return cls(initial_point) # プレミアム会員向け入会ギフトポイント) @classmethod def premium_joining_point(cls): initial_point = cls.__PREMIUM_JOINING_CAMPAIGN_POINT return cls(initial_point) def add_point(self, additional_point): addition = PointClass(additional_point) point = self.point + addition.point return PointClass(point) def main(): point_standard = PointClass.standard_joining_point() print(point_standard.point) point_premium = PointClass.premium_joining_point() print(point_premium.point) added_point_class = point_premium.add_point(3000) print(added_point_class.point)
ポリシーパターン
こちらがわかりやすい。
https://zenn.dev/ryutaro_h/articles/ed58ee31dcd2b4
ファーストクラスコレクション
スキップする。
コードの良し悪しを評価する指標
クラス、メソッドの行数
- 単一責任の原則を遵守するよう設計されたクラスの行数は大体100行程度、どんなに多くても200行程度
- Rubyのコード解析ライブラリRuboCopだと、クラスは100行が上限、メソッドは10行が上限になっている
循環的複雑度
- 循環的複雑度(サイクロマティック複雑度)は、コードの構造的な複雑さを示す指標です。
- 条件分岐やループ処理が増える、ネストすると複雑さは増大していきます。
- 循環的複雑度が10以下だと非常に良い構造
- 分析ツールにより計測可能
凝集度
- モジュール内におけるデータとロジックの関係性の強さを表す指標
- メトリクスとしてはLCOM(Lack of Cohesion in Methods)がある
- 計測ツールで計測可能
結合度
- モジュール間の依存度合を表す指標
- 分析ツールにより計測可能
コード評価に利用できるツール
Code Climate Quality
- コード行数、複雑度などのメトリクスに問題のある個所を可視化
Understand
- コード行数、複雑度、凝集度、結合度などのメトリクスを計測可能
Visual Studio
- コードメトリクス計測機能が利用可能