オブジェクト指向設計の原則の1つは、実装レベルではなく、インターフェイスレベルでのプログラミングです。 どうやら、デザインに関する書籍や記事のコードは主にJavaで表示されるため、他の言語、特に動的型付けのプログラマーは、これらの書籍や記事から作業用プログラミング言語に知識を移すのが困難です。
多くの場合、「インターフェイスレベルのプログラム」の原則を理解することの難しさは、意味ではなく楽器に集中することにあります。 Javaにはinterfaceキーワードが存在するため、原則の誤解が生じ、「 interface
を使用するプログラム」に変わりinterface
。 Pythonにはinterface
キーワードの形式のツールがないため、一部のパイオニストはこの原則をスキップします。
ギャングオブフォーの本では、SmalltalkとC ++で例を示しています。 これらの言語は両方ともキーワードinterface
持ちませんが、これは著者が利用可能な言語構成を使用して原則を適用することを妨げません:
抽象クラスのインターフェースを介して厳密にオブジェクトを操作することには、2つの利点があります。
- クライアントが使用する特定のタイプのオブジェクトに関する情報を持っている必要はありません。ただし、すべてのオブジェクトがクライアントが期待するインターフェースを持っている場合。
- クライアントは、オブジェクトが実装されるクラスについて「知る」必要はありません。 クライアントは、インターフェースを定義する抽象クラスについてのみ知っています。
これらの利点により、サブシステム間の依存関係の数が大幅に削減されるため、再利用のためにオブジェクト指向設計の原則を定式化することもできます 。 実装ではなく、インターフェイスに従ってプログラムを作成します 。
しかし、より広い角度から原理を見ると、引用で引用されている利点だけが利点ではありません。
実世界
ロシア語版ウィキペディアのインターフェースの最も一般的な定義は次のとおりです。
インターフェイスは、個々のシステム間の「共通の境界」であり、それらを介して相互作用します。 個々のシステムの相互作用を保証するツールとルールのセット。
人間はシステムです。つまり、私たちは毎日、他のシステムと対話するための多くのインターフェースに遭遇します。 実世界での興味深いインターフェースの例は、Mosigraからの投稿(1、2、3、4)にあります。 優れたインターフェースとのやり取りは、不必要な手間をかけずに行われます。どのように使用するかに気付きません。 さらに、最もひどいインターフェイスとのやり取りでも、インターフェイスの不便さにのみ注意を払いますが、どちらの場合も実装は目に見えません。
電子レンジで作業するときは、オーブンの電源を入れたり、時間、電力、加熱モードを設定したりするためのコントロールを組み合わせて使用します。 一連のルールだけでなく、料理を温めるには、オーブンに入れてドアを閉め、加熱を開始する必要があります。 同意します。もし昼食を暖めるためにオーブンの電気回路を変更し、それによってマグネトロンの周波数を調整する必要がある場合、これは不便でしょう。 インターフェイスを使用してオーブンを制御することで、最終目標を達成するための時間と労力を削減するだけでなく、ランチを温めるだけでなく、他のタイプの電子レンジや他のメーカーの電子レンジで作業するときにインターフェイスの知識を使用することもできます。
コードの世界
概念的には、プログラムコード内のインターフェイスの意味は、実世界のそれと変わりません。 インターフェースは、個々のシステム間の同じ「共通の境界」です。 この場合にのみ、システムはサービス、マイクロサービス、パッケージ、クラス、または機能です。 これらのプログラムコードの各ユニットは、相互作用する必要があり、違反する必要のない独自の境界を持つシステムです。
夕食を温めるために電子レンジの電気回路を変更する必要性について上記の状況はばかげているようです。 しかし、プログラマーは日常業務でこれに対処する必要があります。 その意図を表していないメソッドの名前が貧弱であっても、実装の開示につながる可能性があります。
class UserCollection: # def linear_search_for(user: User) -> bool: for saved_user in self._all_users: if saved_user == user: return True return False
この名前は、内部で使用されるアルゴリズム、およびUserCollection
基礎となるデータ構造をUserCollection
ます。 この情報はすべて、このレベルの抽象化では不要であり、意図を伝えにくいため、さらに拡張するには不便です。 インターフェイスをきれいにするために、メソッドの名前で「どのように」コードを作成し、「どのように」コードを作成するのかを表現しましょう。
class UserCollection: # def includes(user: User) -> bool: ''' '''
基本的に反射的な名前は、インターフェイスレベルでプログラミングする能力に大きく貢献しますが、名前だけでは原則を実装するには不十分です。 たとえば、明確な名前を持つこの単純な関数は、インターフェイスのみを知っているため、使用が困難です。
from utils import DatabaseConfig # DatabaseConfig , # def is_password_valid(password: str) -> bool: min_length = DatabaseConfig().password_min_length return len(password) > min_length
インターフェイスは私たちを欺き、パスワードだけで十分であると述べています。 必要なデータベースが発生しない環境でこの関数を呼び出すと、エラーが発生し、実装へのアクセスが強制されます。 実装を参照する必要性を取り除くために、インターフェースを充実させます。 この場合、 min_length
パラメーターを明示的に渡すことがmin_length
です。
# DatabaseConfig def is_password_valid(password: str, min_length: int) -> bool: return len(password) > min_length
DatabaseConfig
への暗黙的な依存関係が解決されました。 これに加えて、テストに適した関数を取得しました。これは、必要なすべての入力パラメーターと既知のタイプの出力パラメーターを備えた真のブラックボックスです。
暗黙的な依存関係は、プログラムコードの任意のユニットの実装を明らかにします。 Pythonでは、ファイルのインポート時に実行されるコードを作成できます。 一見、これは無害に見えるかもしれませんが、この方法で作成された暗黙的な依存関係には多くの問題が伴います。
# utils.py class DatabaseConfig: ''' ''' config = DatabaseConfig() # def is_password_valid(password: str, min_length: int) -> bool: return len(password) > min_length
# user.py from utils import is_password_valid # # DatabaseConfig class User: def __init__(self, name: str, password: str): self.name = name self.password = password def change_password(self, new_password: str) -> None: if not is_password_valid(new_password, min_length=6): raise Exception('Invalid password') self.password = new_password
必要なデータベースが実行されていない場合、Userクラスをインタープリターまたはテストにインポートすると、再びエラーが発生し、実装が明らかになります。 この場合のトラブルの原因はfrom utils import is_password_valid
式、つまりインポート中に作成されるグローバル変数であるため、クラスインターフェイスを変更することは意味がありません。 グローバル変数のもう1つの欠点は、インターフェイスレベルでプログラミングできないことです。 問題を解決するには、アプリケーションの起動時にDatabaseConfig
インスタンスを作成し、インスタンスを関係するすべてのオブジェクトに明示的に転送します。
暗黙的な依存関係がなく、名前の本質を反映し、実装の詳細から保護しているため、インターフェイスレベルでプログラミングのすべての利点を得ることができません。 今度は、抽象クラスのインターフェースを介して厳密にプログラミングを行うときです。 ケント・ベックは本「Smalltalk Best Practice Patterns」に次のように書いています。
プロジェクトが良い形であるかどうかの良い予測因子である私が探しているものがいくつかあります。
...
オブジェクトの置換-良いスタイルは、簡単に置換可能なオブジェクトにつながります。 本当に優れたシステムでは、ユーザーが「これとは根本的に異なることをやりたい」と言うたびに、開発者は「ああ、新しい種類のXを作成してプラグインする必要があります」と言います。
特定のクラスの代わりに抽象クラスによって定義されたインターフェースを使用することは、交換可能なオブジェクトを作成するための便利なテクニックです。 Pythonでは、標準のabcライブラリのモジュールを使用して抽象クラスを作成できますが、コードをコンパクトにするために、抽象クラスの未実現メソッドがNotImplementedError
スローする場合のアプローチを使用します。
今日と今週の天気予報を実装する必要があるとします。 サードパーティのリソースから天気予報を取得します。 特定のリソース、およびリソースによって返されるデータ形式にアタッチされないようにするには、抽象クラスの形式で通信メソッドを形式化し、値オブジェクトの形式でデータ形式を形式化する必要があります。
# weather.py from typing import List, NamedTuple class Weather(NamedTuple): max_temperature_: int avg_temperature_: int min_temperature_c: int class WeatherService: def get_today_weather(self, city: str) -> Weather: raise NotImplementedError def get_week_weather(self, city: str) -> List[Weather]: raise NotImplementedError
特定の実装がなければ、提供されたインターフェイスに依存するコードのクライアントは、実際のサービスの代わりに代替オブジェクトを使用して、テストと開発を開始できます。
# test.py from client import WeatherWidget from weather import Weather, WeatherService class FakeWeatherService(WeatherService): def __init__(self): self._weather = Weather(max_temperature_ = 24, avg_temperature_ = 20, min_temperature_c = 16) def get_today_weather(self, city: str) -> Weather: return self._weather def get_week_weather(self, city: str) -> List[Weather]: return [self._weather for _ in range(7)] def test_present_today_weather_in_string_format(): weather_service = FakeWeatherService() widget = WeatherWidget(weather_service) expected_string = ('Maximum Temperature: 24 °C' 'Average Temperature: 20 °C' 'Minimum Temperature: 16 °C') assert widget.today_weather == expected_string
このインターフェースは柔軟性を提供します。ユーザーが予報の精度に満足していない場合、 WeatherService
インターフェースを実装するクラスを作成することにより、別の天気予報リソースに簡単に切り替えることができます。
「実装ではなくインターフェイスに応じたプログラム」の原則を使用すると、より柔軟なアプリケーション設計を作成し、開発者の頭を解放し、チーム内のコミュニケーションを改善できます。 これにより、システムは新しい機能のサポートと追加により適したものになります。 Pythonにはinterface
キーワードはありませんが、原則を実装する他の方法があります:名前の本質を反映する、暗黙的な依存関係を排除する、抽象クラスを使用する。 良いコードの根底にある原則の本質にもっと注意を払いましょう。すべての注意をツールに集中するのではありません。
UPD
pacahonはUserCollectionのPython方式のインターフェースをUserCollection
class UserCollection: # def __contains__(user: User) -> bool: ''' '''
__contains__
メソッドを使用in
とnot in
を使用in
て要素が所有されている__contains__
を確認でき__contains__
。 __contains__
インターフェースでtype-hintsを使用すると、PyCharmはこの場合のint
が間違った型であることを通知します:
print(1 in UserCollection())