0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Pythonで始めるDI/DIP

Last updated at Posted at 2025-03-01

本記事の目的

ある程度アプリケーション開発を行なっていると目にするようになるDI:Dependency Injection、DIP:Dependency Inversion Principleですが、
記事を見ても専門用語が多くてよくわからなかったり、実装はできたもののそのありがたさまでは実感できなかったりと、
知識止まりになることが多いのがDI/DIPです。

本記事では、DI/DIPが効果的に働くテスト実行時やコード変更時を想定し、DI/DIPのあり/なしでどのような違いが出るのかを見てみます。
また、DI/DIPに付随して、テストに対する考え方も少し記載していますので、テストコード実装時の考え方も整理してみます。

本記事の対象外

上記の通り、本記事はDIの実装および効果を見ることが目的のため、以下のような内容は範囲外としています。

  • クリーンアーキテクチャに関する話
  • SOLID原則に関する話
  • DIフレームワークの説明

まずは 使ってみる→DI/DIPの効果を知る からはじめ、もっと知りたくなった方は上記を調べてみるとより理解が深まると思います。

実行環境

Pythonの実行環境があればお好みの環境で。
今回利用するpythonのモジュールは以下になります。事前にpip installでインストールをお願いします。

pytest
injector

クラス設計

今回は以下のようなクラス設計に従いコードを実装していきます。

  1. Keyboardクラス
    キー押下機能を提供する
    この機能は、押下されたキーを知らせる文字列を返す
  2. Mouseクラス
    右クリック、左クリック機能を提供する
    それぞれの機能は、クリックを検知したことを知らせる文字列を返す
  3. Computerクラス
    電源オン、メッセージ入力、アプリ起動、アプリメニュー起動の4つの機能を提供する
    電源オン、メッセージ入力にはKeyboardクラスが提供するキー押下機能を利用する
    アプリ起動、アプリメニュー起動にはMouseクラスが提供するクリック機能を利用する
    各機能は内部で行なわれた操作を知らせる文字列を返す

DI/DIPを理解するうえでも重要なオブジェクト指向ですが、現実世界に置き換えてもわかりやすいと思い、
キーボードとマウスを使ってコンピュータを操作、その結果をディスプレイに表示させるイメージの設計です。

DI/DIP無し

まずはDIもDIPもない通常の方法で、クラス設計に基づいたコードを実装してみます。

アプリケーション実装

今回はmain.pyに全て詰め込んで実装します。

アプリケーションは以下のようなコードを実装してみます。

main.py
# Keyboardクラス
class Keyboard:
    def press_key(self, key: str) -> str:
        return f'{key} key was pressed.'

# Mouseクラス
class Mouse:
    # 左クリック
    def click_left_button(self) -> str:
        return 'left button was clicked.'
    # 右クリック
    def click_right_button(self) -> str:
        return 'right button was clicked.'

# Computerクラス
class Computer:
    def __init__(self):
        # Keyboardクラスのインスタンス作成
        self.keyboard = Keyboard()
        # Mouseクラスのインスタンス作成
        self.mouse = Mouse()

    # 電源オン
    def power_on(self) -> str:
        return self.keyboard.press_key('POWER')
        
    # メッセージ入力
    def input_message(self, message:str) -> list:
        press_list = list()
        for char in message:
            press_list.append(self.keyboard.press_key(char))
        return press_list
    
    # アプリ起動
    def open_app(self, app: str) -> str:
        return f'Start: {app}, {self.mouse.click_left_button()}'
    
    # アプリメニュー起動
    def open_app_menu(self, app: str) -> str:
        return f'OpenMenu: {app}, {self.mouse.click_right_button()}'

if __name__ == '__main__':
    # Computerクラスのインスタンス作成
    computer = Computer()
    # 電源オン
    print(computer.power_on())
    # メッセージ入力
    print(computer.input_message('hello!'))
    # アプリ起動
    print(computer.open_app('vscode'))
    # アプリメニュー起動
    print(computer.open_app_menu('vscode'))

このアプリケーションを実行すると、以下の出力になると思います。

python main.py

POWER key was pressed.
['h key was pressed.', 'e key was pressed.', 'l key was pressed.', 'l key was pressed.', 'o key was pressed.', '! key was pressed.']
Start: vscode, left button was clicked.
OpenMenu: vscode, right button was clicked.

テストコード実装

実装したアプリケーションのテストを実装しましょう。
テストコードは以下のような実装にしてみました。

test_app.py
import main

# Keyboardクラスのテスト
class TestKeyboard:
    def setup_method(self):
        # Keyboardクラスのインスタンス作成
        self.keyboard = main.Keyboard()

    def test_press_key(self):
        # arrange
        test_input = 'ENTER'
        # act
        result = self.keyboard.press_key(test_input)
        # assert
        expected = 'ENTER key was pressed.'
        assert result == expected

# Mouseクラスのテスト
class TestMouse:
    def setup_method(self):
        # Mouseクラスのインスタンス作成
        self.mouse = main.Mouse()
    
    def test_click_left_utton(self):
        # act
        result = self.mouse.click_left_button()
        # assert
        expected = 'left button was clicked.'
        assert result == expected

    def test_click_right_utton(self):
        # act
        result = self.mouse.click_right_button()
        # assert
        expected = 'right button was clicked.'
        assert result == expected

# Computerクラスのテスト
class TestComputer:
    def setup_method(self):
        # Computerクラスのインスタンス作成
        self.computer = main.Computer()

    def test_power_on(self):
        # act
        result = self.computer.power_on()
        # assert
        expected = 'POWER key was pressed.'
        assert result == expected
    
    def test_input_message(self):
        # arrange
        test_message = 'test'
        # act
        result = self.computer.input_message(test_message)
        # assert
        expected = ['t key was pressed.','e key was pressed.','s key was pressed.','t key was pressed.']
        assert result == expected
    
    def test_open_app(self):
        # arrange
        test_app = 'test_app'
        # act
        result = self.computer.open_app(test_app)
        # assert
        expected = 'Start: test_app, left button was clicked.'
        assert result == expected
    
    def test_open_app_menu(self):
        # arrange
        test_app = 'test_app'
        # act
        result = self.computer.open_app_menu(test_app)
        # assert
        expected = 'OpenMenu: test_app, right button was clicked.'
        assert result == expected

このテストコードでテストを回します。

pytest -v

 :
======= 7 passed in 0.02s ========

全てPASSEDになりテスト成功、これでアプリケーションをリリースできますね!

機能修正

リリースした後に改善要望があったため、機能修正を行なうことになりました。
今回は電源オンやメッセージ入力時に表示される文字列が長いため、文字列を短くしてほしい、という要望です。
では、main.pyで該当箇所の修正を行ないます。

main.py
class Keyboard:
    def press_key(self, key: str) -> str:
#        return f'{key} key was pressed.'   # もともとの実装
        return f'pressed {key}.'            # 修正後実装

ちょっとだけ文字列を短くしました。
リリースのためにテストを回しましょう。

pytest -v

 :
FAILED test_app.py::TestKeyboard::test_press_key - AssertionError: assert 'pressed ENTER.' == 'ENTER key was pressed.'
FAILED test_app.py::TestComputer::test_power_on - AssertionError: assert 'pressed POWER.' == 'POWER key was pressed.'
FAILED test_app.py::TestComputer::test_input_message - AssertionError: assert ['pressed t.'... 'pressed t.'] == ['t key was p...was pressed.']
======= 3 failed, 4 passed in 0.04s ========

テスト結果が3つFAILEDになっています。
機能修正を行なったので、もちろんテストコードを修正しないといけないので、予期された結果です。
なので、テストコードを修正しましょう。修正箇所はキーボードを利用する機能のテストコード部分です。

test_app.py
# Keyboardクラスのテスト
class TestKeyboard:
          :
    def test_press_key(self):
        # arrange
        test_input = 'ENTER'
        # act
        result = self.keyboard.press_key(test_input)
        # assert
#        expected = 'ENTER key was pressed.'   # もともとの予測値
        expected = 'pressed ENTER.'            # 修正後の予測値
        assert result == expected

# Computerクラスのテスト
class TestComputer:
          :
    def test_power_on(self):
        # act
        result = self.computer.power_on()
        # assert
#        expected = 'POWER key was pressed.'   # もともとの予測値
        expected = 'pressed POWER.'            # 修正後の予測値
        assert result == expected
    
    def test_input_message(self):
        # arrange
        test_message = 'test'
        # act
        result = self.computer.input_message(test_message)
        # assert
#        expected = ['t key was pressed.','e key was pressed.','s key was pressed.','t key was pressed.']  # もともとの予測値
        expected = ['pressed t.','pressed e.','pressed s.','pressed t.']  # 修正後の予測値
        assert result == expected

テストコードの修正も終わったので、もう一度テストを回します。

pytest -v

 :
======= 7 passed in 0.02s ========

問題なくテストも完了しましたね、アプリケーションも起動してみましょう。

python main.py

pressed POWER.
['pressed h.', 'pressed e.', 'pressed l.', 'pressed l.', 'pressed o.', 'pressed !.']
Start: vscode, left button was clicked.
OpenMenu: vscode, right button was clicked.

想定どおり、キーボード操作に関する文字列が短くなっています。
これで修正後のアプリケーションをリリースしましょう!

...本当にこれで良いのか?

確かにテストもクリアし、アプリケーション自体も想定通りの動作をしました。
しかし、機能修正の際に行なったことを思い出してみてください。

  1. アプリケーションのKeyboardクラス内のpress_keyメソッドの修正
  2. テストコードのKeyboardクラスのテストコードおよび Computerクラスのテストコード を修正

今回の要望を叶えるにはキーボードの機能修正のみでよく、修正箇所がキーボードであればテストコードもキーボードに関するテストを修正するだけで良いはずです。
それなのに、今回は コンピュータに関するテストコード も修正しています。

このように、「ある機能の修正をしたときに、別の機能にも影響する」ような場合、結合度が高い 実装になっています。

  • ん?呼び出している機能が修正されれば、呼び出し元の動作が変わるのは当たり前では?

はい、その通りです。ただし、この考え方はE2E(結合動作)観点です。
さきほど記載したように、「修正箇所がキーボードであればテストコードもキーボードに関するテストを修正するだけで良い」はずなのに、「コンピュータに関するテストコード」も修正しているのは、すこし影響範囲が大きいと思いませんか?
DI/DIPで取り上げられる結合度はさまざまな捉え方がありますが、テストコードの修正範囲がアプリケーションの修正範囲を超えるものは結合度が高い、と捉えるとわかりやすいかもしれません。

Computerクラスのテストコードって...

さらに、Computerクラスのテストコードをよく考えてみます。
例として、power_onメソッドのテストコードは以下のような実装です。

    def test_power_on(self):
        # act
        result = self.computer.power_on()
        # assert
        expected = 'pressed POWER.'
        assert result == expected

これは果たしてComputerクラスのpower_onメソッドをテストしているのでしょうか?
冒頭に記載したクラス設計を思い出してみてください。Computerクラスが提供するのは、以下の機能です。

    各機能は内部で行なわれた操作を知らせる文字列を返す

つまり、「Computerクラスのpower_onメソッドは内部で行なわれる処理がどのような場合であれ、文字列を返す」ことがこのメソッドの機能であり、
テストで見るべきは「power_onメソッドを実行すると任意の文字列を返す」かどうかを見るべきです。

この観点でみると、現在のテストコードは「Computer.power_onメソッドから呼び出されるKeyboard.press_keyが特定の文字列を返すこと」を確認するコードになっています。
これではpower_onメソッドの機能をテストできているかというと違いますね。
DI/DIPの話でよく出てくる単語で言い換えると、「ComputerクラスはKeyboardクラス/Mouseクラスに依存している」です。
そのため、先ほどキーボード機能を修正したときにコンピュータのテストコードも修正することになってしまったのです。

クラス関係

このときのクラス関係は以下のようになっています。

それではComputerクラスとKeyboardクラス、例には記載していませんが同様の実装をしているMouseクラスを切り離すにはどうすればよいでしょうか。
ここで出てくるのが DI: Dependency Injection です。

DI: Dependency Injection

DIを使って、ComputerクラスとKeyboardMouseクラスを切り離してみましょう。

アプリケーション実装

DIを使った実装は以下のようになります。

main.py
class Keyboard:
    def press_key(self, key: str) -> str:
        return f'pressed {key}.'

class Mouse:
    def click_left_button(self) -> str:
        return 'left button was clicked.'
    
    def click_right_button(self) -> str:
        return 'right button was clicked.'

class Computer:
# <== 修正箇所 ==================
    def __init__(self, keyboard: Keyboard, mouse: Mouse):
        self.keyboard = keyboard
        self.mouse = mouse
# ==============================>

    def power_on(self) -> str:
        return self.keyboard.press_key('POWER')

    def input_message(self, message:str) -> list:
        press_list = list()
        for char in message:
            press_list.append(self.keyboard.press_key(char))
        return press_list
    
    def open_app(self, app: str) -> str:
        return f'Start: {app}, {self.mouse.click_left_button()}'
    
    def open_app_menu(self, app: str) -> str:
        return f'OpenMenu: {app}, {self.mouse.click_right_button()}'


if __name__ == '__main__':
# <== 修正箇所 ==================
    # Keyboardクラスのインスタンス作成
    keyboard = Keyboard()
    # Mouseクラスのインスタンス作成
    mouse = Mouse()
    # Computerクラスのインスタンス作成
    computer = Computer(keyboard, mouse)
# ==============================>
    # power on
    print(computer.power_on())
    # input message
    print(computer.input_message('hello!'))
    # open application
    print(computer.open_app('vscode'))
    # open application menu
    print(computer.open_app_menu('vscode'))

先ほどの実装との違いは、<== 修正箇所 ==> になります。
これだけ?と思うでしょう。はい、これだけです。
試しにアプリケーションを起動してみます。

python main.py 

pressed POWER.
['pressed h.', 'pressed e.', 'pressed l.', 'pressed l.', 'pressed o.', 'pressed !.']
Start: vscode, left button was clicked.
OpenMenu: vscode, right button was clicked.

アプリケーションの動作は変わらず、正常に動作しています。

さて、実装においての違いをみてみましょう。

  • 先ほどの実装
    • KeyboardクラスおよびMouseクラスのインスタンス作成をComputerクラス内で行なっている
  • DIを使った実装
    • KeyboardクラスおよびMouseクラスのインスタンス作成を外部で行ない、
      Computerクラスのインスタンス作成時にそれらのインスタンスを渡している

先ほど記載したように、このアプリケーションはComputerクラスはKeyboardクラス/Mouseクラスに依存しています。
この依存しているクラスを依存する側であるComputerクラスに渡している=注入しているため、
DI:Dependency Injection、依存性の注入 と呼ばれています。

テストコード実装

ちょっとしたコード修正だけですが、どのような効果があるのでしょうか。
それではテストコードを実装します。

test_app.py
import main

# Keyboardクラスのテスト
class TestKeyboard:
    def setup_method(self):
        # Keyboardクラスのインスタンス作成
        self.keyboard = main.Keyboard()

    def test_press_key(self):
        # arrange
        test_input = 'ENTER'
        # act
        result = self.keyboard.press_key(test_input)
        # assert
        expected = 'pressed ENTER.'
        assert result == expected

# Mouseクラスのテスト
class TestMouse:
    def setup_method(self):
        # Mouseクラスのインスタンス作成
        self.mouse = main.Mouse()
    
    def test_click_left_utton(self):
        # act
        result = self.mouse.click_left_button()
        # assert
        expected = 'left button was clicked.'
        assert result == expected

    def test_click_right_utton(self):
        # act
        result = self.mouse.click_right_button()
        # assert
        expected = 'right button was clicked.'
        assert result == expected

# <== 追加 ==========================
# テスト用Keyboardクラス
class KeyboardTest:
    def press_key(self, key: str) -> str:
        # arrange
        return key

# テスト用Mouseクラス
class MouseTest:
    def click_left_button(self) -> str:
        # arrange
        return 'left click'
    
    def click_right_button(self) -> str:
        # arrange
        return 'right click'
# ==================================>

# Computerクラスのテスト
class TestComputer:
    def setup_method(self):
        # <== 修正箇所 =====================
        # テスト用Keyboardクラスのインスタンス作成
        test_keyboard = KeyboardTest()
        # テスト用Mouseクラスのインスタンス作成
        test_mouse = MouseTest()
        # Computerクラスのインスタンス作成
        self.computer = main.Computer(test_keyboard, test_mouse)
        # ================================>

    def test_power_on(self):
        # act
        result = self.computer.power_on()
        # assert
        expected = 'POWER'              # <== 修正箇所
        assert result == expected
    
    def test_input_message(self):
        # arrange
        test_message = 'test'
        # act
        result = self.computer.input_message(test_message)
        # assert
        expected = ['t','e','s','t']    # <== 修正箇所
        assert result == expected
    
    def test_open_app(self):
        # arrange
        test_app = 'test_app'
        # act
        result = self.computer.open_app(test_app)
        # assert
        expected = 'Start: test_app, left click'       # <== 修正箇所
        assert result == expected
    
    def test_open_app_menu(self):
        # arrange
        test_app = 'test_app'
        # act
        result = self.computer.open_app_menu(test_app)
        # assert
        expected = 'OpenMenu: test_app, right click'   # <== 修正箇所
        assert result == expected

まずはこれでテストを回してみます。

pytest -v

 :
======= 7 passed in 0.02s ========

問題なく成功しますね。

機能修正

先ほどとの比較をするために、またKeyboardクラスのpress_keyメソッドを修正します。
キー押下に成功してるのかどうかわかりにくい!と要望をもらったので、文字列にsuccessという文字を追加します。

main.py
class Keyboard:
    def press_key(self, key: str) -> str:
#        return f'pressed {key}.'   # もともとの実装
        return f'pressed {key} success.'    # 修正後実装

アプリケーションの修正は終わったので、テストコードを修正します。

test_app.py
# Keyboardクラスのテスト
class TestKeyboard:
          :
    def test_press_key(self):
        # arrange
        test_input = 'ENTER'
        # act
        result = self.keyboard.press_key(test_input)
        # assert
#        expected = 'pressed ENTER.'          # もともとの予測値
        expected = 'pressed ENTER success.'   # 修正後の予測値
        assert result == expected

これで再度テストを回してみます。

pytest -v

 :
======= 7 passed in 0.02s ========

今回はキーボードのテストコードを修正するだけで成功しました!

依存する側のテストコード

ポイントは、テストコードにおける修正箇所および追加の箇所です。
特に、テスト用に作成した以下2つのクラスがポイントです。

test_app.py
# <== 追加 ==========================
# テスト用Keyboardクラス
class KeyboardTest:
    def press_key(self, key: str) -> str:
        # arrange
        return key

# テスト用Mouseクラス
class MouseTest:
    def click_left_button(self) -> str:
        # arrange
        return 'left click'
    
    def click_right_button(self) -> str:
        # arrange
        return 'right click'
# ==================================>

これってテストとして意味があるの?と思うかもしれませんが、ここでも改めてクラス設計を思い出してください。

    各機能は内部で行なわれた操作を知らせる文字列を返す

Computerクラスの機能は、内部で行なわれた操作がなんであれ、文字列で返すだけです。
今回の例ではテストクラスと本番クラスの違いはないですが、たとえば本番クラスにおいて文字列をパースするような処理がおこなわれていたとしても、
Computerクラスとしては文字列さえ返してくれれば良いのです。

このように、DIを使った実装の場合、

  • 依存する側のテストを行なう場合は、依存先のクラスをテスト用クラスに差し替える

ことが可能になり、その結果キーボード機能の修正を行なってもコンピュータ機能のテストコードは修正が必要なくなった = KeyboardクラスとComputerクラスの結合度が低くなった、ということになります。
結合度を低くし、依存する側の単体テストを行なうことができるようになるのがメリットの一つでもあります。

クラス関係

DIを使った場合のクラス関係は以下のようになっています。

DIはあくまで依存性を外部から注入するのみであるため、クラス関係、もう少しいうと依存方向に変化はありません。
つまり、Computerクラスは依存する側のまま、Keyboardクラス、Mouseクラスは依存される側のままです。

DIP: Dependency Inversion Principle

DIは上記のように依存関係を切り離し、単体テストを行ないやすくなるメリットがあります。
ただ、もちろんデメリットもあります。
例えば以下のような実装をしてみます。

main.py
        :
class CustomKeyboard:
    def press_macro_key(self, key: str) -> str:
        return f'macro: ctrl+{key} pressed.'

if __name__ == '__main__':
    # CustomKeyboardクラスのインスタンス作成
    custom_keyboard = CustomKeyboard()
    # Mouseクラスのインスタンス作成
    mouse = Mouse()
    # Computerクラスのインスタンス作成
    computer = Computer(custom_keyboard, mouse)
        :

通常のキーボードでは満足できない人が、マクロを実装したCustomKeyboardクラスを実装し、これをComputerクラスに渡しました。
この状態でアプリケーションを実行してみます。

python main.py

Traceback (most recent call last):
  File "/home/user/qiita_docs/workspace/use_dip/main.py", line 44, in <module>
    print(computer.power_on())
          ^^^^^^^^^^^^^^^^^^^
  File "/home/user/qiita_docs/workspace/use_dip/main.py", line 18, in power_on
    return self.keyboard.press_key('POWER')
           ^^^^^^^^^^^^^^^^^^^^^^^
AttributeError: 'CustomKeyboard' object has no attribute 'press_key'

CustomKeyboardクラスにはpress_keyメソッドが定義されていないため、もちろんエラーが発生します。
この程度のコードであればこんなミスは起きませんが、チームで開発している場合依存する側であるComputerクラスが求めているメソッドを知らずに実装してしまうこともあります。

このように、DIを使うと依存する側のクラスと依存される側のクラスの実装が切り離されてしまい、
コードの不整合が起きやすくなってしまいます。

この問題を解決するには、依存する側と依存される側が共通の規則に則って実装する必要があります。
設計書などに開発規則を記載することも一つの手ですが、コード実装においても規則による制約を設けることが一番良いです。
これを実現する方法が DIP:Dependency Inversion Principle、依存性逆転の原則 です。

アプリケーション実装

DIPを使ってアプリケーションを実装してみます。
コードは以下のような実装になります。

main.py
# <== 追加 =========================
from abc import ABCMeta, abstractmethod

# Keyboard Interface
class InterfaceKeyboard(metaclass=ABCMeta):
    @abstractmethod
    def press_key(self, key: str) -> str:
        pass

# Mouse Interface
class InterfaceMouse(metaclass=ABCMeta):
    @abstractmethod
    def click_left_button(self) -> str:
        pass
    
    @abstractmethod
    def click_right_button(self) -> str:
        pass
# =================================>

# Keyboard Interfaceを継承
class Keyboard(InterfaceKeyboard):          # <== 修正箇所
    def press_key(self, key: str) -> str:
        return f'pressed {key} success.'

# Mouse Interfaceを継承
class Mouse(InterfaceMouse):                # <== 修正箇所
    def click_left_button(self) -> str:
        return 'left button was clicked.'
    
    def click_right_button(self) -> str:
        return 'right button was clicked.'

class Computer:
    # 抽象クラスを受け取る
    def __init__(self, keyboard: InterfaceKeyboard, mouse: InterfaceMouse):   # <== 修正箇所
        self.keyboard = keyboard
        self.mouse = mouse
    
    def power_on(self) -> str:
        return self.keyboard.press_key('POWER')

    def input_message(self, message:str) -> list:
        press_list = list()
        for char in message:
            press_list.append(self.keyboard.press_key(char))
        return press_list
    
    def open_app(self, app: str) -> str:
        return f'Start: {app}, {self.mouse.click_left_button()}'
    
    def open_app_menu(self, app: str) -> str:
        return f'OpenMenu: {app}, {self.mouse.click_right_button()}'


if __name__ == '__main__':
    # CustomKeyboardクラスのインスタンス作成
    keyboard = Keyboard()
    # Mouseクラスのインスタンス作成
    mouse = Mouse()
    # Computerクラスのインスタンス作成
    computer = Computer(keyboard, mouse)
    # power on
    print(computer.power_on())
    # input message
    print(computer.input_message('hello!'))
    # open application
    print(computer.open_app('vscode'))
    # open application menu
    print(computer.open_app_menu('vscode'))

一つずつ説明します。
まずは一番重要なポイントとなる抽象クラス、Javaでいうところのインターフェイスについてです。
Pythonではabcモジュールを利用することでインターフェイスの実装ができます。

from abc import ABCMeta, abstractmethod

# Keyboard Interface
class InterfaceKeyboard(metaclass=ABCMeta):
    @abstractmethod
    def press_key(self, key: str) -> str:
        pass

# Mouse Interface
class InterfaceMouse(metaclass=ABCMeta):
    @abstractmethod
    def click_left_button(self) -> str:
        pass
    
    @abstractmethod
    def click_right_button(self) -> str:
        pass

これらは、文字通り内部に実装を持たない 抽象的な クラスです。
抽象クラスの目的は、「抽象クラスを継承するクラスに対し、実装するメソッドを強制化する」ためにあります。
実装を強制させるメソッドは@abstractmethodデコレータを付与して定義します。

では、抽象クラスを継承するように変更したKeyboardクラスとMouseクラスを見てみます。

# Keyboard Interfaceを継承
class Keyboard(InterfaceKeyboard):          # <== 修正箇所
    def press_key(self, key: str) -> str:
        return f'pressed {key} success.'

# Mouse Interfaceを継承
class Mouse(InterfaceMouse):                # <== 修正箇所
    def click_left_button(self) -> str:
        return 'left button was clicked.'
    
    def click_right_button(self) -> str:
        return 'right button was clicked.'

継承するだけなので、目立った変化はありませんね。

抽象クラスの効力は後で見るとして、最後にComputerクラスをみてみます。

class Computer:
    # 抽象クラスを受け取る
    def __init__(self, keyboard: InterfaceKeyboard, mouse: InterfaceMouse):   # <== 修正箇所
        self.keyboard = keyboard
        self.mouse = mouse

__init__()において受け取るkeyboardmouse変数の型を、
Keyboardクラス/MouseクラスからInterfaceKeyboardクラス/InterfaceMouseクラスに変更しています。
この修正についても後で見ることにします。

このアプリケーションを実行してみます。

python main.py

pressed POWER success.
['pressed h success.', 'pressed e success.', 'pressed l success.', 'pressed l success.', 'pressed o success.', 'pressed ! success.']
Start: vscode, left button was clicked.
OpenMenu: vscode, right button was clicked.

もちろん動作に変わりはありません。

抽象クラスの効力

上記の実装だけでは、抽象クラスのありがたみはわかりませんし、コードも増えてむしろ改悪になっているように見えます。
そこで、さきほど例にあげたCustomKeyboardをもう一度実装してみます。
この開発者はどうしてもマクロを実装したキーボードを利用したいらしく、抽象クラスを継承したCustomKeyboardクラスを実装しました。

        :
class CustomKeyboard(InterfaceKeyboard):          # <== 抽象クラスを継承
    def press_macro_key(self, key: str) -> str:
        return f'macro: ctrl+{key} pressed.'

if __name__ == '__main__':
    # CustomKeyboardクラスのインスタンス作成
    custom_keyboard = CustomKeyboard()
    # Mouseクラスのインスタンス作成
    mouse = Mouse()
    # Computerクラスのインスタンス作成
    computer = Computer(custom_keyboard, mouse)
        :

このアプリケーションを実行してみます。

Traceback (most recent call last):
  File "/home/user/qiita_docs/workspace/use_dip/main.py", line 62, in <module>
    custome_keyboard = CustomeKeyboard()
                       ^^^^^^^^^^^^^^^^^
TypeError: Can't instantiate abstract class CustomeKeyboard without an implementation for abstract method 'press_key'

エラーになりました。
さきほどもエラーになりましたが、エラー箇所を見比べてみます。

Traceback (most recent call last):
  File "/home/user/qiita_docs/workspace/use_dip/main.py", line 44, in <module>
    print(computer.power_on())
          ^^^^^^^^^^^^^^^^^^^
  File "/home/user/qiita_docs/workspace/use_dip/main.py", line 18, in power_on
    return self.keyboard.press_key('POWER')
           ^^^^^^^^^^^^^^^^^^^^^^^
AttributeError: 'CustomKeyboard' object has no attribute 'press_key'

DIの実装では、CustomKeyboardインスタンスを受け取ったComputerクラスがメソッドを実行した際にエラーが起きたのに対し、
DIPの実装では、CustomKeyboardインスタンスの作成時点でエラーが起きています。
抽象クラスは実装するメソッドを強制化しているため、抽象クラスを継承したクラスが@abstractmethodがついているメソッドを実装していない場合、
インスタンス作成時点でエラーを起こすようになっています。

抽象クラスを継承しなかったら?

ここまできてもまだ諦められない開発者はこう考えます。「抽象クラスを継承しなければいいのでは?」と。
抽象クラスを継承することで実装するメソッドを強制化していますが、抽象クラスを継承することは強制されていません。
なので、以下のような実装をしてみます。

        :
class CustomeKeyboard:
    def press_macro_key(self, key: str) -> str:
        return f'macro: ctrl+{key} pressed.'

if __name__ == '__main__':
    # CustomKeyboardクラスのインスタンス作成
    custom_keyboard = CustomKeyboard()
    # Mouseクラスのインスタンス作成
    mouse = Mouse()
    # Computerクラスのインスタンス作成
    computer = Computer(custom_keyboard, mouse)
        :

これはDIのときと同様のコードですね。
このアプリケーションを実行してみます。

python main.py

Traceback (most recent call last):
  File "/home/user/qiita_docs/workspace/use_dip/main.py", line 68, in <module>
    print(computer.power_on())
          ^^^^^^^^^^^^^^^^^^^
  File "/home/user/qiita_docs/workspace/use_dip/main.py", line 40, in power_on
    return self.keyboard.press_key('POWER')
           ^^^^^^^^^^^^^^^^^^^^^^^
AttributeError: 'CustomeKeyboard' object has no attribute 'press_key'

抽象クラスを継承していないのでインスタンス作成に成功し、DIの時と同様にComputerクラスのメソッド実行時にエラーが出るようになりました。

ここで先ほど説明を省略したComputerクラスのコンストラクタがポイントになります。

class Computer:
    # 抽象クラスを受け取る
    def __init__(self, keyboard: InterfaceKeyboard, mouse: InterfaceMouse):
        self.keyboard = keyboard
        self.mouse = mouse

コンストラクタはInterfaceKeyboardクラスを受け取るようにしているはずなのに、InterfaceKeyboardクラスを継承していないCustomKeyboardを受け取ってしまっているのが問題です。
この原因はPythonは型ヒントは行なえるものの、型の制約はかけていない点にあります。
この状態ではどのようなクラスでも受け取れてしまい、せっかく実装するメソッドを制限するために抽象クラスを実装したとしても、
抽象クラスを継承していないインスタンスでも受け取ってしまう状況なので意味がありません。
これでは不十分な実装です。

では、コンストラクタが受け取るインスタンスの型を、抽象クラスを継承したインスタンスに制限する方法はどのようにすればよいでしょうか。
この問題は、isinstance()を利用することで解決できます。
Computerクラスのコンストラクタに、以下を追加してみます。

class Computer:
    # Interfaceを受け取る
    def __init__(self, keyboard: InterfaceKeyboard, mouse: InterfaceMouse):
        # <== 追加 ======================
        if not isinstance(keyboard, InterfaceKeyboard):
            raise TypeError
        if not isinstance(mouse, InterfaceMouse):
            raise TypeError
        # =============================>
        self.keyboard = keyboard
        self.mouse = mouse

isinstance()は、第一引数に指定した変数が、第二引数に指定した型で定義されているかをチェックする関数です。
ここでクラスを指定すると、InterfaceKeyboardクラス自体か、もしくはInterfaceKeyboardクラスを継承しているかどうかをチェックします。
この実装では、InterfaceKeyboardクラスを継承していない場合にTypeErrorを発生するようにしています。
では再度アプリケーションを実行してみます。

python main.py

Traceback (most recent call last):
  File "/home/user/qiita_docs/workspace/use_dip/main.py", line 71, in <module>
    computer = Computer(custome_keyboard, mouse)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/user/qiita_docs/workspace/use_dip/main.py", line 37, in __init__
    raise TypeError
TypeError

Computerクラスのインスタンスを作成するタイミングでTypeErrorが発生するようになりました。

これで、

  • 抽象クラスによりクラスが実装するメソッドの強制化
  • 抽象クラスを利用することの強制化

が行なえるようになりました。

クラス関係

DIPを使った場合のクラス関係は以下のようになっています。

DIでは依存される側だったKeyboardクラスとMouseクラスが、DIPでは依存する側に変わっています。
このように依存方向が逆転しているため、依存性逆転 と呼ばれいています。

最終実装コード

最終的な実装コードは以下になります。

main.py
from abc import ABCMeta, abstractmethod

# Keyboard Interface
class InterfaceKeyboard(metaclass=ABCMeta):
    @abstractmethod
    def press_key(self, key: str) -> str:
        pass

# Mouse Interface
class InterfaceMouse(metaclass=ABCMeta):
    @abstractmethod
    def click_left_button(self) -> str:
        pass
    
    @abstractmethod
    def click_right_button(self) -> str:
        pass


# Keyboard Interfaceを継承
class Keyboard(InterfaceKeyboard):
    def press_key(self, key: str) -> str:
        return f'pressed {key} success.'

# Mouse Interfaceを継承
class Mouse(InterfaceMouse):
    def click_left_button(self) -> str:
        return 'left button was clicked.'
    
    def click_right_button(self) -> str:
        return 'right button was clicked.'

class Computer:
    # Interfaceを受け取る
    def __init__(self, keyboard: InterfaceKeyboard, mouse: InterfaceMouse):
        if not isinstance(keyboard, InterfaceKeyboard):
            raise TypeError
        if not isinstance(mouse, InterfaceMouse):
            raise TypeError
        
        self.keyboard = keyboard
        self.mouse = mouse
    
    def power_on(self) -> str:
        return self.keyboard.press_key('POWER')

    def input_message(self, message:str) -> list:
        press_list = list()
        for char in message:
            press_list.append(self.keyboard.press_key(char))
        return press_list
    
    def open_app(self, app: str) -> str:
        return f'Start: {app}, {self.mouse.click_left_button()}'
    
    def open_app_menu(self, app: str) -> str:
        return f'OpenMenu: {app}, {self.mouse.click_right_button()}'


class CustomeKeyboard(InterfaceKeyboard):
    def press_key(self, key):
        return f'single key: {key} pressed.'
    
    def press_macro_key(self, key: str) -> str:
        return f'macro: ctrl+{key} pressed.'

if __name__ == '__main__':
    # CustomKeyboardクラスのインスタンス作成
    #keyboard = Keyboard()
    custome_keyboard = CustomeKeyboard()
    # Mouseクラスのインスタンス作成
    mouse = Mouse()
    # Computerクラスのインスタンス作成
    computer = Computer(custome_keyboard, mouse)
    # power on
    print(computer.power_on())
    # input message
    print(computer.input_message('hello!'))
    # open application
    print(computer.open_app('vscode'))
    # open application menu
    print(computer.open_app_menu('vscode'))

一点補足として、抽象クラスを継承したクラスは@abstractmethodが付与されたメソッドを定義することを強制されますが、
それさえ実装されていれば独自のメソッドを定義しても問題はありません。
今回のコードでいうと、通常のキーボード機能さえ実装してくれれば、マクロを実装しても問題はないということです。

DI/DIPまとめ

ここまでをまとめると、以下のような流れになります。

  1. あるクラスがあるクラスに依存している場合に、単体テスト等が行なえない状況(結合度が高い状況)になっている
    DI を使って依存されるクラスを外部から注入することで、結合度を低くし、単体テストが行なえるようにする
  2. 外部から注入するようにしたことで、依存する側/される側間で実装メソッドに不整合が発生してしまう
    DIP を使って、実装するメソッドの強制化を行なう

DI/DIPを使えばいいコードになるわけではありません。サンプルコード程度でもDI/DIPを使うとコードの可読性は落ちます。
アプリケーションの規模やクラス設計などによって、どこまで実現するべきか?を検討したうえで、DI/DIPを活用してもらうことが一番です。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?