Pythonのクリーンアーキテクチャ:段階的なデモ。 パート3



内容




シナリオ(パート2)



Gitタグ: Step06







要求オブジェクトと応答オブジェクトを実装したので、それらを追加します。 tests/use_cases/test_storageroom_list_use_case.py



ファイルに次のコードをtests/use_cases/test_storageroom_list_use_case.py



ます。







 import pytest from unittest import mock from rentomatic.domain.storageroom import StorageRoom from rentomatic.use_cases import request_objects as ro from rentomatic.use_cases import storageroom_use_cases as uc @pytest.fixture def domain_storagerooms(): storageroom_1 = StorageRoom( code='f853578c-fc0f-4e65-81b8-566c5dffa35a', size=215, price=39, longitude='-0.09998975', latitude='51.75436293', ) storageroom_2 = StorageRoom( code='fe2c3195-aeff-487a-a08f-e0bdc0ec6e9a', size=405, price=66, longitude='0.18228006', latitude='51.74640997', ) storageroom_3 = StorageRoom( code='913694c6-435a-4366-ba0d-da5334a611b2', size=56, price=60, longitude='0.27891577', latitude='51.45994069', ) storageroom_4 = StorageRoom( code='eed76e77-55c1-41ce-985d-ca49bf6c0585', size=93, price=48, longitude='0.33894476', latitude='51.39916678', ) return [storageroom_1, storageroom_2, storageroom_3, storageroom_4] def test_storageroom_list_without_parameters(domain_storagerooms): repo = mock.Mock() repo.list.return_value = domain_storagerooms storageroom_list_use_case = uc.StorageRoomListUseCase(repo) request_object = ro.StorageRoomListRequestObject.from_dict({}) response_object = storageroom_list_use_case.execute(request_object) assert bool(response_object) is True repo.list.assert_called_with() assert response_object.value == domain_storagerooms
      
      





rentomatic/use_case/storageroom_use_cases.py



の新しいバージョンは次のようになりました。







 from rentomatic.shared import response_object as ro class StorageRoomListUseCase(object): def __init__(self, repo): self.repo = repo def execute(self, request_object): storage_rooms = self.repo.list() return ro.ResponseSuccess (storage_rooms)
      
      





このクリーンなアーキテクチャで得られるものを見てみましょう。 JSONでシリアル化可能で、システムの他の部分から完全に独立した非常に軽量なモデルがあります。 コードには、このAPIによって提供されるストレージがすべてのモデルを取得し、構造化オブジェクト内に返すスクリプトも含まれています。







確かに、まだすべてが実装されているわけではありません。 たとえば、否定的な応答や検証済みの着信要求オブジェクトはありません。







モデルのリストを取得するために使用されるフィルターを表すfilters



パラメーターをfilters



ように現在のスクリプトを変更して、これらの省略を修正してみましょう。 このパラメーターを渡すとエラーが発生する可能性があるため、着信要求オブジェクトのチェックを実装する必要があります







リクエストと検証



Gitタグ: Step07







クエリのfilters



パラメーターを追加します。 このパラメーターにより、呼び出し元は、それぞれに名前と値を指定することでさまざまなフィルターを追加できます(たとえば、 { 'price_lt': 100}



を使用すると、価格が100未満のすべての結果を取得できます)。







編集を開始する最初の場所はテストです。 tests/use_cases/test_storageroom_list_request_objects.py



の新しいバージョンは次のようにtests/use_cases/test_storageroom_list_request_objects.py











 import pytest from rentomatic.use_cases import request_objects as ro def test_valid_request_object_cannot_be_used(): with pytest.raises(NotImplementedError): ro.ValidRequestObject.from_dict({}) def test_build_storageroom_list_request_object_without_parameters(): req = ro.StorageRoomListRequestObject() assert req.filters is None assert bool(req) is True def test_build_file_list_request_object_from_empty_dict(): req = ro.StorageRoomListRequestObject.from_dict({}) assert req.filters is None assert bool(req) is True def test_build_storageroom_list_request_object_with_empty_filters(): req = ro.StorageRoomListRequestObject(filters={}) assert req.filters == {} assert bool(req) is True def test_build_storageroom_list_request_object_from_dict_with_empty_filters(): req = ro.StorageRoomListRequestObject.from_dict({'filters': {}}) assert req.filters == {} assert bool(req) is True def test_build_storageroom_list_request_object_with_filters(): req = ro.StorageRoomListRequestObject(filters={'a': 1, 'b': 2}) assert req.filters == {'a': 1, 'b': 2} assert bool(req) is True def test_build_storageroom_list_request_object_from_dict_with_filters(): req = ro.StorageRoomListRequestObject.from_dict({'filters': {'a': 1, 'b': 2}}) assert req.filters == {'a': 1, 'b': 2} assert bool(req) is True def test_build_storageroom_list_request_object_from_dict_with_invalid_filters(): req = ro.StorageRoomListRequestObject.from_dict({'filters': 5}) assert req.has_errors() assert req.errors[0]['parameter'] == 'filters' assert bool(req) is False
      
      





最初の2つのテストでassert req.filters is None



であることを確認assert req.filters is None



、さらに5つのテストを追加して、フィルターをassert req.filters is None



できるかどうかを確認し、無効なフィルターパラメーターを持つオブジェクトの動作を確認します。







テストに合格するには、 StorageRoomListRequestObject



クラスを変更する必要があります。 当然、思いつくことができる多くの可能な解決策があり、私はあなた自身のものを見つけようとすることをお勧めします。 また、私が通常自分で使用するソリューションについても説明します。 ファイルrentomatic/use_cases/request_object.py



は次のようになります







 import collections class InvalidRequestObject(object): def __init__(self): self.errors = [] def add_error(self, parameter, message): self.errors.append({'parameter': parameter, 'message': message}) def has_errors(self): return len(self.errors) > 0 def __nonzero__(self): return False __bool__ = __nonzero__ class ValidRequestObject(object): @classmethod def from_dict(cls, adict): raise NotImplementedError def __nonzero__(self): return True __bool__ = __nonzero__ class StorageRoomListRequestObject(ValidRequestObject): def __init__(self, filters=None): self.filters = filters @classmethod def from_dict(cls, adict): invalid_req = InvalidRequestObject() if 'filters' in adict and not isinstance(adict['filters'], collections.Mapping): invalid_req.add_error('filters', 'Is not iterable') if invalid_req.has_errors(): return invalid_req return StorageRoomListRequestObject(filters=adict.get('filters', None))
      
      





これらのコード変更について説明させてください。







最初に、2つのヘルパーオブジェクトValidRequestObject



InvalidRequestObject



が導入されInvalidRequestObject



。 不正なリクエストには検証エラーが含まれる必要があるため、両者は異なりますが、同時に両方をブール値に変換する必要があります。







次に、 StorageRoomListRequestObject



は、作成時にオプションのfilters



パラメーターを受け入れます。 __init __ ()



メソッドでは、検証が行われません。これは、内部メソッドであり、パラメーターが既に確認された後に呼び出されるためです。







その結果、 from_dict()



メソッドはfilters



パラメータをチェックします。 抽象collections.Mapping



クラスを使用して、入力パラメーターが辞書であり、 InvalidRequestObject



またはValidRequestObject



インスタンスValidRequestObject



れることを確認します。



正しいまたは誤った要求の存在を報告できるようになったため、スクリプト内の誤った要求またはエラーに対する応答を管理するために、新しいタイプの応答を導入する必要があります。







回答と失敗



Gitタグ: Step08







スクリプトでエラーが発生するとどうなりますか? それらで多数のエラーが発生する可能性があります。前のセクションで説明した検証エラーだけでなく、ビジネスエラーやストレージレイヤーからのエラーもあります。 エラーが何であれ、スクリプトは常に既知の構造(答え)を持つオブジェクトを返す必要があるため、さまざまなタイプの障害を適切にサポートする新しいオブジェクトが必要です。







クエリと同様に、そのようなオブジェクトを表現するための単一の真の方法はなく、次のコードは可能な解決策の1つにすぎません。







最初に行うことは、 tests/shared/test_response_object.py



を展開し、失敗したケースのテストを追加することです。







 import pytest from rentomatic.shared import response_object as res from rentomatic.use_cases import request_objects as req @pytest.fixture def response_value(): return {'key': ['value1', 'value2']} @pytest.fixture def response_type(): return 'ResponseError' @pytest.fixture def response_message(): return 'This is a response error'
      
      





これは、以下のテストで使用するpytest



基づく定型コードです。







 def test_response_success_is_true(response_value): assert bool(res.ResponseSuccess(response_value)) is True def test_response_failure_is_false(response_type, response_message): assert bool(res.ResponseFailure(response_type, response_message)) is False
      
      





ブール値に変換するときに、以前のResponseSuccess



と新しいResponseFailure



が一貫して動作することを確認する2つの基本的なテスト。







 def test_response_success_contains_value(response_value): response = res.ResponseSuccess(response_value) assert response.value == response_value
      
      





ResponseSuccess



オブジェクトには、 value



属性に呼び出しの結果が含まれています。







 def test_response_failure_has_type_and_message(response_type, response_message): response = res.ResponseFailure(response_type, response_message) assert response.type == response_type assert response.message == response_message def test_response_failure_contains_value(response_type, response_message): response = res.ResponseFailure(response_type, response_message) assert response.value == {'type': response_type, 'message': response_message}
      
      





これらの2つのテストにより、 ResponseFailure



オブジェクトが成功時と同じインターフェイスを提供し、オブジェクトにtype



message



パラメーターがあることが確認されます。







 def test_response_failure_initialization_with_exception(): response = res.ResponseFailure(response_type, Exception('Just an error message')) assert bool(response) is False assert response.type == response_type assert response.message == "Exception: Just an error message" def test_response_failure_from_invalid_request_object(): response = res.ResponseFailure.build_from_invalid_request_object(req.InvalidRequestObject()) assert bool(response) is False def test_response_failure_from_invalid_request_object_with_errors(): request_object = req.InvalidRequestObject() request_object.add_error('path', 'Is mandatory') request_object.add_error('path', "can't be blank") response = res.ResponseFailure.build_from_invalid_request_object(request_object) assert bool(response) is False assert response.type == res.ResponseFailure.PARAMETERS_ERROR assert response.message == "path: Is mandatory\npath: can't be blank"
      
      





スクリプトで発生する可能性のあるPython例外からの応答を作成する必要がある場合があるため、 ResponseFailure



オブジェクトを例外で初期化できることを確認します。







最後に、無効なリクエストからのレスポンスの初期化を自動化するbuild_from_invalid_request_object()



メソッドのテストがあります。 要求にエラーが含まれる場合(要求自体をチェックすることを忘れないでください)、応答メッセージでそれらを渡す必要があります。







最後のテストでは、クラス属性を使用してエラーを分類します。 ResponseFailure



クラスには、スクリプトの実行中に発生する可能性のある3つの事前定義済みエラーRESOURCE_ERROR



PARAMETERS_ERROR



およびSYSTEM_ERROR



ます。 この分離により、APIを介して外部システムを操作するときに発生する可能性のあるさまざまなタイプのエラーをカバーしようとします。 RESOURCE_ERROR



は、リポジトリに含まれるリソースに関連するエラーが含まれます。たとえば、一意の識別子でレコードが見つからない場合などです。 PARAMETERS_ERROR



は、クエリパラメータが正しくないか見つからない場合に発生するエラーを示します。 SYSTEM_ERROR



は、データベースからデータを取得する際のファイルシステムの誤動作やネットワーク接続エラーなど、オペレーティングシステムレベルでベースシステムで発生するエラーを対象としています。







このスクリプトは、Pythonコードで発生するさまざまなエラーとのやり取りを担当し、それらを、このエラーの説明が記載された3つのタイプのメッセージのいずれかに変換します。







テストを正常に実行できるようにするResponseFailure



クラスを作成しましょう。 rentomatic/shared/response_object.py



作成します







 class ResponseFailure(object): RESOURCE_ERROR = 'RESOURCE_ERROR' PARAMETERS_ERROR = 'PARAMETERS_ERROR' SYSTEM_ERROR = 'SYSTEM_ERROR' def __init__(self, type_, message): self.type = type_ self.message = self._format_message(message) def _format_message(self, msg): if isinstance(msg, Exception): return "{}: {}".format(msg.__class__.__name__, "{}".format(msg)) return msg
      
      





_format_message()



メソッドを使用して、クラスが文字列メッセージとPython例外の両方を受け入れることができるようにします。これは、未知または関心のない例外を引き起こす可能性のある外部ライブラリを操作するときに非常に便利です。







 @property def value(self): return {'type': self.type, 'message': self.message}
      
      





このプロパティは、辞書であるvalue



属性を提供することにより、クラスとResponseSuccess



APIとの一貫性をResponseSuccess



ます。







 def __bool__(self): return False @classmethod def build_from_invalid_request_object(cls, invalid_request_object): message = "\n".join(["{}: {}".format(err['parameter'], err['message']) for err in invalid_request_object.errors]) return cls(cls.PARAMETERS_ERROR, message)
      
      





上記で説明したように、 PARAMETERS_ERROR



タイプは、送信されたパラメーターの誤ったセットで発生するすべてのエラーをカバーします。つまり、一部のパラメーターはエラーを含むか、省略されます。







エラーに対する回答を作成する必要があることが多いため、ヘルパーメソッドがあると便利です。 tests/shared/test_response_object.py



ビルダー関数の3つのテストを追加しtests/shared/test_response_object.py











 def test_response_failure_build_resource_error(): response = res.ResponseFailure.build_resource_error("test message") assert bool(response) is False assert response.type == res.ResponseFailure.RESOURCE_ERROR assert response.message == "test message" def test_response_failure_build_parameters_error(): response = res.ResponseFailure.build_parameters_error("test message") assert bool(response) is False assert response.type == res.ResponseFailure.PARAMETERS_ERROR assert response.message == "test message" def test_response_failure_build_system_error(): response = res.ResponseFailure.build_system_error("test message") assert bool(response) is False assert response.type == res.ResponseFailure.SYSTEM_ERROR assert response.message == "test message"
      
      





クラスに適切なメソッドを追加し、 build_parameters_error()



メソッドに新しいbuild_parameters_error()



メソッドの使用を追加しました。 rentomatic/shared/response_object.py



ファイルにこのコードが含まれるようになりました







  @classmethod def build_resource_error(cls, message=None): return cls(cls.RESOURCE_ERROR, message) @classmethod def build_system_error(cls, message=None): return cls(cls.SYSTEM_ERROR, message) @classmethod def build_parameters_error(cls, message=None): return cls(cls.PARAMETERS_ERROR, message) @classmethod def build_from_invalid_request_object(cls, invalid_request_object): message = "\n".join(["{}: {}".format(err['parameter'], err['message']) for err in invalid_request_object.errors]) return cls.build_parameters_error(message)
      
      





パート4に続きます。








All Articles