Navigate back to the homepage

依存性逆転物語 / Dependency Inversion Story

Kenya Hondoh
August 31st, 2020 · 1 min read

概要

  • CLEAN コードという良いコードを書くためのプラクティスがあります。
  • この中に, 「疎結合なコードはいいぞ」 というのがあり,あんまり意味がわかってませんでした。
  • 色々調べた結果,以前勉強した「依存性逆転の原則」が,この「疎結合なコード」に一役買うことがわかってきた。

この投稿では,開発者お客さん のやりとりを通じて,疎結合なコードによる恩恵と,それを実現するための設計指針となる「依存性逆転の原則」のお気持ちを感じることを試みます。

PDF を印刷したい A出版社

あるところに,「PDF を印刷するソフトが欲しい」という A出版社 のお客さんがいました。

そこで A出版社 のために,開発者は以下のようなコードを書きました。

1class Printer:
2 def __init__(self):
3 pass
4
5 def print_PDF(self, pdf_file: PDF):
6 return pdf_file.print_to_paper()
7
8class PDF:
9 def __init__(self, file_name: str):
10 self.file_name = file_name
11
12 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 pass
4
5 def print_PDF(self, pdf_file: PDF):
6 return pdf_file.print_to_paper()
7
8 def print_JPG(self, jpg_file: JPG):
9 return jpg_file.print_to_paper()
10
11class PDF:
12 def __init__(self, file_name: str):
13 self.file_name = file_name
14
15 def print_to_paper(self):
16 # PDF 特有の処理
17 return print(f"This is a PDF file! (file_name: {pdf_file.file_name})")
18
19class JPG:
20 def __init__(self, file_name: str):
21 self.file_name = file_name
22
23 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, abstractmethod
2
3# Printer と PDF, JPG の架け橋となる抽象クラス
4class Printable(ABC):
5 def __init__(self):
6 pass
7
8 @abstractmethod
9 def print_to_paper(self):
10 pass
11
12# 上位クラス,クライアント
13class Printer:
14 def __init__(self, printable_obj: Printable):
15 self.printable_obj = printable_obj
16 pass
17
18 def print_to_paper(self):
19 return self.printable_obj.print_to_paper()
20
21# 下位クラス
22class PDF(Printable):
23 def __init__(self, file_name: str):
24 self.file_name = file_name
25 pass
26
27 def print_to_paper(self):
28 # PDF 特有の処理
29 return print(f"This is a beautiful PDF output! (file_name = {self.file_name})")
30
31# 下位クラス
32class JPG(Printable):
33 def __init__(self, file_name: str):
34 self.file_name = file_name
35 pass
36
37 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 pass
4
5 def print_PDF(self, pdf_file: PDF):
6 return pdf_file.print_to_paper()
7
8 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_obj
4 pass
5
6 def print_to_paper(self):
7 return self.printable_obj.print_to_paper()

1つの Printable 型と,1つの print_to_paper() に依存していて,外部モジュールへの結合は 2箇所 だけです。

このように「結合度」が下がってるのがわかります。これが多分「疎結合なコード」ということなのです。

まとめ

  • 「依存性逆転の原則」は変更しやすいコードを書くのに役立つ。
  • 「依存性逆転の原則」を実現するために,例として,インタフェース(Printable)を作って,上位クラスが下位クラスのそれぞれに依存しないような設計にした。この結果,クラス間の結合度が下がった。

More articles from Kenya Hondoh

CLEAN Code is What You Need

『レガシーコードからの脱却』〜第9章より〜

August 27th, 2020 · 2 min read

JavaScript のお勉強 with MDN web docs

俺たちは雰囲気で JavaScript を書いている。

August 16th, 2020 · 1 min read
© 2020–2023 Kenya Hondoh
Link to $https://twitter.com/EarllibraryLink to $https://github.com/kenchonLink to $https://www.linkedin.com/in/kenya-hondoh-2a7067123/