これはなに
こんにちは。もんたです。
趣味でエンジニアをしています。
最近、「改訂新版 良いコード/悪いコードで学ぶ設計入門」という本を読みました。
わたくし、本を読む時はNotionにメモするようにしておりまして、結構しっかり書くようにしているので「ついでにQiitaの記事にしたらみんなのためになるし、イノベーションじゃね?」って思ったので、投稿しようと思った次第です。
ちなみに、抜粋しているのはメモの一部になります。
この読書メモが皆様の学びのきっかけになれば幸いです。
🐶「この記事は個人の読書メモですので、間違った解釈、記述をしている可能性があることご理解いただけますと幸いですだワン 」
↓↓↓↓↓↓↓↓↓↓ここから読書メモ↓↓↓↓↓↓↓↓↓↓
もんた的目的
良いコード、悪いコードを学び、実践に活かしたい。
保守性を意識し、誰でも理解できるコードを書けるようになり、成長できるエンジニアになりたい。
特にリファクタリング周り、読みやすいコード、アンチパターンについて学びたい。
その他にも設計で意識すること、良いコードを書くために意識するべきことは何か、ソフトウェアエンジニアとして成長するために意識するべきことは何かを本書から学びたい。
エンジニアとして成長するためのきっかけとしたい。
本書で学びたいこと
- リファクタリングの時に意識するべきこと
- ソフトウェアエンジニアリングにおけるアンチパターン
- 読みやすいコードをとは何か
- 悪いコードとは何か
- エンジニアとして成長するために自分が意識すべきことは何か
- 設計の際に気をつけるべきことは何か?
- カプセル化とはなにか
- 関心の分離で意識することは?
- 多能性による機能の取り替えとは?
- 変更容易性を高めるには?
第1章 悪しき構造の弊害を知覚する
データクラス
データクラスとはロジックを何も持たない、変数だけを扱うクラスのことを指す。
データクラスは悪魔を招きやすいクラスとのこと。
Pythonで言うと以下のようなクラスのことをデータクラスと呼ぶ。
@dataclass
class Person:
name: str
age: int
email: str = None
🐶「データクラスとは?(PythonとGolangで比較)」
1️⃣データクラスの解説 - Python と Golang の比較
Pythonとgolangのデータクラスについて解説します。
2️⃣Python のデータクラス
Pythonでは、Python 3.7からdataclasses
モジュールが導入され、データを格納するためのクラスを簡単に作成できるようになりました。
from dataclasses import dataclass
@dataclass
class Person:
name: str
age: int
email: str = None # デフォルト値の設定も可能
# 使用例
person = Person("田中太郎", 30)
print(person) # Person(name='田中太郎', age=30, email=None)
print(person.name) # 田中太郎
# 同値比較が自動的に実装される
person1 = Person("田中太郎", 30)
person2 = Person("田中太郎", 30)
print(person1 == person2) # True
データクラスの主な特徴:
- コンストラクタ(
__init__
)の自動生成 - 文字列表現(
__repr__
)の自動生成 - 比較メソッド(
__eq__
)の自動生成 - イミュータブル(変更不可)にすることも可能(
frozen=True
)
3️⃣Golang のデータクラス
Golangには正式な「データクラス」という概念はありませんが、構造体(struct)がその役割を果たします。
package main
import "fmt"
// Person 構造体の定義
type Person struct {
Name string
Age int
Email string
}
func main() {
// 構造体の初期化
person := Person{
Name: "田中太郎",
Age: 30,
Email: "",
}
// フィールドへのアクセス
fmt.Println(person.Name) // 田中太郎
// Goには自動的な等価比較はないため、独自に実装する必要がある
person1 := Person{Name: "田中太郎", Age: 30}
person2 := Person{Name: "田中太郎", Age: 30}
// 構造体の比較(同じフィールド値を持つ構造体は等しいと判断される)
fmt.Println(person1 == person2) // true
}
Golangの構造体の特徴:
- 値型として扱われる
- コンストラクタは手動で実装する必要がある
- メソッドを追加することができる(レシーバーを使用)
- タグを使用してJSONなどへの変換方法を指定できる
4️⃣両言語の比較
機能 | Python (dataclass) | Golang (struct) |
---|---|---|
自動コンストラクタ | ✅ | ❌(手動実装が必要) |
文字列表現 | ✅ 自動生成 | ❌(String()メソッドを実装する必要あり) |
比較機能 | ✅ 自動生成 | ✅ フィールド単位の比較(複雑な型は手動実装が必要) |
イミュータブル | ✅(frozen=True) | ❌(const指定なし) |
型ヒント/型チェック | ✅ | ✅ |
JSON変換 | モジュール必要 | タグ機能で簡単に設定可能 |
Pythonのデータクラスは、データ中心のクラスを簡潔に記述するのに適しています。一方、Golangの構造体はより低レベルな制御が可能ですが、Pythonのデータクラスが自動的に提供する機能は手動で実装する必要があります。
仮に、このクラスが契約金額みたいに複雑な計算が必要なクラスなのに対して、データクラスとして定義されていた場合、実際の計算ロジックを持つクラスはこのデータクラスとは別のクラスで実装されることになる。
データクラスは小規模な開発だとあまり問題にはならないが、大規模な開発になると問題が起きてくるので気をつけておこう。
例えば、データクラスと実装クラスが別ロジックで実装されている場合を考えてみる。
そうなると、税率を新しいものに変更してほしいという依頼が来た時に、ある実装クラスを変更したとする。
しかし、よくコードを調べてみると実装クラスが複数にわたって定義されており、そのロジックごとに変更を修正する必要があったみたいなことが往々にしてある。
こういったことを引き起こす原因、つまり悪魔となりやすいのだ。設計する側は利用者がこのデータクラスの与える影響を考えて設計を行う必要があることを覚えておこう。
🐶「フィードバック」
はい、この理解は適切です。データクラスと実装の分離に関するリスクと設計上の注意点を的確に指摘しています。
-
データクラスと実装の分離によるリスク:
データを保持するクラスと振る舞い(計算ロジック)を持つクラスを分離すると、関連するロジックが複数の場所に分散しやすくなります。 -
大規模開発での問題:
小規模な開発では管理しやすくても、大規模になると関連ロジックの把握が難しくなり、修正漏れや一貫性の欠如につながります。 -
具体例の適切さ:
税率変更の例は、ビジネスロジックが散在した場合の問題を明確に示しています。同じような計算ロジックが複数のクラスに実装されていると、変更時にすべての箇所を特定して修正する必要があります。 -
設計者の責任:
「設計する側は利用者がこのデータクラスの与える影響を考えて設計を行う必要がある」という指摘は重要です。データの表現と操作の一貫性を保つためには、意図的な設計が必要です。
この問題に対処するためには、以下のような設計アプローチが考えられます:
- ドメインモデルの適切な設計:データと振る舞いを適切に配置し、生焼けオブジェクトを避ける
- 共通サービスの導入:重複ロジックを集約する
- ファサードパターンの活用:複雑な操作を単一のインターフェースの背後に隠蔽する
- 変更の局所化:ビジネスルールの変更が一箇所で完結するよう設計する
データクラスそのものが問題ではなく、それを使った設計の仕方と責務の配置が重要なポイントです。
生焼けオブジェクト
生焼けオブジェクトとは、初期化しないと使い物にならないクラスのことを指す。
例えば、Pythonで言うと以下のような例を指す。
class User:
def __init__(self, name):
self.name = name
self.email = None
self.age = None
# emailが未定義の時、このメソッドはエラーになる
def get_email(self):
return self.email
このように定義が未完全なクラスのことを生焼けオブジェクトと呼ぶ。
🐶「生焼けオブジェクトってなんや?」
生焼けオブジェクト(Half-baked Object)とは、オブジェクト指向プログラミングにおける設計上の問題を指す概念です。具体的には、以下のような特徴を持つオブジェクトを指します:
- 責務が不明確:クラスの目的や役割が曖昧で、単一責任の原則に違反している
- 不完全な実装:必要な機能の一部だけが実装されており、残りは他のクラスに依存している
- 状態と振る舞いの不一致:データ(状態)を持っているが、それに関連する振る舞い(メソッド)が別の場所に実装されている
- 過度な外部依存:自身のデータを処理するために、他のクラスやサービスに過度に依存している
生焼けオブジェクトは、前回の質問で議論したデータクラスと実装クラスの分離がうまくいかない場合によく見られます。例えば、単なるデータホルダーとして始まったクラスに、徐々にロジックが追加されていったり、逆に機能的なクラスからデータだけが分離されたりした結果として発生します。
このような設計は、コードの保守性や拡張性を損なう原因となります。オブジェクト指向設計の原則に従えば、クラスは関連するデータとそのデータを操作する振る舞いを適切にカプセル化すべきです。生焼けオブジェクトはこの原則に反しており、前回の例のように「税率変更」のような修正が複数箇所に影響する状況を引き起こしやすくなります。
改善策としては、クラスの責務を明確にし、関連するデータと振る舞いを適切に配置することが重要です。場合によっては、Value ObjectとServiceの適切な分離、あるいはドメイン駆動設計などの手法を取り入れることで、より堅牢な設計に改善できます。
未初期化状態、または生焼けオブジェクトとは、コンストラクタが完全にオブジェクトを初期化せず、未完全な状態でオブジェクトが存在することを指します。これはしばしばバグや不明な挙動を引き起こす原因となります。
この対策としては初期化の時点でオブジェクトを生成するのに必要な値を与えるようにすること。
以下のように、オブジェクトの初期化の段階で必要な値を渡すようにすることをマストにすることで生焼けオブジェクトは対策することができる。
class User:
def __init__(self, name: str, email: str, age: int):
self.name = name
self.email = email
self.age = age
def get_name(self):
return self.name
def get_email(self):
return self.email
def get_age(self):
return self.age
🐶「フィードバック」
はい、その理解は正しいです。
オブジェクト生成時に必要な値をすべて渡すことで、コンストラクタ内で完全な初期化が保証され、生焼け(部分的に初期化された)オブジェクトを防止できます。たとえば、上記のUserクラスでは、name、email、ageの各パラメータを必須として受け取り、インスタンス変数に設定しています。これにより、Userオブジェクトは常に必要な情報を持った状態で生成されるため、後から不足分を補う必要がなくなります。
まとめ
- データクラスは設計に気を使わないとさまざまなバグの原因となる
- データクラスを用いることで起きうるバグは、「重複コード・修正漏れ・生焼けオブジェクト」などである
- 生焼けオブジェクトとはオブジェクトが未完全な状態で定義をしていること
- 対策のためにオブジェクトの初期化の時点で必要なデータを全て渡すようにすることが重要
第3章 カプセル化の基礎―ひとつにまとめる―
クラス単体で正常に動作するように設計する
データクラスはそれ単体だと、正常に動作するかどうかを判断することはできない。
データクラスによる弊害をざっくりまとめると「重複コード・修正漏れ・生焼けオブジェクト」だった。
あまり考えずにデータクラスを設計すると、あるクラスをオブジェクト化した時、初期化をしないとバグが発生したり、ある操作を行うために別のクラスと紐づける必要があったみたいなことが起きる。
つまり、データクラスとは他のクラスがサポートしないと正常に動作することもできない未熟なクラスということになる。
これはカプセル化の概念からほど多いことがわかる。
カプセル化を意識するには、「このクラスはこれ単体で正常に動くかどうか」ということを考えることが重要。
🐶「フィードバック」
基本的な理解としては合っています。データクラス(たとえば Python の @dataclass で定義されるもの)は、主にデータを保持するための便利な仕組みであり、以下の点が問題となりやすいです。
-
重複コード・修正漏れ:
データクラスに対して追加のバリデーションや振る舞いを別途実装する場合、コードが散在しやすく、更新時に修正が漏れるリスクがあります。
-
生焼けオブジェクト:
オブジェクトが完全に初期化されることを前提としないと、内部状態が不完全なまま使用される可能性があり、これがバグの原因となります。
-
カプセル化の欠如:
データクラスはそのままではオブジェクト単体で自己完結的に正しい動作(例えば不変条件の維持など)を保証しないため、他のクラスや補完的な仕組みに依存しがちです。
つまり、データクラスはあくまでデータ保持のためのツールであり、単体で完全な振る舞いを保証するわけではありません。オブジェクトが単体で正常に動作するようにするためには、必要な初期化処理やバリデーションをクラス内部でしっかり実装するか、もしくは外部のサポートを適切に設計する必要があります。
以上の点から、あなたの指摘は本質を捉えており、厳密にチェックした場合も問題の本質に合致していると言えます。
このような未熟なクラスを使いたいと思う人はいない。使うとしたら、そのクラスが単体で必要なデータを保持しており、必要なメソッドを保持しているものが良い。
このように、必要なデータとメソッドを保持しているクラスのことをドメインモデルの完全性と呼ぶ。
まぁ、要するにカプセル化を意識するのであれば、クラスにはプロパティとメソッドを持たせましょうねってこと。
🐶「フィードバック」
はい、その理解は基本的に適切です。ドメインモデルとして完全性を持たせるためには、クラスは単体で必要なデータと、そのデータに対する振る舞い(メソッド)を保持していなければなりません。これにより、クラス自体が自身の状態の整合性やビジネスルールを担保でき、いわゆる「生焼けオブジェクト」を防ぐことができます。ただし、用途によってはデータの転送や単なる情報保持を目的とする場合もあり、その場合は必ずしも振る舞いを含む必要がないという点は留意すべきです。しかし、カプセル化の観点からは、オブジェクト自体が必要な検証や操作を内包する設計が理想とされます。
総じて、カプセル化を意識し、必要なプロパティとメソッドをクラスに持たせるという考え方は、ドメインモデルの完全性を実現する上で重要な原則となります。
不変で思わぬ変更を防ぐ
不変で思わぬ動作を防ぐとは、クラスのプロパティをfinal
やconst
といった修飾子で表現してしまうことで、直接値の変更をすることができないようにするということ。
こうすることで、不正な変更を防ぐことができる。そして、値を変更したい場合は改めてインスタンスを作成するようにする。
Goでいうと以下のようにAgeを変更したかったらWithAgeメソッドを呼び出すようにし、新しいAgeにしたかったらインスタンスを作り直すようにする必要があるみたいな感じ。
type User struct {
name string
age int
}
func NewUser(name string, age int) User {
return User{name: name, age: age}
}
func (u User) Name() string {
return u.name
}
func (u User) Age() int {
return u.age
}
func (u User) WithAge(newAge int) User {
return User{name: u.name, age: newAge}
}
ちなみにPythonだと以下のように書ける。
@dataclass(frozen=True)
class User:
name: str
age: int
def with_age(self, new_age: int) -> "User":
return replace(self, age=new_age)
frozen
やFinal
を使ってイミュータブルなオブジェクトを作成することができる。
個人的にめっちゃ重要やなと思ったのが『 値オブジェクト+バリデーション+完全コンストラクタ 』である。
値オブジェクトを使うことで同じint型だけど、意味が違う値同士の足し算を防ぐ。
バリデーションを使うことでメソッドやプロパティの定義の際に、不正な値が代入されることを防ぐ。
最後に完全コンストラクタを意識することで、クラスをすぐに使える状態であると保証して公開することができる。
それぞれカプセル化にとってめちゃくちゃ重要な概念だなという印象を持った。
まとめ
- 重要なのはクラス単体で正常に動作するかどうかを保証すること
- そのために、値オブジェクトやバリデーション、イミュータブル設定などを意識してクラスを作成することが大事
- 値オブジェクト、バリデーション、完全コンストラクタを意識することがカプセル化において重要
第4章 不変の活用―安定動作を構築する―
不正な再代入を防ぐために、変数・引数を不変にする
これはシンプルなTipsになるが、変数や引数に対して不正な再代入を機械的に防ぐために、finalやconstを使って再代入を禁止するようにしよう。
pythonだとこんな感じでFinalアノテーションを使うことで、「この変数は不変だよ」ってことを伝えることができる。
from typing import Final
foo: Final[int] = 100
foo = 111
参考:https://note.com/tlo_oly/n/ne823b1e9f317
ちなみに全てを不変にするのではなく、一部のケースでは可変にしても良い。
- スコープが局所的なケース(for文の中など)
- 高速な画像処理や大量のデータを処理するケース
- リソースの制約が厳しいケース
しかし、基本的には不変であることが望ましい。近年のプログラミング言語では不変を基本とするものが多い。
まとめ
- 不正な変更を防ぐために、
final
やconst
といった修飾子を使い、明示的に「この値は不変だよ」ってことを伝えるようにしよう - セッター関数とゲッター関数を使いこなそう。基本的にプライベートな値にアクセスする時はゲッターを、プライベートな値を変更する時はセッターを使おう
第5章 バラバラなデータとロジックをカプセル化する実践技法
プリミティブ型への執着をやめよう
プリミティブ型執着とは、int
, string
, boolean
などのプリミティブ型をあちこちで使い倒すスタイルの開発手法のことを指します。
プリミティブ型でも動くコードは書ける。書けるのだが、それだけでは重複コードがあちこちに存在してしまったり、バリデーションをその都度書く必要があったりとかなり無駄が多くなる。
また、重複コードが多いとその文実装漏れの可能性や修正の範囲が広くなるので、重複コードはマジで撲滅した方がいい。
バグを減らすためには面倒だけど、一つ一つ丁寧にカプセル化することが重要。
カプセル化するとは、以下のように実装することを指す。
class RegularPrice:
def __init__(self, price: int):
self.price = price # setter経由でバリデーションが実行される
@property
def price(self) -> int:
return self._price
@price.setter
def price(self, new_price: int):
if new_price < 0:
raise ValueError("Price cannot be negative")
self._price = new_price
プライベートコンストラクタとファクトリメソッド
コンストラクタ(Pythonでいう__init__()
)を公開してしまうと、さまざまな用途で使われてしまう。
🐶「プライベートコンストラクタってなんや?」
プライベートコンストラクタとは、クラス外部から直接インスタンス化できないように設計されたコンストラクタのことです。これにより、クラスのインスタンス生成を制限し、たとえば以下のような用途に利用されます。
-
シングルトンパターン:
クラスのインスタンスを1つだけ生成し、以降はその同じインスタンスを返す実装に利用されます。
-
ファクトリメソッドとの併用:
外部から直接インスタンス化させず、内部で定義されたファクトリメソッドを通じてインスタンス生成を行い、初期化の制御や検証を一元化するために使われます。
他の言語との違い
-
JavaやC++の場合:
これらの言語では、コンストラクタに
private
アクセス修飾子を付けることで、クラス外からの直接呼び出しを防ぐことができます。 -
Pythonの場合:
Pythonでは、厳密なアクセス制御機構はなく、名前の先頭にアンダースコア(例:
_
)を付けるなどの慣習で非公開を示すのみです。したがって、完全なプライベートコンストラクタは実現できませんが、設計上「外部から直接インスタンス化しないでほしい」という意図を示すことは可能です。 -
Goの場合:
Goではクラスの概念はありませんが、構造体のフィールド名の先頭を小文字にすることでパッケージ外からのアクセスを制限し、コンストラクタ的な関数(例:
NewType
)を用いてインスタンス生成の制御を行います。
このように、プライベートコンストラクタは言語や設計方針に応じて利用される概念であり、インスタンス生成の制御や設計パターンの実現に役立ちます。
例えば、会員に関するクラスがあったとする。
このクラスのコンストラクタを公開してしまうと、プレミアム会員や通常会員の生成を外部で行われてしまい、メンテナンスが大変になってしまう。
会員ポイントを変更したいときに、調べるコードが多くなってしまうのだ。
それを防ぐために、プライベートコンストラクタを使うことをお勧めする。
プライベートコンストラクタを使うと、会員クラスのロジックの中にプレミアム会員の生成や通常会員の生成ロジックを持たせるように実装することになる。
そうなると、会員ポイントの変更をしたい場合でも、会員クラスのロジックを変更すればいいので、変更が容易になる。(なんならプレイベートプロパティの変更だけで済む)
ちなみに、ファクトリメソッドとはオブジェクトの生成方法をカプセル化するデザインパターンのこと。
上記の会員クラスの例で言うと、プレミアム会員や通常会員の生成を会員クラスの責任にしてしまうことをファクトリメソッドと呼ぶ。
🐶「ファクトリメソッドってなや?」
ファクトリメソッドは、オブジェクトの生成方法をカプセル化するための手法(またはデザインパターン)です。具体的には、クライアントコードが直接コンストラクタを呼ぶのではなく、ファクトリメソッドを通じてオブジェクトを生成することで、生成ロジックを隠蔽し、柔軟性や保守性を向上させます。
1️⃣主な特徴
-
生成ロジックのカプセル化:
ファクトリメソッド内でオブジェクトの初期化やバリデーション、前処理・後処理をまとめることで、インスタンス生成の詳細を隠すことができます。
-
柔軟なインスタンス生成:
条件に応じて異なるサブクラスのオブジェクトを生成したり、特定の設定に基づいて異なる生成方法を選択できるため、クライアント側での分岐処理が不要になります。
-
統一的なインターフェース:
複数の生成パターンがあっても、同じファクトリメソッドを呼ぶだけで適切なオブジェクトを得られるため、コードの可読性が向上します。
2️⃣Pythonでの例
Pythonでは、クラスメソッドを利用してファクトリメソッドを実装するのが一般的です。
class MyClass:
def __init__(self, value):
self.value = value
@classmethod
def create_from_string(cls, value_str):
# 文字列から値を生成する処理をカプセル化
value = int(value_str)
return cls(value)
# 利用例
instance = MyClass.create_from_string("123")
print(instance.value) # 123
この例では、create_from_string
メソッドがファクトリメソッドとして働き、文字列から適切な値を抽出してインスタンスを生成しています。
3️⃣Goでの考え方
Goではクラスやメソッドの概念はありませんが、同様の目的でコンストラクタ関数(例: NewType
)を用います。例えば、
package mypackage
type MyStruct struct {
Value int
}
// コンストラクタ関数としてファクトリ的な役割を果たす
func NewMyStruct(value int) (*MyStruct, error) {
if value < 0 {
return nil, fmt.Errorf("invalid value: %d", value)
}
return &MyStruct{Value: value}, nil
}
このように、Goでも関数を通じてインスタンス生成のロジックをまとめることで、生成時のバリデーションや初期化処理を統一的に行うことができます。
まとめると、ファクトリメソッドはオブジェクト生成に関する柔軟性とカプセル化を提供する重要な概念であり、PythonやGoといった言語でもそれぞれの方法で活用されています。
ちなみに、ファクトリクラスが増えてくるようであれば、ファクトリメソッドを実装する専用のクラスであるファクトリクラスの実装を検討すると良い。
🐶「おう、ファクトリクラスなんてもんもあんのかい。」
ファクトリクラスは、オブジェクト生成のロジックを1か所に集約し、クライアントコードから直接コンストラクタを呼び出さずにインスタンスを作成する手法です。これにより、生成時の初期化処理や検証、場合によってはサブクラスの選択などを内部に隠蔽できます。
以下は、Pythonでのシンプルなファクトリクラスの例です。
class Product:
def __init__(self, name: str, price: float):
self.name = name
self.price = price
def display(self):
print(f"Product: {self.name}, Price: {self.price}")
class ProductFactory:
@staticmethod
def create_product(name: str, price: float) -> Product:
# ここで生成前の検証や前処理を実施できる
if price < 0:
raise ValueError("Price cannot be negative")
return Product(name, price)
# 利用例
if __name__ == "__main__":
product = ProductFactory.create_product("Gadget", 99.99)
product.display()
この例のポイント
-
ファクトリクラス(
ProductFactory
):create_product
という静的メソッドを提供し、これを通じてProduct
のインスタンスを生成します。これにより、たとえば価格のバリデーションなどの前処理を一元管理できます。 -
静的メソッドの利用:
ファクトリクラスはインスタンス化せずに利用できるように、
@staticmethod
や@classmethod
を使うことが一般的です。ここではシンプルに静的メソッドを用いています。 -
生成ロジックの隠蔽:
クライアントコードは
ProductFactory.create_product
を呼ぶだけで良く、内部でどのようにProduct
が生成されるかは隠蔽されています。これにより、将来的に生成方法が変更になってもクライアントコードへの影響が少なくなります。
このように、Pythonではファクトリクラスを利用することで、オブジェクト生成に関する複雑なロジックを分離・統一的に管理できるメリットがあります。
結果を返すために引数を使わない
🐶「結果を返すために引数を使わない?どゆこと」
「結果を返すために引数を使わない」というのは、関数の処理結果をその関数の戻り値(return
文)として返すべきであり、入力として渡した引数自体を変更して結果を伝える(たとえば、引数のリストやオブジェクトを直接書き換える)という方法を避けるという考え方です。
📚詳細な説明
-
戻り値を使うメリット:
Pythonでは、関数が計算結果や処理結果を直接戻り値として返すのが一般的で、これにより関数の動作が明確になり、コードの可読性やテストのしやすさが向上します。
def calculate_sum(a, b): return a + b result = calculate_sum(3, 4)
-
引数を書き換えて結果を伝える方法の問題点:
一部の言語(CやC++など)では、出力用の「アウトパラメータ」として、ポインタや参照を用いる方法があります。しかし、Pythonの場合、引数で渡されたオブジェクトを変更することは可能でも、関数の意図が不明瞭になりやすく、副作用を生みやすいです。例えば、
def calculate_sum(a, b, result): result[0] = a + b res = [None] calculate_sum(3, 4, res) print(res[0])
このようにすると、どこで値が更新されるかが明示的でなくなり、デバッグや保守が難しくなる可能性があります。
-
Pythonicな設計:
Pythonでは、副作用をできるだけ避け、入力に対する処理結果を関数の戻り値として明示する設計が推奨されています。これにより、関数が純粋な処理を行い、再利用性も高まります。
まとめると、「結果を返すために引数を使わない」とは、関数の出力を引数の変更に依存させず、戻り値として返すことで、コードの意図を明確にし、保守性やテストの容易さを高めるという設計方針を指します。
以下のコードが悪い例
def calculate_sum(a, b, result):
result[0] = a + b
res = [None]
calculate_sum(3, 4, res)
print(res[0])
以下のコードがいい例
def calculate_sum(a, b):
return a + b
result = calculate_sum(3, 4)
悪い例で言うと、resultという値を新しい結果を得るために引数として渡しているのが悪いと言うこと。
このようにしてしまうと、どう言う経緯でresultの値が変更されたかがわからないからである。
この実装をしてしまうと、予期せぬタイミングで値が変更されることになり、バグの原因となる。
それを防ぐために、いい例のように引数を渡すのが推奨されている。
ほしい結果が引数によって得られていることがわかる。
まぁ、要するにresultの値を変更したいからといってそのための関数の引数にresult自体を渡すのをやめろって言うこと。これが結果を得るために引数を使わないである。
尋ねるな、命じろ
尋ねるな、命じろとは、例えば以下のような実装を悪とするデザインパターンのこと。
def delete_user_is_zero_amout(users List[User], user_id int):
if users.get_user(user_id).is_zero():
users.get_user(user_id).is_zero().delete
適当に書いたコードなので分かりづらいかもだが、要するに数珠繋ぎのアクセスはやめなさいってこと。
「尋ねるな、命じよ」とは、getter/ setterのように具体的な実装を外部に丸投げしてしまう傾向がある。
public class Employee {
private String name; // 外部から参照できないのでOK
private LocalDate contractDate;
public int salary; // publicなインスタンス変数は外部から参照できるので禁止
...
// getterで外部からインスタンス変数を参照できるので禁止
public LocalDate getContractDate() {
return this.contractDate;
}
}
getter/ setter系の関数はビジネスロジックを持たない。「取得してなんなの?」ってなってしまう。
getter/ setterを使うと、getterで取得した値を使って条件分岐したり、そのデータを何かに使ったりとブラックボックスが増えてしまう。
なので、getter/ setterを使うのではなく、呼び出し側は以下のように関数を命じるだけでOKなように設計しようねというのが尋ねるな、命じよの原則。
public class Employee {
private LocalDate contractDate;
private int salary;
...
public int calculateSalary() {
if (this.contractDate.isBefore(LocalDate.now())) {
return this.salary;
}
return 0; // この返し方はイマイチですが、例ということで...
}
}
int salary = employee.calculateSalary();
参考:https://qiita.com/TakeshiFukushima/items/0b16aef3d320a2ccb7c3
🐶「フィードバック」
あなたの理解は基本的に正しいです。1️⃣詳細な解説
-
Getter/Setter の問題点
Getter や Setter を使うと、オブジェクト内部の状態が外部に露出してしまい、外部側でその状態に基づいた処理(たとえば条件分岐など)を行う結果、オブジェクトが自らの状態管理やビジネスロジックを担当しなくなります。これにより、オブジェクトの責任範囲が曖昧になり、システム全体の整合性が損なわれる可能性があります。
-
「尋ねるな、命じよ」(Don't Ask, Tell) の原則
この原則は、オブジェクトに対して「何かを計算して返してくれ」というように内部状態を尋ねるのではなく、必要な動作をオブジェクトに「命じる」ことで、オブジェクト自体がその状態を基に適切な処理を実行するように設計することを推奨します。
あなたの例で言えば、
getContractDate()
で単に値を取得させるのではなく、calculateSalary()
のようにオブジェクト内部で必要な判断や処理を行うようにして、外部には結果だけを返すようにする設計が、責任の所在を明確にし、数珠繋ぎのアクセス(「train wreck」)を防ぐという考え方です。 -
実際のメリット
- カプセル化の向上: オブジェクト内部の状態やロジックが隠蔽されるため、変更の影響範囲が限定され、保守性が高まります。
- 責任の明確化: 各オブジェクトが自らのビジネスロジックを持つことで、どのエンティティがどの処理を担っているのかが明確になります。
- コードの直感性: 呼び出し側は「命令」するだけで良いため、コードの意図が明確になり、ブラックボックスが減少します。
2️⃣まとめ
あなたが示した例と説明は、「尋ねるな、命じよ」の原則に基づいた設計の正しい理解を示しています。Getter/Setter を多用して内部状態を露出させる設計は、結果としてオブジェクトの本来の責務を曖昧にし、大規模化した際の保守性に悪影響を与えがちです。
そのため、必要な処理はオブジェクト自身に命じる形で実装することが推奨されます。
参考:https://qiita.com/TakeshiFukushima/items/0b16aef3d320a2ccb7c3
まとめ
- プリミティブ型はできるだけ使わないようにする。積極的にvalueObjectを使うようにしよう
- ファクトリメソッドやプライベートコンストラクタを使って、インスタンスの生成ロジックをクラス内の責任にしよう
- 尋ねるな、命じろとは数珠繋ぎのアクセスにならないようにクラスの責任を細かく分けようぜってこと
第6章 関心の分離という考え方―分けて整理する―
関心ごとを分離しよう
関心ごとを分離するとは、オブジェクトの目的・手段ごとにクラスを作成し、明確に分けるようにしようぜってこと。
現実世界で関心ごとの分離ができていない例で言うと、一つの箱の中に洗濯用のはさみがあったり、トイレ掃除用の用具があったり、キッチンペーパーがあるような状態を指す。
このような箱はものが多くなると探し物をする時に、大変になる。
「ある程度関連しているからとりあえずクラスAに実装しよう」みたいなことを繰り返していると、クラスが膨大になり、いろいろな役割をもつカオスなクラスになってしまう。
そうではなく、しっかりとこのクラスAの役割はこう、クラスBはこうと明確に役割や関心ごとを分け、カプセル化することを心がけよう。
まとめ
- 関心ごとの分離をすることで、脳への負荷が減る
- オブジェクトごとの役割が明確になり、変更に強いコードが書ける
- オブジェクトの振る舞いをインターフェースなどを用いて定義しよう
第7章 関心が混ざったコードを分けて整理する実践技法
DRYの原則を勘違いするな
DRY原則とは、**Don't Repeat Yourself(繰り返しを避けよ)**と言う意味。
しかし、たまに重複は悪であるという勘違いをしている人がいる。
例えば、RegularDiscountPrice
クラスとSummerDiscountPrice
クラスがあったとする。
どちらもamountから一定値をdiscountするだけのクラスだ。
一見すると『ロジック同じで、割り引く値違うだけだから重複コードじゃね?』って思うかもしれない。
しかし、SummerDiscountPriceの時は単純な差ではなく、10%の割引を行うみたいにロジックが変更した場合、全く違う実装となる。
このように一見同じに見えるが、その責任は異なる場合があるのだ。
ロジックは同じようでも、目的が違うような場合は共通化すべきではないと言うことを覚えておこう。
スマートUI
利口なUIとは、ビジネスロジックをユーザーインターフェイスに入れてしまうパターンです。
アンチパターンとして紹介されてますが、それがわかっていてもいつの間にか、ユーザーインターフェスにビジネスロジックが入ってしまっていることもあるかも。テンプレートエンジンを使用しているときなど、htmlの中にifやforが入っていたらそこが危険ですね。ifの条件にビジネスロジックが入っていたり、forで繰り返すリストの取得方法が入っていたり・・・
参考:https://qiita.com/YasuhiroKimesawa/items/fc635c50cba78f2d45b5
🐶「スマートUIてなんや?」
「スマートUI」という用語は、文脈によって意味が変わる場合がありますが、ここでは主にアンチパターンとしての「スマートUI」について説明します。
つまり、UI(ユーザーインターフェース)層に過度なビジネスロジックや状態管理、データ処理の責務を押し付けてしまう状態のことを指します。
この状態になると、UIコンポーネントが複雑になり、再利用性や保守性、テストの容易さが損なわれる可能性があります。
1️⃣スマートUIの具体例
アンチパターンの例(Reactの場合)
以下は、データの取得、ロジック、状態管理、そして表示までを1つのコンポーネントに詰め込んでしまっている例です。
// スマートUI(アンチパターン)の例
import React, { useState, useEffect } from 'react';
const SmartComponent = () => {
const [data, setData] = useState(null);
useEffect(() => {
// データの取得とロジックがここに集約されている
fetch('/api/data')
.then(response => response.json())
.then(json => {
// ビジネスロジックもUIコンポーネント内で処理
if (json.status === 'ok') {
setData(json.data);
} else {
setData([]);
}
});
}, []);
return (
<div>
{data ? (
data.map(item => <div key={item.id}>{item.name}</div>)
) : (
<p>Loading...</p>
)}
</div>
);
};
export default SmartComponent;
この例では、コンポーネントが「何を表示するか」だけでなく、「どのようにデータを取得し、状態を管理するか」も自前で行っています。結果として、コンポーネントが肥大化し、以下の問題が生じます。
- 再利用性の低下: 同じロジックが他のUIでも必要になった場合、再利用が難しくなります。
- テストの困難さ: UIとロジックが密結合していると、単体テストやユニットテストの設計が複雑になります。
- 保守性の低下: 変更が必要な際に、UIの変更とロジックの変更が絡み合い、修正箇所が増えてしまいます。
2️⃣対策と改善例
1. 責務の分離
- プレゼンテーショナルコンポーネント(ダンプUI)とコンテナコンポーネント(スマートコンポーネント)に分けるアプローチが有効です。
- プレゼンテーショナルコンポーネント: 単に受け取ったデータを表示するだけの役割。
- コンテナコンポーネント: データの取得、状態管理、ロジックなどを担当し、その結果をプレゼンテーショナルコンポーネントに渡す。
2. カスタムフックの活用
Reactの場合、データ取得やロジックをカスタムフックに切り出すことで、UIコンポーネントをシンプルに保つことができます。
改善例
まず、データの取得とロジックをカスタムフックに切り出します。
// useData.js: データ取得とロジックを担当するカスタムフック
import { useState, useEffect } from 'react';
export const useData = () => {
const [data, setData] = useState(null);
useEffect(() => {
fetch('/api/data')
.then(response => response.json())
.then(json => {
setData(json.status === 'ok' ? json.data : []);
});
}, []);
return data;
};
次に、プレゼンテーショナルコンポーネントを定義します。
// DataList.js: 単にデータを表示するコンポーネント
const DataList = ({ data }) => (
<div>
{data ? (
data.map(item => <div key={item.id}>{item.name}</div>)
) : (
<p>Loading...</p>
)}
</div>
);
export default DataList;
最後に、コンテナコンポーネントでカスタムフックを利用してプレゼンテーショナルコンポーネントにデータを渡します。
// ContainerComponent.js: コンテナコンポーネント
import React from 'react';
import { useData } from './useData';
import DataList from './DataList';
const ContainerComponent = () => {
const data = useData();
return <DataList data={data} />;
};
export default ContainerComponent;
このように、UIの表示部分とロジック部分を明確に分離することで、各コンポーネントの責務がシンプルになり、再利用性、テスト容易性、保守性が向上します。
3️⃣まとめ
-
スマートUI(アンチパターン)の定義:
UI層に過度なロジックや状態管理を組み込み、責務が肥大化している状態。これにより、コードの再利用性や保守性、テストの容易さが損なわれる。
-
具体例:
1つのコンポーネントでデータ取得、ビジネスロジック、状態管理、UI描画すべてを行う例(上記のReactの例)。
-
対策:
- 責務の分離: プレゼンテーショナルコンポーネントとコンテナコンポーネントに分ける。
- カスタムフックの利用: ロジック部分をカスタムフックに切り出して、UIコンポーネントは表示に専念する。
- 設計パターンの採用: MVC、MVVMなどのアーキテクチャパターンを用いることで、役割を明確にする。
これらの対策により、UIの複雑さを抑え、システム全体の品質と保守性を向上させることが可能になります。
つまり、スマートUIとはビューロジックにビジネスロジックが含まれているアンチパターンのこと。
これは感覚的にもわかる。ビューに関するクラスはあまりロジックを持つべきではない。基本的に表示に関することにのみ関心を持つべきである。しかし、スマートUIではビューのロジックに複雑な計算処理が含まれていたりする。
対策としては、このロジックはどこに責任があるのかを考えることである。
明らかに表示するだけ以上の役割をビューが持っている場合は、責任と目的を明確にし、切り分けるようにしよう。
神クラス
🐶「神クラスて?」
「神クラス」(God Class)とは、プログラム内で極端に多くの責務を持ち、さまざまな処理やロジックを一つのクラスに詰め込んでしまったアンチパターンです。
その結果、コードの可読性や保守性、再利用性が大きく低下し、バグが発生しやすくなるだけでなく、テストも困難になります。
1️⃣具体例
以下は Python で書かれた、神クラスの例です。ここでは、ユーザー管理、データベースアクセス、ログ記録、エラーハンドリングなど、多岐にわたる処理を一つのクラスにまとめています。
class ApplicationController:
def __init__(self):
# 初期化処理
self.db_connection = self.connect_to_database()
self.current_user = None
def connect_to_database(self):
# データベース接続の初期化処理
print("データベースに接続中...")
return "DB_CONNECTION_OBJECT"
def authenticate_user(self, username, password):
# ユーザー認証ロジック
print(f"ユーザー {username} を認証中...")
# 仮の認証処理
if username == "admin" and password == "password":
self.current_user = username
return True
else:
return False
def fetch_user_data(self):
# 認証済みユーザーのデータ取得
if not self.current_user:
self.log_error("未認証のユーザーがデータ取得を試みました")
return None
print(f"{self.current_user} のデータを取得中...")
# 仮のデータ
return {"name": self.current_user, "role": "administrator"}
def log_error(self, message):
# エラーログの記録
print(f"エラー: {message}")
def update_user_data(self, new_data):
# ユーザーのデータ更新
if not self.current_user:
self.log_error("未認証のユーザーがデータ更新を試みました")
return False
print(f"{self.current_user} のデータを更新中... 新しいデータ: {new_data}")
# 仮の更新処理
return True
def run(self):
# アプリケーションの主要な処理を統括
print("アプリケーションを起動中...")
if self.authenticate_user("admin", "password"):
user_data = self.fetch_user_data()
print("ユーザーデータ:", user_data)
self.update_user_data({"role": "superadmin"})
else:
self.log_error("認証に失敗しました")
この ApplicationController
は、以下のような問題を抱えています。
- 責務の過多: 認証、データベース接続、データ取得、データ更新、ログ出力など、異なる関心事が一つのクラスに混在。
- 保守性の低下: 機能追加や修正の際、どこに手を入れれば良いかが不明瞭になり、バグが生じやすい。
- テストの難しさ: 多くの機能が絡み合っているため、単体テストやモックの作成が複雑になり、再利用が困難。
2️⃣対策
1. 単一責任の原則 (SRP: Single Responsibility Principle) の遵守
各クラスは一つの責務だけを持つように分割します。たとえば、認証、データアクセス、ログ記録などはそれぞれ独立したクラスに分けることが有効です。
改善例
認証用クラス:
class Authenticator:
def authenticate(self, username, password):
print(f"ユーザー {username} を認証中...")
if username == "admin" and password == "password":
return True
else:
return False
データベースアクセス用クラス:
class DatabaseManager:
def __init__(self):
self.connection = self.connect_to_database()
def connect_to_database(self):
print("データベースに接続中...")
return "DB_CONNECTION_OBJECT"
def fetch_user_data(self, username):
print(f"{username} のデータを取得中...")
return {"name": username, "role": "administrator"}
def update_user_data(self, username, new_data):
print(f"{username} のデータを更新中... 新しいデータ: {new_data}")
return True
ログ管理用クラス:
class Logger:
@staticmethod
def log_error(message):
print(f"エラー: {message}")
アプリケーションコントローラ(統括用):
class ApplicationController:
def __init__(self):
self.db_manager = DatabaseManager()
self.authenticator = Authenticator()
self.logger = Logger()
self.current_user = None
def run(self):
print("アプリケーションを起動中...")
if self.authenticator.authenticate("admin", "password"):
self.current_user = "admin"
user_data = self.db_manager.fetch_user_data(self.current_user)
print("ユーザーデータ:", user_data)
self.db_manager.update_user_data(self.current_user, {"role": "superadmin"})
else:
self.logger.log_error("認証に失敗しました")
このように責務を分割することで、各クラスはシンプルになり、以下のメリットが得られます。
- 可読性と保守性の向上: 変更すべき箇所が明確になり、機能追加や修正が容易に。
- テストのしやすさ: 個別のクラスが単一の機能に集中しているため、単体テストが簡単になる。
- 再利用性の向上: 各クラスは特定の役割に特化しているため、他のプロジェクトや機能でも再利用しやすい。
3️⃣コンポジションの利用
クラス間の依存関係を整理し、各コンポーネントを組み合わせる(コンポジション)ことで、システム全体の柔軟性を高めます。上記の例では、ApplicationController
がそれぞれのコンポーネント(DatabaseManager
、Authenticator
、Logger
)を組み合わせています。
4️⃣デザインパターンの活用
例えば、Facade パターンを利用して、複雑なサブシステムへのアクセスを単一のシンプルなインターフェースで提供することも有効です。
5️⃣まとめ
-
神クラスの定義:
多くの責務を一つのクラスに詰め込み、システム全体の保守性、拡張性、テスト容易性に悪影響を及ぼすクラス。
-
具体例:
ユーザー認証、データベースアクセス、ログ記録、エラーハンドリングなどが一つのクラスに混在している
ApplicationController
の例。 -
対策:
- 単一責任の原則に基づいて、クラスを責務ごとに分割する。
- コンポジションを利用して、各クラスを組み合わせ、役割を明確にする。
- 必要に応じてデザインパターン(Facade など)を活用する。
これにより、クラスごとの役割が明確になり、システム全体が柔軟かつ保守しやすい設計に改善されます。
神クラスとはその名の通り、神のようなクラスのこと。
めちゃくちゃ多様なメソッドやデータを持っているクラスのことで、さまざまな役割を担うクラスになっていることを指す。
このクラスも当たり前だがバグの温床となる。
変更範囲が膨大になり、一つの小さな変更なのに、大量のコードや依存関係を調べないといけなくなり、結果的に保守がクソほど大変になる。
まとめ
- DRYの原則とは「繰り返しコードは悪である」ではなく、「目的別にコードを分けなさい」という意味
- 一見似ているコードでも、目的が異なるのであればそれは分けて実装するべきである
- レイヤーごとに目的を明確にすること。関心ごとが複数存在するクラスを作らないことが保守性の高いコードを書く上で重要である
- 常に「このクラス、オブジェクトは関心ごとが複数存在しないか?」を自問自答することが大切
第8章 条件分岐―迷宮化した分岐処理を解きほぐす技法―
早期returnを使って条件と実装ブロックを分ける
これは結構いいなと思ったのでメモ。
可読性を上げるために早期returnを使うのは理解していたが、以下のように条件ブロックと実装ブロックを分けることでより可読性が上がるなと思った。
if not user.can_action():
return
if not user.has_magic_point():
return
user.use_magic(magic_name)
このような書き方だと、新たに条件を追加するとなっても、条件ブロックにつかするだけでOKだから簡単。
しかも、ロジックを追加するとなっても条件ブロックは気にしなくてもいいので、開発していてどこに集中すればいいかがかなり明確になる。
早期returnのほかにも、条件を満たさない場合はcontinueやbreakを使ってループ処理の先頭に戻らせるっていうのもあるよ。
この考え方も結構重要だなと思ったから使っていこう。
for i := 0; i < 100; i++ {
if user[i] != isCondition { break } # 可読性を上げるために早い段階でreturnしてしまう
user[i].state .changeState(newState)
if 0 < user[i].hitPoint { break }
user[i].state.ChangeState(states.getState(DEAD))
}
これはサンプルコードなので、将来なロジックは無視して欲しいのだが、要するに、breakとcontinueを条件を満たさない状態で含めてしまうことで、早期returnと同じように条件分岐ロジックと処理ロジックを見やすくすることができるのだ。
switch文は気をつけて使おう
switch文を実装する時はできるだけ1つのswitch文で全ての要件を満たすように実装しよう。
例えば、RPGのゲームにおいて、魔法使いが魔法を使う時、「MPの消費」「相手にダメージを与える」「杖の耐久」などいくつもの条件が出てくる。
ファイヤーの時はダメージはXXXで、MP消費はXXXで、耐久はXXXで…それでアイスの時は…とばらばらのswitch文で行うのは非効率である。
仮に、新しく業火滅却という魔法が追加された時にダメージとMP消費のswitchに新しくcaseを追加したが、耐久でcaseを追加するのを忘れていたとなる可能性がある。
それを防ぐために、できるだけ一つのswitch文でダメージ、MP消費、耐久の計算を行うようにしよう。
しかし、魔法が数百もある場合はこのswitch文はゴリくそに長いコードになってしまい、可読性がめちゃくちゃに下がってしまう。
それを防ぐためにはインターフェースを使うことをお勧めする。インターフェースでクラスを抽象化し、必要な振る舞いを定義した上でENUMやらMAPやらを使って漏れのないように実装することをお勧めする。
例えば、インターフェースとしてMagicAtackというクラスを作ったとする。このクラスのメソッドにmagicName()
, consumeMagicPoint()
, atackDamege()
, consumeWeaponDurability()
を実装する。こうすることで、実装の漏れがなくなるのと、ENUMやMAPを使っているのでめちゃくちゃ可読性が上がるようになる。
ENUM, MAPを使った後は武器を使うメソッドとMP消費、武器の耐久計算、ダメージの関数を呼び出せばいいだけだからである。
🐶「意味わかんない?わかりやすい例はこちら」
1️⃣ ✖️ 悪い例:if/elif が散在する実装
以下の例では、魔法ごとに MP コスト・ダメージ・耐久減少をそれぞれ別の if
文で処理しています。
新魔法を追加するたびに3箇所すべてを修正し忘れると、バグの原因に…。
def cast(magic_type: str):
# MP消費
if magic_type == "fire":
mp_cost = 10
elif magic_type == "ice":
mp_cost = 8
elif magic_type == "thunder":
mp_cost = 12
elif magic_type == "hellfire":
mp_cost = 50
else:
raise ValueError("Unknown magic")
# ダメージ
if magic_type == "fire":
damage = 30
elif magic_type == "ice":
damage = 20
elif magic_type == "thunder":
damage = 40
elif magic_type == "hellfire":
damage = 120
else:
raise ValueError("Unknown magic")
# 耐久減少
if magic_type == "fire":
durability_loss = 1
elif magic_type == "ice":
durability_loss = 1
elif magic_type == "thunder":
durability_loss = 2
# hellfire を忘れてしまう…
else:
durability_loss = 0
# 処理
use_mp(mp_cost)
deal_damage(damage)
lose_durability(durability_loss)
-
問題点
- 条件分岐が散らばり、追加・修正漏れが起こりやすい
- コードが冗長で可読性・保守性が低い
2️⃣ ✅ 良い例:Enum+dataclassで一元管理
魔法のパラメータを dataclass
にまとめ、Enum
のメンバとして持たせます。
これなら魔法追加時は MagicType
に1行追加するだけで OK。
from enum import Enum
from dataclasses import dataclass
@dataclass(frozen=True)
class MagicStats:
name: str
mp_cost: int
damage: int
durability_loss: int
class MagicType(Enum):
FIRE = MagicStats("Fire", mp_cost=10, damage=30, durability_loss=1)
ICE = MagicStats("Ice", mp_cost=8, damage=20, durability_loss=1)
THUNDER = MagicStats("Thunder", mp_cost=12, damage=40, durability_loss=2)
HELLFIRE = MagicStats("Hellfire", mp_cost=50, damage=120, durability_loss=3)
def cast(magic: MagicType):
stats = magic.value
# stats.mp_cost, stats.damage, stats.durability_loss を一元的に参照
use_mp(stats.mp_cost)
deal_damage(stats.damage)
lose_durability(stats.durability_loss)
# ユーティリティ関数は省略
def use_mp(mp): print(f"Use {mp} MP")
def deal_damage(d): print(f"Deal {d} damage")
def lose_durability(l): print(f"Lose {l} durability")
# 実行例
if __name__ == "__main__":
cast(MagicType.HELLFIRE)
# Use 50 MP
# Deal 120 damage
# Lose 3 durability
-
メリット
- 条件分岐が一箇所にまとまり、追加・修正がラク
-
MagicStats
で扱う値が明確化され、ドキュメント代わりにもなる - 再利用やテストが容易になり、バグも激減
🎯 まとめ
-
悪い例:複数の
if
/switch
による重複処理はバグのもと -
良い例:
Enum
+dataclass
で魔法ごとの振る舞いを一元管理し、実装漏れを防ごう!
型の判定で分岐しないようにする
せっかくinterface
を使っているのに、instanceof
などを使って条件分岐をしていては、インターフェースの良さを活かせていない。
結局重複コードが増えてしまう。
例えばホテル予約サイトの開発をしているとする。
通常料金の時と繁忙期の時で料金が違うといったことが出てくるだろう。
通常料金はMoneyクラスにし、繁忙期料金をBusyMoneyクラスにしたとする。
そのように実装すると、繁忙期かどうかを確かめる際にinstanceof
を使って条件分岐し、繁忙期なら通常よりも多くの料金を設定するみたいなロジックになる。
busy_season = MoneySeason()
if isinstance(hotel_rate, RegularRates):
busy_season = hotel_rates.fee().add(Money(3000))
else if isinstance(hotel_rate, PremiumRates):
busy_season = hotel_rates.fee().add(Money(5000))
これを防ぐにはそもそもMoneyクラスの中に繁忙期の時の料金を返すロジックを追加するようにしちゃえばいい。そうすれば繁忙期の時の料金を取得する時に、繁忙期かどうかの条件分岐が不要になる。
busy_season = hotel_rates.busy_season_fee()
interfaceの実装で重要なのは、オブジェクトの振る舞いや目的、役割を抽象化すること。
「このオブジェクトの振る舞い、役割はどうだろうか?」「共通化することができる部分はどこだろうか?」「目的は一致しているだろうか?」を考えて実装するようにしよう。
初心者と中級者の違い
初心者 | 中級者 | |
---|---|---|
処理の切り替え | 迷わずにifやswitch文を使う | interface設計を試みる |
条件ごとの処理 | if分やswitch文の中に詳細なロジックをベタ書きする | 条件ごとの処理を別のクラスに分ける |
メソッドの処理の切り替えをbooleanで行うことをフラグ引数という。
例えば通常攻撃と特殊攻撃のロジックを同じDamegeというメソッドに書いていたとする。この時、is_special
みたいなフラグ引数を使って特殊と通常の切り替えを行っていたらまさにフラグ引数となる。
できるだけこの書き方は避けた方が良い。なぜならコードが長くなってしまい、可読性が下がるからだ。初心者のうちは許されるが、中級者になるとこれは許されない。
中級者はこういう時、interfaceを使って設計するようにする。
interface
とMAP
, ENUM
を使ってロジックを設計する。
🐶「初級者から中級者になるには」
💡 初級者から中級者になるための実装メモ
🆚 初心者 vs 中級者 の意識比較
項目 | 初心者 | 中級者 |
---|---|---|
分岐の書き方 |
if /switch でタイプ判定やフラグ引数を多用 |
ポリモorphism(interface/抽象クラス)で振る舞いを切り替え |
条件ごとのロジック |
if 文内に詳細ロジックをベタ書き |
各処理を別クラス(Strategy/Enum+Map)に分離し、共通インターフェースで扱う |
フラグ引数 | メソッドに is_special=True などを渡して切り替え |
フラグではなく「特殊攻撃用クラス」「通常攻撃用クラス」を使い、呼び出しは同一メソッドに統一 |
コード重複 | 同じ条件判定・計算を複数箇所で記述 | 一箇所にまとめて定義し、新規追加時の漏れを防止 |
可読性/保守性 | 分岐が増え可読性ダウン、修正漏れでバグが頻発 | 役割ごとにクラスを分け、責任範囲を明確化。テスト・拡張がラク |
🔑 中級者が意識すべき6つのポイント
-
ポリモorphismを活用する
- 振る舞い(振り分けロジック)をインターフェース/抽象クラスとして定義
- 具体クラスに処理を実装し、呼び出し側は型やフラグを意識せず同じメソッドを呼ぶ
class Rate: def fee(self): ... def busy_season_fee(self): ... class RegularRates(Rate): ... class PremiumRates(Rate): ... # 呼び出しは統一 final_fee = hotel_rate.busy_season_fee()
-
StrategyパターンやEnum+Mapを使う
- 切り替え処理をMapに集約し、キー(Enum)→戦略オブジェクト参照
-
if/elif
やswitch
を完全に排除
from enum import Enum class MagicType(Enum): FIRE = ... ICE = ... strategy_map = { MagicType.FIRE: FireStrategy(), MagicType.ICE: IceStrategy(), } strategy_map[magic_type].execute()
-
フラグ引数を避ける
-
do_thing(is_special=True)
のような設計はリファクタの敵 - 特殊処理は別クラス/別メソッドで切り出す
-
-
単一責任の原則(SRP)を徹底
- 1クラス/1メソッドにつき「1つの振る舞い」だけを持たせる
- 条件分岐やビジネスロジックを分散させず、責任範囲を明確化
-
オープン・クローズド原則(OCP)に従う
- 新しい要件(新魔法、新料金)が増えても既存コードを 変更せずに 拡張できる設計
-
class NewMagic(Strategy)
を追加するだけでOK
-
テスト容易性を高める
- 個々の戦略クラスを単体テストしやすくする
- 条件分岐不要なら分岐バグも発生せず、自動化テストがシンプルに
🎯 まとめ
- 中級者は「どこで何を切り替えるか」を考えるよりも、切り替え自体を設計でなくすことを目指す
- Interface/抽象化+Strategy(Enum+Map) で、追加・修正時の漏れと重複を防止
- SOLID原則 に沿ったクリーンなコード構成が、中級者への第一歩です!
まとめ
- 早期returnを使って条件ブロックと実装ブロックを明確に分ける
- switchを使うのは最後の手段!その前にinterface設計ができるかを考える
- 型判定でロジックを分けるのではなく、interfaceを使ってできるだけ条件分岐をなくすようにしよう
- interface設計ができるのが中級者の第一歩である
第9章 コレクション―ネストを解消する構造化技法―
標準ライブラリのコレクションを使う
これはよくReactの開発をしている時に出くわす問題だ。
for文を使ってコードを書こうとしている時に、実はmap()
やfilter()
を使えば1行でかけましたみたいなやつ。
積極的にコレクション関数を使うようにしよう。
そうすることで複雑なfor文での処理を書く必要がなくなる。
コレクション関数をうまく使うことはコーディングのテクニックの1つです。
コレクション関数をうまく使うことで処理の効率が上がったり、コードの可読性が上がり、いわゆるイケてるコードを書くことができます。
コレクションというのは、データをまとめて格納するデータ構造の総称であり、JavaScriptでは以下のコレクションがあります。
- 配列
- 連想配列
参考:https://zenn.dev/web_tips/articles/503da56f2ef8ab
コレクション処理をカプセル化する
🐶「ファーストクラスコレクションとは?」
1️⃣ 🐣 ナイーブなリスト操作
まずは普通に List[User]
を直接操作する例。
from dataclasses import dataclass
@dataclass
class User:
name: str
hp: int # ヒットポイント
def is_alive(self) -> bool:
return self.hp > 0
# パーティを単なるリストとして扱う
party = [
User("Alice", hp=100),
User("Bob", hp=0),
User("Cecil", hp=50),
]
# メンバー追加
party.append(User("Diana", hp=80))
# 全員生存しているかの判定
all_alive = all(user.is_alive() for user in party)
print(f"All alive? {all_alive}") # False
# 生存者リストの取得
alive_members = [user for user in party if user.is_alive()]
print([u.name for u in alive_members]) # ['Alice', 'Cecil', 'Diana']
-
問題点
- 生存判定、追加、取り出し…同じようなリスト操作が散在
- 使うたびに
all(...)
やリスト内包表現を書く必要がある - ポイントごとにコードが重複し、可読性/保守性が低下
2️⃣ 🦅 ファーストクラスコレクションでラップ
Party
クラスを用意し、パーティ専用の振る舞いをまとめて実装します。
from dataclasses import dataclass
from typing import List
@dataclass
class User:
name: str
hp: int
def is_alive(self) -> bool:
return self.hp > 0
class Party:
def __init__(self, members: List[User] = None):
self._members: List[User] = members[:] if members else []
def add_member(self, user: User) -> None:
self._members.append(user)
def all_alive(self) -> bool:
return all(user.is_alive() for user in self._members)
def alive_members(self) -> List[User]:
return [user for user in self._members if user.is_alive()]
def member_names(self) -> List[str]:
return [user.name for user in self._members]
def __iter__(self):
return iter(self._members)
# 使い方
party = Party([
User("Alice", hp=100),
User("Bob", hp=0),
User("Cecil", hp=50),
])
party.add_member(User("Diana", hp=80))
print("All alive?", party.all_alive()) # All alive? False
print("Alive:", party.alive_members()) # [User(name='Alice', ...), ...]
print("Names:", party.member_names()) # ['Alice', 'Cecil', 'Diana']
-
メリット
- パーティ特有の振る舞い(生存判定・追加・一覧取得など)が
Party
に集約 - 呼び出し側は
party.all_alive()
のように、直感的なメソッド名で利用可能 - 同じロジックの重複を排除し、変更箇所を1カ所に固定
- パーティ特有の振る舞い(生存判定・追加・一覧取得など)が
3️⃣ 🌟 まとめ
- ファーストクラスコレクション は「コレクション操作を専用オブジェクトにラップ」して、ロジックを一元管理する手法
- リスト操作コードの重複や分散を防ぎ、可読性・保守性を大きく向上させる
-
Party
のように「ドメイン概念」に名前を与え、振る舞いを絞り込むことがコツです!
プログラミング言語で提供されているリストやマップなどのコレクションをプリミティブと見なして、それをラップしたクラスをファーストクラスコレクションと呼んでいる
コレクションを持つクラスには、他のメンバ変数を持たせないようにしてください。各コレクションをそれぞれ独自のクラスにラップすることで、コレクションに関する振る舞いをそのクラスに置くことができるようになります。
参考:https://zenn.dev/yuyu_hf/articles/93ba0a734208b3
ファーストクラスコレクションとは、例えばUserというクラスがあった時、このUserクラスをListとして処理したいコードを書きたい時があるだろう。
例えば、RPGのゲームで例えるとUserのListをPartyと見なすことができ、そのPartyに新しいUserを追加したり、User全員が生存状態かを確認したいメソッドを書きたい時があるとする。
そういうUserのListを操作したい時に、ファーストクラスコレクションが使える。
ファーストクラスコレクションは、User
のList
を操作する専用のParty
というクラスを作成し、そのParty
クラスのメソッドにコレクション処理やUser
が生存しているかを判定するロジックを持たせることを指す。
ざっくり解説すると、ArrayやList、Collection(言語ごとにそれ相当のものに読み替えてください)をそのまま扱わずにクラスを定義してラップする、という実装手法です。
参考:https://zenn.dev/tesnshin_o255/articles/a9af0a9a2e5596
ファーストクラスコレクションを使って、リスト系のコードをラップし、保守性の高いコードを書くことを心がけよう!
注意しておいて欲しいのが、ファーストクラスコレクションでの実装はリスト操作に関するメソッド、それも同じようなメソッドがあちこちに乱立しているような時に使うと効果的ということ。
リスト系のメソッドでadd()
とかdelete()
って基本的に同じようなコードになりがち、それを解消するのがファーストクラスコレクションだよってことを覚えておこう。
まとめ
- forを使っているとネスト構造に陥りやすい。標準のコレクション関数について調べて、積極的に使っていこう
- continueとbreakを早期returnのように使い、条件と実装を明確に分けよう
- ファーストクラスコレクションを使って重複しがちなコレクション処理をパッケージ化しよう!
第10章 設計の健全性をそこなうさまざまな悪魔たち
YAGNIの原則
YAGNIとはYou Aren’t Going to Need Itのこと。今は必要ないでしょうって意味。
これは、「将来を見越して実装するコードはほとんどの場合使われない」っていう意味。
多くの人は先を見越してコードを実装しようとする。しかし、要件も固まっていないのに作成したコードは往々にして使われない。
それよりかは必要になった時に実装する方が楽だから、デッドコードを生み出さないためにも必要なやつだけ開発しようねという開発方針のようなもの。
たしかに、要件も固まっていないのに先を見越してコードを書けばデッドコードになる可能性も高いし、なにより使われないコードがなんで残っているんだろうという心理的な負荷にもなりやすい。
今すぐにやめよう。
メタプログラミングには気をつけよ
ところでメタプログラミングとは何か。定義は色々ありそうだが、メタなプログラミング、プログラムをプログラムする(ある言語がある言語を読み書きする)というイメージで良いっぽい。
例えば
eval
に渡す文字列を組み立てるプログラムは、まさにプログラムをプログラムが書いているのでメタプログラミングの1つになると思う。 また、SQL の組み立てもそうだし、TypeScript も JavaScript のメタプログラミングになると思う。先程の自己書き換えコードがメタプログラミングに含まれるかは微妙かもしれない(C言語がアセンブリ言語(機械語)を読み書きしていると言えるのかもしれない)
reflection を実装し、プログラム自身の構造を読み書きしたい背景として、このメタプログラミング(特に、操作する言語と読み書き対象の言語が一致している場合)に役立つからというものがあると思う。 自分は reflection の概念を Go 言語で初めて知ったが、他の多くの言語にも実装されている。例えば Java や JavaScript にもある。
参考:https://pokuwagata.hatenablog.com/entry/2020/07/02/120920
🐶「メタプログラミングってなんや?」
1️⃣ ✨ メタプログラミングとは?
プログラム「自身を操作する」技術のことです。
- コード生成:コードを自動で書き出す
- リフレクション:実行時にオブジェクト定義を調べたり動的に呼び出す
- マクロ/デコレータ:関数やクラスの振る舞いを拡張する
つまり、「プログラムがプログラムを扱う」ことを指します。
2️⃣ 🐹 Go でのリフレクション例
Go の標準ライブラリ reflect
を使うと、型や値を動的に調べられます。
package main
import (
"fmt"
"reflect"
)
func PrintFields(x interface{}) {
v := reflect.ValueOf(x)
t := v.Type()
if t.Kind() != reflect.Struct {
fmt.Println("Struct 型を与えてください")
return
}
for i := 0; i < v.NumField(); i++ {
field := t.Field(i)
value := v.Field(i).Interface()
fmt.Printf("%s = %v\n", field.Name, value)
}
}
type User struct {
Name string
Age int
}
func main() {
u := User{"Alice", 30}
PrintFields(u)
}
-
何が起きている?
-
reflect.TypeOf
で型情報を取得 -
reflect.ValueOf
で値を取得 - 構造体のフィールドを動的に列挙・参照
-
3️⃣ 🐍 Python でのメタクラス例
Python のメタクラスを使うと、クラス定義時の振る舞いをカスタマイズできます。
# メタクラス定義
class AutoRepr(type):
def __new__(cls, name, bases, dct):
# __repr__ を自動追加
def __repr__(self):
attrs = ", ".join(f"{k}={v!r}" for k, v in self.__dict__.items())
return f"{name}({attrs})"
dct.setdefault("__repr__", __repr__)
return super().__new__(cls, name, bases, dct)
# メタクラスを使うクラス
class User(metaclass=AutoRepr):
def __init__(self, name, age):
self.name = name
self.age = age
u = User("Bob", 25)
print(u) # 自動生成された __repr__ によって: User(name='Bob', age=25)
-
何が起きている?
- クラス生成時に
AutoRepr.__new__
が呼ばれ、__repr__
メソッドを注入
- クラス生成時に
4️⃣ 🧩 TypeScript でのデコレータ例
TypeScript のデコレータで、クラスやメソッドに処理を追加できます。
function Log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const original = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`Call ${propertyKey} with`, args);
const result = original.apply(this, args);
console.log(`Result:`, result);
return result;
};
}
class Calculator {
@Log
add(a: number, b: number): number {
return a + b;
}
}
const calc = new Calculator();
calc.add(2, 3);
// 出力:
// Call add with [2, 3]
// Result: 5
-
何が起きている?
-
@Log
デコレータが実行時にメソッドをラップし、ログを出力するよう改変
-
5️⃣ ⚠️ なぜアンチパターンと呼ばれる?
- 可読性の低下:コードのフローが見えづらく、追いにくい
- 静的解析困難:IDE の補完や型チェックが効かない部分が増える
- テストしづらい:隠れた動的処理が多いとユニットテストが複雑に
- 依存性の混乱:どこでどのように拡張・注入されたかが把握しにくい
メタプログラミングは強力ですが、**「裏で何が行われるか不透明になりがち」**なため、使い過ぎると保守コストが急増します。
6️⃣ 🎯 まとめ・使いどころ
- 適切な範囲で活用:ログ挿入やシリアライズなど、繰り返し実装しがちな処理を共通化
- 過度な使用は避ける:可読性・テスト性・保守性が損なわれる
- 代替手法を検討:コード生成ツール、コンパイル時マクロ、明示的なファクトリ/ヘルパー関数など
メタプログラミングは「魔法の杖」ですが、かしこく使わないとコードが呪われてしまいます!
まぁ、要するにメタプログラミングとはメタ的にプログラムのコード、構造、振る舞いを操作したり参照したりすることができるプログラミング手法ってことですな。
そして、メタプログラミングのひとつにリフレクションというものが存在する。
🐶「リフレクションって?」
リフレクションとは、プログラムの実行時にオブジェクトの型情報や状態を動的に取得・操作する仕組みのことです。以下、それぞれの言語におけるリフレクションの特徴と実装例を説明します。
1️⃣Python におけるリフレクション
Python は動的型付けの言語であり、リフレクション(およびインスペクション)が言語仕様に深く組み込まれています。
主な特徴は次の通りです:
-
組み込み関数を利用
getattr
,setattr
,hasattr
,dir
,type
などの組み込み関数を使うことで、オブジェクトの属性や型情報を簡単に取得・変更できます。 -
inspect モジュール
より詳細な情報(関数の引数、ソースコード、クラス階層など)を取得するために、
inspect
モジュールが用意されています。 -
例
class Sample: def __init__(self): self.value = 42 def show(self): print("Value:", self.value) obj = Sample() # 属性の取得と確認 if hasattr(obj, 'value'): val = getattr(obj, 'value') print("Retrieved value:", val) # 属性の設定 setattr(obj, 'value', 100) obj.show() # 出力: Value: 100 # オブジェクトのメソッド一覧を表示 print(dir(obj))
2️⃣Golang におけるリフレクション
Golang は静的型付け言語ですが、標準ライブラリの reflect
パッケージを使うことでリフレクション機能を利用できます。
主な特徴は次の通りです:
-
型と値の動的取得
reflect.TypeOf
やreflect.ValueOf
を用いて、変数の型情報や値を取得できます。 -
構造体の解析
構造体のフィールド、タグ、メソッドを動的に調査・操作することが可能で、シリアライゼーションや ORM で活用されることが多いです。
-
例
package main import ( "fmt" "reflect" ) type Person struct { Name string Age int } func main() { p := Person{Name: "Alice", Age: 30} // 型情報の取得 t := reflect.TypeOf(p) fmt.Println("Type:", t.Name()) // 出力: Person // 値情報の取得 v := reflect.ValueOf(p) fmt.Println("Value:", v) // 構造体フィールドの列挙 for i := 0; i < t.NumField(); i++ { field := t.Field(i) value := v.Field(i) fmt.Printf("Field: %s, Value: %v\n", field.Name, value) } }
-
注意点
リフレクションは柔軟な反面、コンパイル時の型安全性が失われたり、実行速度に影響が出たりするため、必要な場合に限定して使用するのが望ましいです。
3️⃣TypeScript におけるリフレクション
TypeScript は JavaScript のスーパーセットで、コンパイル時に型チェックを行いますが、実行時には型情報は消去されます。そのため、標準では Python や Golang のような豊富なリフレクション機能はありません。しかし、以下の方法でリフレクションに似た機能を実現できます。
-
JavaScript の基本機能
オブジェクトのプロパティ一覧の取得には
Object.keys
やObject.getOwnPropertyNames
を使えます。ただし、これらは「構造の調査」であって、型そのものの情報(例: 変数が数値かオブジェクトかなど)は得られません。
-
デコレータと reflect-metadata
TypeScript の実験的なデコレータ機能と
reflect-metadata
ライブラリを利用することで、クラスのプロパティやメソッドのメタデータ(例えば型情報)を実行時に取得可能にできます。このためには、
tsconfig.json
で"experimentalDecorators": true
および"emitDecoratorMetadata": true
を有効にする必要があります。 -
例
// npm install reflect-metadata import "reflect-metadata"; function LogType(target: any, key: string) { const type = Reflect.getMetadata("design:type", target, key); console.log(`${key} type: ${type.name}`); } class Example { @LogType num: number; constructor(num: number) { this.num = num; } } const ex = new Example(42); // コンストラクタ呼び出し時に、num の型が出力される
-
注意点
TypeScript のリフレクションは、JavaScript のランタイム環境上での工夫に過ぎず、完全な型情報は保持されない点に注意が必要です。また、反射による操作はデバッグやライブラリ実装など特定のケースに限定して使用されることが一般的です。
4️⃣まとめ
-
Python
リフレクションが豊富にサポートされ、組み込み関数や
inspect
モジュールで簡単にオブジェクトの詳細情報を動的に扱えます。 -
Golang
reflect
パッケージを通じて、型と値の情報を動的に取得・操作できるものの、型安全性やパフォーマンスに注意が必要です。 -
TypeScript
実行時には型情報が消えるため、基本的なオブジェクト操作機能はあるものの、より高度なリフレクションを行うためにはデコレータと
reflect-metadata
の組み合わせが必要になります。
これらの特徴を理解することで、各言語のリフレクションを適切に利用し、必要な場面で柔軟なコードを書けるようになります。
リフレクションとは、プログラムの実行時にオブジェクトの型情報や状態を動的に取得・操作する仕組みのことです。以下、それぞれの言語におけるリフレクションの特徴と実装例を説明します。
リフレクションを使うことで、オブジェクトの型情報・状態を取得、操作することができる。
しかし、これには注意が必要。せっかくの設計を台無しにすることができる力をリフレクションは持っているからだ。
仮に、バリデーションもしっかり行い、ValueObjectもしっかりと定義したクラスがあるとする。
しかし、そのクラスに対してリフレクションなどでsetter系のメソッドを使えば、エラーになることなく値を変えてしまうことができるのだ。
例えるなら、家の防犯をしっかり行なった(=設計をしっかり行なった)のに、裏口から勝手に入ってこられる(=リフレクションを使って値を変更する)ようなものである。
データベースのスキーマを読み取り、それを元にオブジェクトを作るみたいなこともメタプログラミングの一つと言えるだろう。
🐶「理解度チェック」
はい、その理解は正しいです。データベースのスキーマを読み取り、その情報を基にオブジェクトを生成するという動的なコード生成は、メタプログラミングの一例と捉えられます。
⭐️詳細な説明
-
動的なコード生成:
プログラムが外部のメタデータ(この場合はデータベースのスキーマ)を読み取り、その情報を元にクラスやオブジェクトを動的に生成する場合、コードが実行時やビルド時に自分自身の構造を変更しています。これはメタプログラミングの典型的な例です。
-
ORM(オブジェクトリレーショナルマッピング)の実装:
多くのORMライブラリは、データベーススキーマからモデルクラスを自動生成する機能を持っています。これにより、開発者は毎回手動でモデルを定義する必要がなくなり、データベースの構造の変更にも柔軟に対応できます。
-
利点と注意点:
- 利点としては、コードの重複を減らし、スキーマの変更に伴う影響を最小限にできる点が挙げられます。
- 一方で、動的に生成されるコードは、静的なコードに比べてデバッグや保守が難しくなる可能性があるため、設計や実装の段階で十分な検証が必要です。
このように、外部メタデータを用いてコードの生成や振る舞いを動的に変更するアプローチは、まさにメタプログラミングの応用例と言えるでしょう。
メタプログラミングは便利な一方で静的なコードと比べて保守性が低くなる傾向がある。そのため、用途にはめちゃくそに気をつける必要があるということを覚えておこう。
技術駆動パッケージング
技術駆動パッケージングとは、技術的な関心にパッケージを区切る設計のことを指す。
以下の記事が非常にわかりやすかったです。ぜひ、参考にしてみてください。
🍽 食卓 💭 抽象的な食卓 🍽 🏠 家の食卓 🍽 🏢 オフィスの食卓 🍽 🚪 部屋 💭 抽象的な部屋 🚪 🏠 家の部屋 🚪 🏢 オフィスの部屋 🚪 🚽 トイレ 💭 抽象的なトイレ 🚽 🏠 家のトイレ 🚽 🏢 オフィスのトイレ 🚽
参考:https://zenn.dev/misuken/articles/bdd33790ed4cd0#%E5%95%8F%E9%A1%8C%E3%81%AE%E6%A7%8B%E6%88%90
技術駆動なパッケージングにすると、どのエンティティがどのエンティティと関連するかがわかりづらくなってしまい、開発しずらいディレクトリとなってしまうのだ。
ちなみに適切な構成とは、ドメインドリブンな設計にすることだと理解した。
💭 抽象的な存在 🍽 食卓 🚪 部屋 🚽 トイレ ⭐ 具体的な存在 🏠 家 🏠 家の食卓 🍽 🏠 家の部屋 🚪 🏠 家のトイレ 🚽 🏢 オフィス 🏢 オフィスの食卓(食堂) 🍽 🏢 オフィスの部屋 🚪 🏢 オフィスのトイレ 🚽
参考:https://zenn.dev/misuken/articles/bdd33790ed4cd0#%E9%81%A9%E5%88%87%E3%81%AA%E6%A7%8B%E6%88%90
この設計だと、それぞれのオブジェクトの関心ごとが近くに存在しているので、みやすいコードになるのだ。
MVCとかはまさに技術駆動なパッケージングになっている。ディレクトリの分け方が技術的な要件によって決まってしまっているのだ。
だから、開発していて「あれ、この値オブジェクトってどのエンティティに関連しているんだ…?」ということが大規模化すると大量発生してくる。
小規模な開発だとそこまで問題にならない技術駆動パッケージングだが、大規模化してくると値オブジェクトやエンティティ、ユースケースが大量に発生するので直感的にわかりづらい構造になってしまうということに注意しよう。
🐶「理解度チェック」
あなたの理解は概ね適切です。
⭐️補足説明
-
技術駆動パッケージングの問題点
技術的な関心(例えば、MVC の各層)に基づいてパッケージを分割すると、エンティティ間の関連性がパッケージ構造からは明確にならず、特に大規模システムでは「この値オブジェクトはどのエンティティに属するのか」という疑問が頻発しやすくなります。
-
ドメインドリブンなパッケージングのメリット
ドメインごとにオブジェクトをまとめることで、同じ関心やビジネスロジックを持つオブジェクトが近接して配置され、コードの意図やエンティティ同士の関係が直感的に把握しやすくなります。
-
実際の例
あなたが示した例のように、「抽象的な存在」と「具体的な存在」でパッケージを分割することで、家やオフィスなど各ドメインの中で関連する食卓、部屋、トイレがまとまり、全体の構造が明確になります。
-
まとめ
技術駆動パッケージングは小規模なプロジェクトでは問題にならない場合も多いですが、システムが大規模化すると保守性や理解の面で不都合が出るため、ドメインドリブンな設計が推奨されるという点は正しい認識です。
参考記事でも、このような観点から技術駆動パッケージングの落とし穴と、ドメインドリブンなアプローチの重要性が指摘されています。
まとめ
-
YAGNIの原則を意識しよう。YAGNIとは、「今は必要ないでしょう」という意味で、基本的に将来を見越して作った機能は使われませんよっていう意味。YAGNI原則に従って、将来を見越してコードを実装するのはやめよう。必要になったらで良い。
-
メタプログラミングはせっかくの設計を破壊する裏口のようなもの。使い方には十分に気をつけよう。リフレクションを使えばメタ的にオブジェクトの情報を取得したり、更新したりすることができる。さらにこの時、型情報は考慮しないので本来あり得ない形にすることだってできる。しっかりと設計をした上でメタプログラミングを使うようにしよう
-
技術駆動パッケージングはオブジェクト間の関連性を不透明にしがち。なので、ドメインドリブンで近い関係のものは近い場所に設置することを心がけよう。
-
例えばトイレとか椅子とかで固めるのではなく、オフィスとか家とかでディレクトリを分けるのを意識すると良い。技術駆動でパッケージングすると、トイレを使うために遠くのビルに移動するみたいなことになる。そんなのは面倒だ。漏れちゃう
第11章 名前設計―あるべき構造を見破る名前―
目的駆動名前設計
目的駆動名前設計とは、そのものの果たしたい目的からブレークダウンして、名前を決めなさいという設計方針のこと。
例えばECサイトの開発をしていて、『商品オブジェクト』を作成するのは非常に危険。商品は複数のオブジェクトと関連性がある。例えば、注文・予約・在庫・発送などだ。
商品だけでこれだけ複数のオブジェクトと関連性がある。
仮に、商品の仕様を変更するとなった時、注文・予約・在庫・発送とありとあらゆるオブジェクトに対する影響範囲を調べなければならなくなる。
そこで商品というデカすぎる言葉(目的不明オブジェクト
)を使うのではなく、もっと分割しましょうというのが本書が推奨するやり方。
商品は前述した通り、注文・予約・在庫・発送と関連があった。
なので商品を4つに分け、それぞれ注文品・予約品・在庫品・発送品と分けることで、一気にどのオブジェクトがどのオブジェクトに関心があるかがわかるようになる。
大事なのはそのオブジェクトの目的と『何に関心があるか?』という点を考えて名前をつけること。
存在駆動から目的駆動へ
存在駆動とは商品、アカウント、金額みたいな存在からブレークダウンして命名されることを指す。
一方で、目的駆動は予約品、在庫品、プロフィール、個人プロフィール、請求金額、消費税額など目的が明確な命名のことを指す。
名前をつける時は「これは存在駆動になっていないか?ちゃんと目的駆動になっているか?」を考えることが重要。
🐶「存在駆動?目的駆動?」
1️⃣ 🚩 存在駆動命名とは?
「何が存在しているか」をそのまま名前にするスタイルです。
-
変数:
userList
、orderService
、configMap
-
関数/メソッド:
getUsers()
、updateRecord()
-
クラス:
CustomerRepository
、FileManager
特徴:コードを読んだとき「何というモノがある」のはわかるが、「どう使うのか/何のためなのか」はわかりにくい。
2️⃣ 🎯 目的駆動命名とは?
「何をしたいのか/何のために使うのか」を名前に込めるスタイルです。
-
変数:
activeUsers
(アクティブなユーザーを保持) -
関数/メソッド:
fetchActiveUsers()
(アクティブなユーザーを取得する) -
クラス:
InvoiceCalculator
(請求書を計算する役割)
特徴:名前を見ただけで「何をするのか」が直感的に伝わる。
3️⃣ 🛠️ 具体例で比較
用途 | 存在駆動命名 | 目的駆動命名 |
---|---|---|
ユーザーをサーバーから取ってくる処理 | getUsers() |
fetchActiveUsers() |
請求書の合計金額を計算する処理 | InvoiceService.calculate() |
calculateInvoiceTotal(invoice) |
ローカル設定を保持するオブジェクト | config |
appSettings /loadAppSettings()
|
// 存在駆動
function getUsers() { … }
// 目的駆動
async function fetchActiveUsers(): Promise<User[]> { … }
4️⃣ ✨ なぜ目的駆動命名が良いのか?
-
意図が明確
名前を見るだけで「何をしてくれるのか」がひと目でわかる -
セルフドキュメント
ドキュメントを書かなくても、コードがそのまま仕様書になる -
保守性向上
仕様変更で振る舞いが増えたとき、名前を見直すだけで影響範囲を把握しやすい -
テストしやすい
テストケース名も「should fetch active users」のように自然言語化しやすい -
不要な抽象化を防ぐ
モノをただ管理するだけの曖昧なManager
/Service
クラスを減らせる
「何というモノがあるか」ではなく「何をしたいか」を表現するから、コードが使い手の脳内モデルとピタリと合うのです。
5️⃣ 🎯 まとめ
- 存在駆動命名は「モノの名前」をそのまま使う。何があるかはわかるが意図は不明瞭
- 目的駆動命名は「役割・目的」を名前に込める。何をするのかが直感的に理解できる
- チーム開発・長期保守 では、目的駆動な命名を徹底することでバグを減らし、コミュニケーションコストを下げられます!
4️⃣ ✨ なぜ目的駆動命名が良いのか?
- 意図が明確
名前を見るだけで「何をしてくれるのか」がひと目でわかる- セルフドキュメント
ドキュメントを書かなくても、コードがそのまま仕様書になる- 保守性向上
仕様変更で振る舞いが増えたとき、名前を見直すだけで影響範囲を把握しやすい- テストしやすい
テストケース名も「should fetch active users」のように自然言語化しやすい- 不要な抽象化を防ぐ
モノをただ管理するだけの曖昧なManager
/Service
クラスを減らせる
目的駆動な命名は意図が明確なので開発者は「これなんのためにあんの?」がなくなる。
名前でやりたいことを伝えるようにすることで、影響範囲が明確になるので保守性が向上する。
ジョシュアツリーの法則
ジョシュアツリーの法則とは、人間は名前の知らないものは知覚すらできないという法則のことを指す。
例えばECサイトだったら安全在庫や適正在庫といったドメイン知識が必要になってくる。しかし、これらの情報を知らなければそもそも実装使用ということにすらならない。
人間は自分の感情に対して命名することで、「あ、これは〇〇ていう感情や。つまり、対処法はこうや!」と対策が打てるようになるという。
つまり、名前を知っておくというのはめちゃくちゃ重要なことなのだ。
要するにドメイン知識をしっかり持っていようねという話なのだが、そのためにはチーム内やチーム外の人とコミュニケーションを取り、ドメインに対する知識を増やしていく姿勢が大事になってくる。
形容詞が出てきたらクラス化のチャンス
本書ではRPGゲームを例に説明がされていた。
HitPointというクラスに対して、アクセサリによるヒットポイント増加を合計するというロジックが元々実装されていたとする。
そこに新入社員がアーマーによるヒットポイント増加のロジックを組んだ。
するとアクセサリーのヒットポイントがなくなるというバグが発生した。
その原因はmenbar.maxHitPoint
はもともとのヒットポイントで、実は別の箇所でmaxHitPoint = member.maxHitPoint + accesary.maxHitPointIncreasments()
という実装がされていた。
新入社員くんは新しくmaxHitPoint = member.maxHitPoint + armor.maxHitPointIncreasments()
としていたため、アクセサリーのヒットポイントが打ち消されていたということ。
正しく実装するにはもともとのヒットポイントにアクセサリ分が加わった補正されたヒットポイントに対して、アーマーの増加分を追加する必要があったのだ。
ここで、『もともと』とか『補正された』みたいな形容詞が出てきていることがわかるだろう。
形容詞が出てきた時はクラス化のチャンスである。
変更後はOriginalMaxHitPoint
とかCorrectMaxHitPoint
などのクラスを実装し、新しくクラスを作成し、クラスを小さくすることを心がけよう。
英語にした時に不自然かどうかを考える
class Common:
# いろいろな処理
def is_member_in_confusion(member: Member):
return member.states.contain(state_type.confusion)
上記の例だと、Commonクラスにメンバーに関するメソッドが追加されている。
これは明らかにおかしいことがわかる。
しかし、たま〜に「これどうなんやろ…」って感じるメソッドもあるだろう。
そういう時は英語を使うとわかりやすいかもしれない。
例えば、「メンバーは空腹状態である」を伝えたい時。
This Member is hungury.
と表現できる。
つまり、以下のように表現できるわけだ。
{CLASS_NAME} is {STATUS}
では先ほどの例を考えてみる。仮に、is_member_in_confusion
がMemberクラスに定義されていた場合…
This member is in confusion.
と表現できる。
では先ほどのCommonクラスに定義されている場合だとどうか?
Common is member in confusion.
となる。明らかにおかしい。
このように「どのクラスが適切か」を判断しかねる場合は、英語で考えるってのをやってみるといいかもしれない。
まとめ
- 命名は目的駆動で考えること。技術駆動だと、目的が不明瞭なため
- ジョシュアツリーの法則とは、「名前の知らないものは対策すらできない」ことを指す。ドメインの知識は技術知識と同じくらい重要
- (もともとの)状態とか(加工された)状態のように形容詞が出てくるようになったら、クラス化のチャンス!!
-
{CLASS_NAME} is {STATUS}
で考えた時に不自然であれば、メソッドを別の場所に移したほうが良い
第14章 モデリング―クラス設計の土台―
モデリング
モデリングはアプリケーションの設計をする上で土台となるもの。
例えばUserや商品みたいにめちゃくちゃ意味の広い名前をベースにモデルを作成するのはバグの温床を生む原因となりやすいので注意しよう。
本書におけるモデルとは「目的を達成するために必要最低限の要素を備えたもの」である。
重要なのはこの必要最低限の要素っていうところ。
ここでモデルに本来の目的以上のものを与えてしまうと、そこから保守するコードが爆増していくので、本来の目的以上の役割を与えないということを覚えておこう。
そのためUserという広すぎる意味を持つ名前はモデリングを行う上で不適切であることがわかる。
GitHubを例にしてみると、ユーザーに関するモデルとしてprofileやaccountなどがあることがわかる。
このように、それぞれのモデルの目的を明確にし、それにあった名前をモデルに与えることがモデリングを行う上で重要になってくるのだ。
モデルはあくまでも目的達成の手段と考えるようにすることが大切。
モデルをモノとして考えるため、ありとあらゆる意味を持つようになってしまう。
そうではなく、目的駆動で命名することで、必要最低限の要素をだけを備えたモデルを作成することができる。
目的駆動で名前を考えよう。
まとめ
- モデリングは設計の土台である
- 広い意味を持つ名前をモデルに使ってはいけない(例:User, Product)
- モデルはモノではなく、目的達成の手段
- モデルには必要最低限の要素のみを備えるようにする
第15章 リファクタリング―既存コードを成長に導く技―
テスト駆動開発でコードをリファクタリングする
本書でもテスト駆動開発を用いてリファクタリングを行うことを推奨していた。
テスト駆動開発とは以下の手順でコードを書いていく開発手法のことを指す。
- まずは雛形となるコードを書く
- テストの失敗を確認する
- テストを通るようにハードコーディングでもいいのでコードを書く
- テストが通ることを確認する
- 徐々にコードを追加していく
- 最後にリファクタリングする
重要なのはとにかくベタ書きでもいいから通るコードを書くって部分。リファクタリングをするのは最後でいい。とにかくコードを通るようにする。この進め方が重要。
本書では初めに「雛形となるクラスを書く」ってところがテスト駆動開発とすこーしだけ違う点だなという印象を持った。
個人的には初めに雛形となるコードを書くっていうのはいいなと思った。雛形コードを書いてから、テストコードを書くことでだいたい「あ〜、こんなふうに書けばいいな。そして、ここを修正すればいけそうだな。」っていうのが感覚的にわかる気がする。
自分はテスト駆動開発をするなら、先に雛形となるコードを作ってからテストコードを書くっていう方法を採用したいなと思った。
🐶「あくまで個人の感想ですだワン」
まとめ
- テスト駆動開発では、先にオブジェクトの雛形を作るというアプローチをとってみるといいかもしれない
第16章 設計の意義と設計への向き合い方
木こりのジレンマ
ある木こりが森で一生懸命斧を使って木を切っていた。
ある旅人が通りがかったので、その様子をずっと眺めていた。
なかなか木は切れない。
木こりの斧をよく見てみると刃が欠けているのがわかった。
旅人は木こりに「刃を研げば木を早く切れますよ」と言った。
すると木こりは「刃を研ぐ時間なんかない!」と言った。
これは馬鹿げた話に思うかもしれないが、実際の開発現場では往々にして起きる現象である。
効率よく木を切る(サービスの機能を実装する)のであれば、刃を研ぎ素早く木を倒す(素早くバグの修正や変更を加えられるコードである)必要がある。
素晴らしい、保守性の高いコードを書くことは最終的なスピードを手に入れるために重要な能力なのである。
理想形を知ることで初めて課題を知覚できる
理想を知ることはめちゃくちゃ重要。理想を知っているとその理想に向かって練習すればいいだけなので、師匠すら不要になる。
しかし、逆に理想を知らない状態で練習をするのは結構危険だよってことも覚えておいた方が良い。なぜならがむしゃらに練習した結果、独自の型が身についてしまいそれを修正するのは難しくなっていくから。
なのでまずは理想形を知るってことを意識しよう。
理想を知ることで、「自分は何ができていなくて、なにができているのか」を明確に理解することができるようになる。
まずは理想形を知る。そうすることで課題を知覚できる。
覚えておこう。
🐶「ベストプラクティスをまず理解する」
リファクタリングを行う対象
本書を読んだ後は全てのコードがクソコードに見えてきてしまい、いっそのこと全てコードを書き直したいと思う人も見るかもしれない。
しかし、ここで注意して欲しいのが会社のリソースは有限ということ。
そのため手入れするコードはしっかりと見極める必要があるよってことを覚えておこう。
パレートの法則を参考にすると、会社の売り上げの8割は2割の成果によって構成されると言われている。
つまり、リファクタリングを行うのであればそういった企業的に価値のある箇所に留めるべきである。他にも、アプリケーションにおいて60%の機能は使われていないというのもよく聞く話。
なので、リファクタリングや詳細な設計を行うのは「サービスとして価値のあるものにのみ留めるようにする」ということを覚えておこう。
まとめ
-
木こりのジレンマのようなことは開発現場でも往往にして発生する。刃を研ぐ時間は待っていてもこない。今すぐに刃を研ごう。
-
理想形を知って初めて課題を知覚できる。理想を知らない練習はむしろ悪影響ということを覚えておこう。
-
リファクタリングの対象はパレートの法則に従って、2割の価値ある部分、重要な部分に留めるようにしよう。アプリケーションの機能のうち、6割は使われない。
第17章 設計を妨げる開発の進め方との戦い
コーディング規約を守る
コーディング規約を守ることで、コードが統一され一気に見やすくなる。
だいたいのプログラミング言語のはコーディング規約が提供されているので、それを積極的に使っていこう。
例えば、以下の記事を参考にするとPythonにはPEP8というコーディング規約があるらしい。
このコーディング規約には、インデントやimportの書き方、命名規則について書かれてある。
命名規則
関数名や変数名
- 小文字のみで、単語間をアンダースコアで区切ります。(スネークケース : sample_func)
クラス名
- 先頭を大文字で、パスカルケースで記載します。(SampleClass)
定数名
- すべて大文字で、単語をアンダースコアで区切ります。(SAMPLE_CONST)
モジュール名
- すべて小文字で表現します。読みやすくなるのであればアンダースコアを使用しても問題ありません。(samplemodule, sample_module)
パッケージ名(フォルダ名)
- すべて小文字で表現します。アンダースコアの使用は推奨されません。(samplepackage)
- ※アンダースコア非推奨というのは結構見落としがちで、仕事だと引き継いだ時点でアンダースコアが使われていたので、合わせる形で普通に使用しています。このあたりは、stackoverflowでも、アンダースコア使っても別にだれも責めないよ、といったことが言われているので、まあいいか・・という印象。
But honestly, I don't think anyone will blame you if you use underscores and your code will run with either decision.
公開されていない変数・関数名
- モジュールやクラスが持つprivate的な要素(厳密にはアクセスできるのでprivateではない)の先頭にアンダースコアを記載します。
- 基本的に、各モジュールで先頭にアンダースコアが付いている要素はアクセスすべきではありません。公開されている要素と異なり、バージョンによっては互換性のない更新がされる可能性があります。
- ※仕事では、自身で書いたモジュールに関してはテスト用のモジュールのみに限ってアクセスしています。
- ※一括のimportを指定した場合にも、import対象にならないなど、若干の挙動も変わります。
予約語などと変数名が被る場合
- 他の変数名などが思いつかない場合(もしくは無理に省略などして読みづらくなる場合など)には、末尾にアンダースコアを付けて被らないようにします。(id_, list_など)
- ※サードパーティのライブラリなどによっては別のルールで使われています。例 : scikit-learnであれば、推定済みの値などに使われます。
trailing underscore (self.gamma_) in class attributes is a scikit-learn convention to denote "estimated" or "fitted" attributes.
Why do you use so many leading and trailing underscores in the code examples?
参考:https://qiita.com/simonritchie/items/bb06a7521ae6560738a7
まとめ
- コーディング規約に則った開発を行うことでコードが統一され、保守性が高まる
- コーディング規約に則ったツールを導入する、CDが通ることをマストにするなどして、保守性の高いコードを書くことを徹底する
- 勉強会などを実施して、チーム全体に保守の重要性、設計の重要性を理解してもらう
第18章 設計技術の理解の深め方
動くコードを書いてから設計し直してコミットする
これは個人的にも納得いく開発手法なのでメモしておく。
いきなり設計してもだいたい見落としがあるもの。
なので開発をする時は、まずは動くコードを書いてから、設計し直す、リファクタリングしてからコミットをするようにしよう。
そうすることで、コミットが綺麗になるし、設計スキルもアップする。
設計スキルが最も上がるのはリファクタリングをしている時。
まずは動くコードを書く。そしてリファクタリングする。
::note
まとめ
- まずは動くコードを書く。その後にリファクタリング、設計のし直しを行う
もんた的感想
大変学びになる本だった。
良いコードとは何か、悪いコードとは何かを改めて整理できる内容だった。
本書のおかげで良いコードを書くことに対する温度感がめっちゃ高まるなと思った。
もっと良いコードを意識して書けるようになりたいなと思う。
本書はリーダブルコードをより詳細に書いた本だなという印象を持った。
リーダブルコードは本当にいいコードって何かを理解するための本っていう感じ何の対して、この本はより踏み込んだん内容、例えば設計だったり、オブジェクトだったり、モデリングだったりとより実務で出てくるワードをベースに書かれてあった。それがよかった。
個人的にいいなと思ったのは、テスト駆動開発を行うってのと、条件とロジックを明確に分けるっていうのと、クラス変数や引数を不変にするっていうのがすぐにできそうでいいなと思った。
学びたかったことの答え合わせ
-
リファクタリングの時に意識するべきこと
- 設計を意識する。この命名は見やすくなっているか?とか、このコードは単一の責任になっているか?とかをしっかりと理解するようにする。
- 保守の観点でやっぱり、見やすい、単一の責任になっているかどうかを意識するのが大切だなと思った。例えば、モデリングした時にUserとかDataとかManagerみたいに、広すぎる言葉を使っていないかを確認するのが大事だなと思った。
- あと、条件分岐が増えた時はクラスを使うようにするってのがいいなと思った。コードも見やすくなるし、ないより漏れがなくなるのでバグが減るってのがいい
-
ソフトウェアエンジニアリングにおけるアンチパターン
- 生焼けオブジェクト、副作用(関数名から予想できない変更がされている)、プリミティブ型への執着などなど
-
読みやすいコードをとは何か
- 単一責任
- 命名が的確
- コードが整理されている
- 早期returnを使っている
- 積極的にクラスを使っている
- クラスの役割が明確
- 目的駆動のディレクトリ構成
-
悪いコードとは何か
- 責任が曖昧
- デカすぎる責任を持つクラス(神クラス)
- ネストが深い
- クラスを使っていない
- 技術駆動のアーキテクチャ
- 名前からは予想できない副作用がある
-
エンジニアとして成長するために自分が意識すべきことは何か
- アーキテクチャの学習をするべき
- 圧倒的に実践が足りていない。もっとコードを書いて、いいコードに慣れる必要がある
- モデリングの訓練が足りていない。オブジェクト指向への理解が足りていない
-
設計の際に気をつけるべきことは何か?
- 単一責任
- 命名が適切か?
- 目的駆動のアーキテクチャになっているか?
- 条件と処理を分けているか?
-
カプセル化とはなにか
- オブジェクト指向てきなこと
- コードを一つのオブジェクトとしてパッケージ化すること。クラス化すること
-
関心の分離で意識することは?
- 目的から考えるってこと。このコードで達成したい目的は〇〇だから、この関数はいらないな、必要だなというふうに目的駆動で考えること
-
多能性による機能の取り替えとは?
- 多能性とは要するにinterfaceのこと。クラスごとに異なる挙動を同じ関数で呼び出せるってこと。
-
変更容易性を高めるには?
- 保守しやすいコードにする
- 単一の責任にする
- 目的駆動のアーキテクチャにする
- 名前を明らかにする
- DRYの原則を守る
- YAGNIの原則を守る
- 引数を不変にする
- 尋ねるな、命じろの原則に従う