概要
- CLEAN コードという良いコードを書くためのプラクティスがあります。
- この中に, 「疎結合なコードはいいぞ」 というのがあり,あんまり意味がわかってませんでした。
- 色々調べた結果,以前勉強した「依存性逆転の原則」が,この「疎結合なコード」に一役買うことがわかってきた。
この投稿では,開発者 と お客さん のやりとりを通じて,疎結合なコードによる恩恵と,それを実現するための設計指針となる「依存性逆転の原則」のお気持ちを感じることを試みます。
PDF を印刷したい A出版社
あるところに,「PDF を印刷するソフトが欲しい」という A出版社 のお客さんがいました。
そこで A出版社 のために,開発者は以下のようなコードを書きました。
1class Printer:2 def __init__(self):3 pass45 def print_PDF(self, pdf_file: PDF):6 return pdf_file.print_to_paper()78class PDF:9 def __init__(self, file_name: str):10 self.file_name = file_name1112 def print_to_paper(self):13 # PDF 特有の処理14 return print(f"This is a PDF file! (file_name: {pdf_file.file_name})")
クラスの関係図は,以下のようになっています。
これで A出版社 は満足しました。
JPG を印刷したい B写真館
B写真館 は「JPG を印刷するソフトが欲しいな〜」と思っていました。
そこで開発者は,B写真館のために,JPG を印刷するソフトを作ろうと思いました。
そこで以前, A出版社 のために作った Printer
クラスが使いまわせそうだったので,そうすることにしました。
1class Printer:2 def __init__(self):3 pass45 def print_PDF(self, pdf_file: PDF):6 return pdf_file.print_to_paper()78 def print_JPG(self, jpg_file: JPG):9 return jpg_file.print_to_paper()1011class PDF:12 def __init__(self, file_name: str):13 self.file_name = file_name1415 def print_to_paper(self):16 # PDF 特有の処理17 return print(f"This is a PDF file! (file_name: {pdf_file.file_name})")1819class JPG:20 def __init__(self, file_name: str):21 self.file_name = file_name2223 def print_to_paper(self):24 # JPG 特有の処理25 return print(f"This is a JPG file! (file_name: {pdf_file.file_name})")
B写真館 は喜びました。
トラブル発生
B写真館が Printer
を使えるようにするために開発者は Printer
クラスに変更を加えました。
よって,A出版社 が使っている Printer
も更新する必要が出てきました。
開発者は A出版社 の方に,「Printer
に JPG を印刷できる機能を追加したから,一度アプリケーションを停止して更新します」と言いました。
すると A出版社 は「なぜ他のお客さんのために加えた変更のために,我々も影響をうけなきゃいけないの?」と不満そうな顔をしています。
開発者も「確かに…」となっています。
どうすればよかったのでしょうか?
依存性逆転の原則「上位クラスよ,お前は具象を知り過ぎている。」
開発者が考えた構成では,Printer
が,印刷する文書 PDF
, JPG
の個別の事情を「知り過ぎた」構成になっています(print_PDF()
, print_JPG()
)。
しかし,Printer
では,印刷する対象の個別の知識は取り扱わない方が良さそうに思えます。
なぜなら,あるお客さんのために加えた Printer
の変更が,他のお客さんにも影響を与えるということが起きるからです。
ここで,このクラス構造をもう一度俯瞰してみてみます。
すると,Printer
という呼び出し元(上位)のクラスが,複数の PDF
, JPG
という(下位)クラスに依存していることがわかります。
この問題に対して,依存性逆転の原則(DIP) は以下のような示唆を与えます。
- 上位クラスは,下位クラスに依存してはならない。
- 上位クラス,下位クラスのどちらも,「抽象」に依存すべきである。
このままではよくわからないと思いますが,これを適用したクラス設計は以下のようになります。
Printable
という抽象クラスを用意し,下位クラス PDF
, JPG
でこれを実装している格好です。
これにより,上位クラスは抽象に依存 し,下位クラスも抽象に依存 するようになりました。
そして,上位クラスは下位クラスに依存しない ようになりました。
ソースコード:
1from abc import ABC, abstractmethod23# Printer と PDF, JPG の架け橋となる抽象クラス4class Printable(ABC):5 def __init__(self):6 pass78 @abstractmethod9 def print_to_paper(self):10 pass1112# 上位クラス,クライアント13class Printer:14 def __init__(self, printable_obj: Printable):15 self.printable_obj = printable_obj16 pass1718 def print_to_paper(self):19 return self.printable_obj.print_to_paper()2021# 下位クラス22class PDF(Printable):23 def __init__(self, file_name: str):24 self.file_name = file_name25 pass2627 def print_to_paper(self):28 # PDF 特有の処理29 return print(f"This is a beautiful PDF output! (file_name = {self.file_name})")3031# 下位クラス32class JPG(Printable):33 def __init__(self, file_name: str):34 self.file_name = file_name35 pass3637 def print_to_paper(self):38 # JPG 特有の処理39 return print(f"What a wonderful JPG output! (file_name = {self.file_name})")
こうすると,もし新たに HTML
形式のファイルを印刷したくなったとしても,変更が加わるのは「新たに追加する HTML
クラス」だけです。
つまり,既存の Printer
クラスには変更を加えることなく,新しい形式のファイルに対応できるようになるということです。
余談:これにより,SOLID 原則の「開放閉鎖の原則」(≒ 新しい機能を追加するために,既存のソースコードをいじるんではなく,新しくコードを追加すれば良いようにしよう)もカバーできています。
DIP を実現することで,ソースコードを疎結合に
「依存性逆転の原則」を実現したことで,Printer
クラスは変更に強くなりました。
このクラスの改変と,冒頭に言った「疎結合なコード」とはどのような関わりがあるでしょうか。
ここで,ソフトウェアの変更しやすさのメトリクスである 「結合度」 に着目してみます。
これは,あるモジュールがどのくらい外部のモジュールに依存しているのか(結合しているのか)を表す指標です。
もとの素朴に作成した Printer
クラスは以下のようになっていました:
1class Printer:2 def __init__(self):3 pass45 def print_PDF(self, pdf_file: PDF):6 return pdf_file.print_to_paper()78 def print_JPG(self, jpg_file: JPG):9 return jpg_file.print_to_paper()
ここでは,2つの型(PDF
, JPG
),そして2つの print_to_paper()
という外部要因に依存していることがわかります。
このクラスの外部モジュールへの結合は 4箇所 あることがわかります。
一方,リファクタリングした Printer
は,
1class Printer:2 def __init__(self, printable_obj: Printable):3 self.printable_obj = printable_obj4 pass56 def print_to_paper(self):7 return self.printable_obj.print_to_paper()
1つの Printable
型と,1つの print_to_paper()
に依存していて,外部モジュールへの結合は 2箇所 だけです。
このように「結合度」が下がってるのがわかります。これが多分「疎結合なコード」ということなのです。
まとめ
- 「依存性逆転の原則」は変更しやすいコードを書くのに役立つ。
- 「依存性逆転の原則」を実現するために,例として,インタフェース(
Printable
)を作って,上位クラスが下位クラスのそれぞれに依存しないような設計にした。この結果,クラス間の結合度が下がった。