『達人プログラマー 熟達に向けたあなたの旅(The Pragmatic Programmer)』は、1999年にAndy HuntとDave Thomasによって著されたソフトウェアエンジニアリングの名著です。
ネットでも数多くのプログラマーに愛読されており、その日から使える実践的なテクニックが数多く記されています。
良い設計は悪い設計より変更しやすい
「Tips 14 良い設計は悪い設計より変更しやすい」
— P.35『達人プログラマー 第2版』より(Andy Hunt, Dave Thomas/オーム社)
本書の中で、私にとって最も学びが大きかったのは、上述の「変更しやすさ」に関する概念です。
これは一見すると自明なようでいて、深く掘り下げるとソフトウェア設計の本質を突いていると感じました。
そもそもシステムが存在するということは、その背景にビジネス上の目的や制約があるということを意味します。そして、その目的は決して静的なものではなく、時代や環境、顧客のニーズによって常に変化していくものですよね。
本書は、そうした変化を前提とし、プログラマーは目の前の仕様だけでなく、その背後にある目的や文脈を考慮した上でコードを書くべきだ、という思想を主張しています。特にその姿勢が「良い設計は変更に強い」という一文に凝縮されているように感じました。
実際、現場において「変更」が発生する理由は多岐にわたります。たとえば、顧客からの機能追加要望、要件定義の不備、スケジュールの都合による仕様変更、といった現象は、誰もが経験することだと思います。
本書全体を俯瞰すると、この設計哲学を中心に据えて、後半はその思想をいかに実践に落とし込むかを具体的に解説する構成になっている印象を受けます。
「直交性」という考え方
「2つ以上の物事で、片方を変更しても他方に影響を与えない場合、それらを直行していると呼ぶわけです。」
— P.50『達人プログラマー 第2版』より(Andy Hunt, Dave Thomas/オーム社)
本書には重要な概念や実践的な知見が数多く盛り込まれていますが、その中でも特に印象に残ったのがこの「直交性」という考え方でした。
プログラムを書いていると、どんなに小さな機能であっても、何かしらの「結合」を引き起こしますよね。たとえば、特定のフレームワークを使っている場合、そのフレームワークの思想に沿ったアーキテクチャ設計や、制御の流れに自然と制約が生まれてきます。
多くのフレームワークでは、独自のライフサイクル管理やイベント駆動型の構造、あるいは依存性注入DIコンテナなどの機構を採用していて、開発者はその設計原則に沿って動く必要がありますよね。
結果として、ある処理のタイミングやスコープを変更したいだけでも、他のコンポーネントとの結合が避けられず、直交性が崩れていくことがあります。
MVCやMVVMなどの基本的な構造においては、「どこで何を処理すべきか」がある程度決まっているため、一部を独立して変更することが難しくなる場面にもよく出会います。
また、サードパーティ製のライブラリに限らず、自分で書いた機能であっても、「別の機能の中で再利用しよう」とした途端に、思わぬ依存関係が生まれることがあります。
その瞬間、「直交性」は失われ、ある変更が他の部分へ影響を及ぼすリスクが生まれてしまいます。
私自身も開発の中で、「ここを直すと、こっちが壊れる」「このクラスを変更したら、別のサービスの挙動まで変わってしまった」といった経験は何度もあります。
直交性のサンプルコード
直交性が失われているコードをサンプルとしてまずは書いてみました。
class Logger:
def log(self, message: str):
print(f"[LOG]: {message}")
class User:
def __init__(self, username: str, created_by: str = "admin"):
self.username = username
self.created_by = created_by
def to_record(self) -> str:
return f"{self.username}:{self.created_by}"
class Database:
def __init__(self):
self.logger = Logger()
def save(self, data: str):
self.logger.log(f"Saving data: {data}")
print(f"Data '{data}' saved to database.")
class UserService:
def __init__(self):
self.db = Database()
def create_user(self, username: str):
user = User(username)
record = user.to_record()
self.db.save(record)
print(f"User '{username}' created.")
UserService
は Database
に強く依存しており、かつ Database
は Logger
に依存しています。この構造では、Logger
を変更したくても Database
の実装を一緒に書き換える必要があり、影響が連鎖します。
一番厄介なのはUser
クラスがデータベースへの保存方法をなぜか知っていることです。(この時点でUser
クラスを見たとき、それがデータクラスとしての文脈なのか、それともデータの保存形式を担う文脈なのかわからなくなっています。)
しかも、UserService
はUser
クラスの “あいまいさ” を知っていて、User
がデータベース保存形式を知っているという事実に依存しています。
これにより、各コンポーネントが独立して変更または再利用できなくなっています。このような状態のことを直交性が失われていると本書では表現されています。
次にこのコードをリファクタリングしてみたいと思います。
リファクタリングしてみたコード
from abc import ABC, abstractmethod
class AbstractLogger(ABC):
@abstractmethod
def log(self, message: str): ...
class SimpleLogger(AbstractLogger):
def log(self, message: str):
print(f"[LOG]: {message}")
class User:
def __init__(self, username: str, created_by: str = "admin"):
self.username = username
self.created_by = created_by
class UserRecordMapper:
@staticmethod
def to_record(user: User) -> str:
return f"{user.username}:{user.created_by}"
class AbstractDatabase(ABC):
@abstractmethod
def save(self, data: str): ...
class InMemoryDatabase(AbstractDatabase):
def __init__(self, logger: LoggerInterface):
self.logger = logger
def save(self, data: str):
self.logger.log(f"Saving data: {data}")
print(f"Data '{data}' saved to database.")
class UserService:
def __init__(self, db: AbstractDatabase):
self.db = db
def create_user(self, username: str):
user = User(username)
record = UserRecordMapper.to_record(user)
self.db.save(record)
print(f"User '{username}' created.")
このように直交性を意識してリファクタリングすると、コードの読みやすさや変更のしやすさが格段に上がります。
上記のコードでは、UserService
は DatabaseInterface
にのみ依存しており、具体的なデータベースの実装(InMemoryDatabase)や Logger の存在すら知りません。
この設計にすることで、たとえばログの出力方法を変えたいときや、データベースの保存先をファイルやクラウドに切り替えたいときでも、UserService
側の変更は不要になります。
User
クラスの責任が明確になっている点も注目です。以前の例では User
が自分自身を「どう保存するか」まで知っていましたが、ここでは ユーザー情報の構造を定義することだけ に限定しています。保存形式の詳細は UserRecordMapper
に切り分けられていて、関心が分離されているのがわかります。
保存形式に関する責任を User
クラスから外に出したことで、UserService
側に処理の主導権(コントロールフローの支配権)が戻ってきたように感じました。
リファクタリング前のコードは、User
が to_record()
を持っていたため、UserService
はそのメソッドに従うほかなく、データの扱い方を User
側に委ねてしまっている状態でした。一見すると便利な構造ですが、これは裏を返せば、サービス層が本来持つべき判断の自由度を失っていたとも言えます。
一方、リファクタ後は保存形式への変換処理を UserRecordMapper
に委譲することで、「どのタイミングで、どのように変換するか」を UserService
側で明示的に制御できる構造に変わりました。
保存形式とユーザー情報を直交させたことで、設計全体の見通しがよくなり、拡張や保守もしやすくなるとわかります。
直交性を保つための工夫がなされていると、たとえば将来的に「User に登録日を追加したい」「保存形式を JSON に変えたい」となった場合でも、影響範囲を局所化して安全に変更できるようになります。
まとめ
本書は「初学者にもおすすめ」と紹介されていることが多いですが、実際に読んでみると、そう簡単な本ではないと感じました。確かに文章は非常に平易で読みやすいのですが、その中に込められている内容はどれも実践的で本質的。だからこそ、表面的に読むだけではつかみきれない深さがあると思います。
読みながら何度も「これ、自分のコードにもあったな」と振り返りたくなる場面がありました。
日々コードを書いていると、どうしても自分の得意なパターンや、慣れた書き方に偏ってしまいがちです。本書は、そうした思考の癖や惰性に気づかせてくれる一冊でもありました。時間を開けつつ何度も読み返したい本です。
ここまでお付き合いいただきありがとうございます。
以上、お疲れ様でした。