ロバストネスとモジュール性
以前の記事 (アーキテクトのしごと) で、アーキテクト(の人格)として気にしたいこととして、 ロバストな設計をする と述べた。
ロバストネス は、ここでは「変更しにくい部分を、なるべく小さくすること」と説明した。
ロバストネスを実現する方法は多種ある。例えば、設計書に必ず Why を書くこともまた、のちの変更しやすさに大きく影響を与えるという意味で、実現する方法の一つである。
モジュール性 もまた、そのロバストネスを実現するためにキーとなる概念である。
この記事では、そんなモジュール性について話をする。
モジュール性
モジュール性とは システムを独立した部分に分ける考え方 のこと。厳密な言い方もあるようだけれども、僕がいまいちピンとこなかったので、このいい加減な言い方に留める。
そもそも、エンジニアならばモジュール性と聞いて、それぞれすぐに連想するものがあるだろう。それ。
ソフトウェア・エンジニアリングの範囲はインフラからアプリケーションからカーネルまで、多様に広くあるけれども、どのレイヤーでも、表現を異にしながらモジュール性の話ばかりしているように思う。
モジュールの単位は、インフラのレイヤーだと Docker Container のようなアプリケーション単位、一方アプリケーションのコードの中だとクラス、あるいは関数になる。
「これは疎結合か」「一つのクラスに役割をもたせすぎじゃないか」「テストできるような内部構造になっているか」「ユーザーの数が増えて一つのアプリでは持ちこたえられなくなったときに、もう一つ増やす、という選択肢が存在するか」「Webサーバーは冗長化できているか」
全部、モジュール性が暗に言及されている。
なので、僕もこれからモジュール性の話ばかりすることになる。
凝集度と結合度
モジュール性には、その度合を測る観点がある。それが 凝集度 と 結合度 である。
凝集度も結合度も、それぞれ細かい定義やらカテゴリがある。 Wikipedia を覗くとそれが垣間見える。
Wikipedia の中にあるような細かいカテゴリ分けは、あんま好きじゃない。僕はあんまり役に立った覚えがない。厳密に当てはめようと、人生で一度はトライしてみるけど、だいたいその後空虚さを感じる。(コナーセンスは別。これは結合度の度合いを示す指標の一つだけれども、リファクタリングの方針など、具体的な提案をしてくれているのでよさげ。)
そう思いつつ、モジュール性を語る 観点 としての有用さは同意する。アーキテクチャを評価するときに、「これは高凝集だね」「こりゃ密結合だ」といった会話がエンジニア間でできると、コミュニケーションはスムーズになるだろう。
また僕らが日頃エンジニアリングで悩んでいるときに、その問題がこの凝集度や結合度で説明できるのなら、 Google で検索してみてもいい。凝集度・結合度ともに一般的な用語なので、だいたい同じ悩みを抱えている人を見つけられる。 ChatGPT に聞くときにも悩みを伝えやすい。
凝集度
凝集度とは以下のようなもの:
モジュール内部の要素がどれだけ機能的に関連しているかの度合いのこと。
凝集度はその度合に応じて 高凝集, 低凝集 と言われる。
高凝集、低凝集の説明をするよりも、例を見てもらったほうが理解がはやいかもしれない。
例えば、以下は高凝集な例。
class Calculator:
def add(self, x, y):
return x + y
def subtract(self, x, y):
return x - y
def multiply(self, x, y):
return x * y
def divide(self, x, y):
if y == 0:
raise ValueError("Cannot divide by zero.")
return x / y
# 使用例
calc = Calculator()
print(calc.add(10, 5)) # 15
print(calc.divide(10, 2)) # 5.0
そして以下は低凝集な例。
class RandomStuff:
def add(self, x, y):
return x + y
def make_greeting(self, name):
return f"Hello, {name}!"
def calculate_age(self, birth_year):
return 2023 - birth_year # 例として2023年を現在の年として使用
def read_file(self, file_path):
with open(file_path, 'r') as file:
return file.read()
# 使用例
stuff = RandomStuff()
print(stuff.add(10, 5)) # 15
print(stuff.make_greeting("Alice")) # Hello, Alice!
ぱっと見、高凝集の例のほうがコードが見やすい。クラスの中のメソッドたちが目的や機能で共通の特性を持って整理されている。
低凝集の方は、一方で、メソッド同士の関連性が意味不明で、見にくい。
この例が示す通り、一般に 高凝集なほど嬉しい。
以下は、一般に語られる高凝集性の利点:
- 可読性の向上: 各コンポーネントの機能が明確になるため、コードの理解が容易になる。
- 保守性の向上: 関連する機能が一箇所にまとまっているので、変更が必要な時に影響範囲が小さくなる。
- 再利用性の向上: 一貫した機能を提供するモジュールは、他のプロジェクトやアプリケーションでも再利用しやすくなる。
- テストの容易さ: 高凝集度のモジュールは独立してテストしやすいので、ユニットテストが効率的に行える。
結合度
結合度とは:
モジュール間の相互依存の度合いのこと。
結合度はその度合いによって 疎結合, 密結合 と言われる。
これも例を見せて、疎結合、密結合の説明をする。
以下は疎結合な例。
# 疎結合の関数例
def add(x, y):
return x + y
# 疎結合のクラス例
class Greeter:
def greet(self, name):
print(f"Hello, {name}!")
def main():
result = add(1, 2)
print(result)
greeter = Greeter()
greeter.greet("Alice")
if __name__ == "__main__":
main()
一方、以下は密結合な例。
# 密結合のクラス例
class User:
def __init__(self, name):
self.name = name
class UserGreeter:
def __init__(self, user):
self.user = user
def greet(self):
# Userクラスの内部実装に依存している(name属性が存在することを前提としている)
print(f"Hello, {self.user.name}!")
# 使用例
user = User("Alice")
greeter = UserGreeter(user)
greeter.greet()
なんとなく例がしょーもない気もするけど、一応説明にはなってるので続ける。
結合度が気になるのは、変更を加えようとするとき。
密結合な例のほうは、 UserGreeter
のあるメソッドで User
クラスの仕様に依存した実装が行われている。これは User
クラスをいじるときに面倒なことを引き起こす。なぜなら、User
クラスをいじりたいだけなのに UserGreeter
クラスが壊れないかを心配しなければならないから。
一方疎結合な例のほうは、 Greeter
クラスと add
関数は互いに全く無関係。 それぞれ好きなだけいじっても、互いに影響を与えることはない。
ここからもわかるように、一般に 疎結合なほど嬉しい。
以下は一般に語れる疎結合性の利点:
- 変更への対応: 一つのモジュールを変更しても他のモジュールに影響を与えにくい。
- 柔軟なシステム構築: 結合度が低いことで、システムの一部を変更したり、新たな機能を追加したりする際の柔軟性が増す。
- 並行作業の容易さ: 複数の開発者が異なるモジュールを独立して作業できるため、開発プロセスがスムーズになる。
- テストとデプロイメントの簡素化: 個々のコンポーネントを独立してテストし、必要に応じてデプロイできるため、継続的インテグレーション(CI)や継続的デリバリー(CD)と相性がいい。
- 障害の局所化: 一つのコンポーネントの障害がシステム全体に広がりにくくなる。
- 技術スタックの柔軟性: 他のモジュールに依存しないため、特定の技術スタックの選択や更新がしやすくなる。
- スケーラビリティの向上: 疎結合なシステムは、拡張やスケールアウトがしやすくなる。
"高凝集かつ疎結合なモジュール性を目指す"こと
アーキテクトの一般的なゴールは、高凝集かつ疎結合なモジュールの設計を行うことにある。
...けれども、いつもいつもうまくいくわけじゃない。
get_customer_orders
と cancel_customer_order
はどこにあるべきか?
class CustomerMaintenance:
def add_customer(self, customer):
# 顧客を追加する
pass
def update_customer(self, customer):
# 顧客を更新する
pass
def get_customer(self, customer_id):
# 顧客を取得する
pass
def notify_customer(self, customer_id):
# 顧客に通知する
pass
def get_customer_orders(self, customer_id): # <- これ
# 顧客の注文を取得する
pass
def cancel_customer_order(self, order_id): # <- これ
# 顧客の注文をキャンセルする
pass
class CustomerMaintenance:
def add_customer(self, customer):
# 顧客を追加する
pass
def update_customer(self, customer):
# 顧客を更新する
pass
def get_customer(self, customer_id):
# 顧客を取得する
pass
def notify_customer(self, customer_id):
# 顧客に通知する
pass
class OrderMaintenance:
def get_customer_orders(self, customer_id): # <- これ
# 顧客の注文を取得する
pass
def cancel_customer_order(self, order_id): # <- これ
# 顧客の注文をキャンセルする
pass
どちらが良い設計かと問われれば「場合による」。
-
OrderMaintenance
の操作が本当にget_customer_orders
とcancel_customer_order
に限るなら、全部まとめてCustomerMaintenance
に入れちゃったほうが高凝集にはなる。 - でも、本当に2つだけだろうか。これから
CustomerMaintenance
の規模が大きくなって、振る舞いを抽出したくならないか? その低凝集になるのが既に予見できるならば、いまクラスを分けておくのも理にかなっている。 -
OrderMaintenance
はCustomerMaintenance
の知識を必要とするか? もしそうならば、下手に分けてしまうと密結合の度合いを高めてしまうことになる。
ここにも トレードオフ の関係を見て取れる。手放しに高凝集・疎結合な場面に出くわすほうが珍しい。
僕らは何かを犠牲することを自覚しながら、どちらかを選択しなければならない。
そしてそれが最善の振る舞いである。
高凝集かつ疎結合なモジュール性を目指す
このようなトレードオフを見出しつつ、この理想的な状態をなんとか作ろうとした先人の知恵も多くある。
このあたりは面白い。中には「お前頭いいな」と感心するものもある。
ここではキーワードを散りばめておく。勉強したいときにこのあたりから入ると面白いかも。僕もこれから、このあたりを中心に具体的な事例を交えながらいくつかを記事にしていこうと思う。
-
テスト駆動開発(TDD)
- TDD をそのままやるのは、まあしんどい。なんだけど、 TDD が言いたいことは理解できる。高凝集かつ疎結合な関数は、ユニットテストが作りやすい。ここからヒントを得て、ユニットテストが作りやすいように関数を設計すれば、おのずと高凝集かつ疎結合なものに近づくんじゃないか?という考え方。たしかに。
-
オブジェクト志向プログラミング(OOP)
- カプセル化なり委譲なり。
-
デザインパターン、例えば GoF(Gang of Four)
- ファクトリーパターン、ストラテジーパターン、プロトタイプパターン
- 古典的な OOP のデザインパターン集。中にはこれいつ使うんだ?と思うものもある。
-
SOLID 原則
- 高凝集・疎結合にしやすくするために考えたい以下の5つの原則のこと。この原則を実践する手法の一つに 依存性の注入(Dependency Injection) などがある。
- S - Single Responsibility Principle (単一責任の原則): 変更するための理由が、一つのクラスに対して一つ以上あってはならない
- O - Open/Closed Principle (開放/閉鎖の原則): ソフトウェアの実体(クラス、モジュール、関数など)は、拡張に対して開かれているべきであり、修正に対して閉じていなければならない
- L - Liskov Substitution Principle (リスコフの置換原則): ある基底クラスへのポインタないし参照を扱っている関数群は、その派生クラスのオブジェクトの詳細を知らなくても扱えるようにしなければならない
- I - Interface Segregation Principle (インターフェース分離の原則): 汎用なインターフェースが一つあるよりも、各クライアントに特化したインターフェースがたくさんあった方がよい
- D - Dependency Inversion Principle (依存性逆転の原則): 上位モジュールはいかなるものも下位モジュールから持ち込んではならない。双方とも具象ではなく、抽象(インターフェースなど)に依存するべきである
- システムアーキテクチャのパターン
- アプリの中というよりもインフラのレイヤーで見たときのアーキテクチャパターン
- モノリシックアーキテクチャ と マイクロサービスアーキテクチャ、イベントドリブンアーキテクチャ など。
- ドメイン駆動設計(DDD)
- 有名な設計技法。DDD が述べていることは巨大で、僕には全容を捉えがたいところがあるけれども、都合よく抜き出して話をするならば、例えばエンティティやアグリゲートなんかは高凝集性を意識しているし、境界づけられたコンテキストというのは疎結合性を志向している。
おわり。