📜 ⬆️ ⬇️

Pure Python Architecture: A Walkthrough. Part 2



Domain Models


Git tag: Step02

Let's start with a simple definition of the StorageRoom model. As mentioned earlier, models in pure architecture are very light, at least easier than their ORM counterparts in frameworks.

Once we follow the TDD methodology, the first thing we write is tests. Create the file tests/domain/test_storageroom.py and place this code inside it:

 import uuid from rentomatic.domain.storageroom import StorageRoom def test_storageroom_model_init(): code = uuid.uuid4() storageroom = StorageRoom(code, size=200, price=10, longitude='-0.09998975', latitude='51.75436293') assert storageroom.code == code assert storageroom.size == 200 assert storageroom.price == 10 assert storageroom.longitude == -0.09998975 assert storageroom.latitude == 51.75436293 def test_storageroom_model_from_dict(): code = uuid.uuid4() storageroom = StorageRoom.from_dict( { 'code': code, 'size': 200, 'price': 10, 'longitude': '-0.09998975', 'latitude': '51.75436293' } ) assert storageroom.code == code assert storageroom.size == 200 assert storageroom.price == 10 assert storageroom.longitude == -0.09998975 assert storageroom.latitude == 51.75436293 

These two tests ensure that our model can be initialized with the correct values ​​passed to it or using a dictionary. In the first case, you must specify all the parameters of the model. Later it will be possible to make some of them optional by pre-writing the necessary tests.
')
In the meantime, let's write the StorageRoom class by placing it in the rentomatic/domain/storageroom.py . Do not forget to create a file __init__.py in each subdirectory of the project, which Python should perceive as modules.

 from rentomatic.shared.domain_model import DomainModel class StorageRoom(object): def __init__(self, code, size, price, latitude, longitude): self.code = code self.size = size self.price = price self.latitude = float(latitude) self.longitude = float(longitude) @classmethod def from_dict(cls, adict): room = StorageRoom( code=adict['code'], size=adict['size'], price=adict['price'], latitude=adict['latitude'], longitude=adict['longitude'], ) return room DomainModel.register(StorageRoom) 

The model is very simple and requires no explanation. One of the advantages of clean architecture is that each layer contains small pieces of code that, when isolated, should perform simple tasks. In our case, the model provides an API for initializing and storing information within the class.

The from_dict method from_dict useful when creating a model from data coming from another layer (such as a database layer or from a query string in a REST layer).

It may be tempting to try to simplify the from_dict function by abstracting and presenting it as a method of the Model class. And considering that a certain level of abstraction and generalization is possible and necessary, and the initialization of models can interact with various other scenarios, it is better to implement it directly in the class itself.

The DomainModel abstract base class is an easy way to classify a model for future scenarios, such as checking for class membership of a model in a system. For more information on using Abstract Base Classes in Python, I advise you to read this post .

Serializers


Git tag: Step03

If we want to return our model as a result of an API call, then it will need to be serialized. The typical serialization format is JSON, since it is a widespread standard used for web APIs. Serializer is not part of the model. This is an external special class that receives an instance of the model and translates its structure and values ​​into some representation.

To test the JSON serialization of our StorageRoom class StorageRoom place the following code in the tests/serializers/test_storageroom_serializer.py file

 import datetime import pytest import json from rentomatic.serializers import storageroom_serializer as srs from rentomatic.domain.storageroom import StorageRoom def test_serialize_domain_storageroom(): room = StorageRoom('f853578c-fc0f-4e65-81b8-566c5dffa35a', size=200, price=10, longitude='-0.09998975', latitude='51.75436293') expected_json = """ { "code": "f853578c-fc0f-4e65-81b8-566c5dffa35a", "size": 200, "price": 10, "longitude": -0.09998975, "latitude": 51.75436293 } """ assert json.loads(json.dumps(room, cls=srs.StorageRoomEncoder)) == json.loads(expected_json) def test_serialize_domain_storageruum_wrong_type(): with pytest.raises(TypeError): json.dumps(datetime.datetime.now(), cls=srs.StorageRoomEncoder) 

Place the code that passes the test in the rentomatic/serializers/storageroom_serializer.py file:

 import json class StorageRoomEncoder(json.JSONEncoder): def default(self, o): try: to_serialize = { 'code': o.code, 'size': o.size, 'price': o.price, "latitude": o.latitude, "longitude": o.longitude, } return to_serialize except AttributeError: return super().default(o) 

Providing a class inherited from JSON.JSONEncoder , we use json.dumps(room, cls = StorageRoomEncoder) to serialize the model.

We may notice some repetition in the code. This is a minus of pure architecture, which is annoying. Since we want to isolate the layers as much as possible and create lightweight classes, we end up repeating some actions. For example, the serialization code that assigns the attributes from StorageRoom to JSON attributes is similar to what we use to create an object from a dictionary. Not the same, but the similarity of these two functions is present.

Scripts (part 1)


Git tag: Step04

It's time to implement the real business logic of our application, which will be accessible from the outside. Scenarios are the place where we implement the classes that request the repository, apply business rules, logic, transform the data as we please, and return the result.

Given these requirements, let's begin to consistently build the script . The simplest scenario we can create is one that retrieves all storage space from the storage and returns it. Please note that we have not yet implemented the storage layer, so in our tests we will be wet (replace with fiction).

Here is the basis for a simple test script that lists all storage spaces. Place this code in the 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 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) result = storageroom_list_use_case.execute() repo.list.assert_called_with() assert result == domain_storagerooms 

The test is simple. First, we changed the repository to provide the list() method, which returns a list of the models previously created above. Then we initialize the script with the repository and execute it, memorizing the result. The first thing we check is that the storage method was called without any parameter, and the second is the correctness of the result.

But the implementation of the script that passes the test. Place the code in the file rentomatic/use_cases/storageroom_use_case.py

 class StorageRoomListUseCase(object): def __init__(self, repo): self.repo = repo def execute(self): return self.repo.list() 

However, with such an implementation of the scenario, we will soon encounter a problem. Firstly, we do not have a standard way to transfer call parameters, which means that we do not have a standard way to check their correctness. The next problem is that we are missing the standard way of returning the results of a call, and therefore we cannot find out if the call was successful or not, and if not, for what reason. The same problem with the wrong parameters discussed in the previous paragraph.

Thus, we want to introduce some structures to wrap the input and output of our scripts . These structures are called request and response objects .

Requests and Answers


Git tag: Step05

Requests and responses are an important part of pure architecture. They move the call parameters, input data and call results between the script layer and the external environment.

Requests are created based on incoming API calls, so that they will encounter such things as incorrect values, missing parameters, incorrect format, etc. Answers , on the other hand, must contain the results of the API calls, including those that must submit errors and give detailed information about what happened.

You have the right to use any implementation of requests and responses , pure architecture says nothing about it. The decision on how to pack and present the data rests entirely with you.

In the meantime, we just need a StorageRoomListRequestObject , which can be initialized without parameters, so let's create the file tests/use_cases/test_storageroom_list_request_objects.py and put a test for this object in it.

 from rentomatic.use_cases import request_objects as ro def test_build_storageroom_list_request_object_without_parameters(): req = ro.StorageRoomListRequestObject() assert bool(req) is True def test_build_file_list_request_object_from_empty_dict(): req = ro.StorageRoomListRequestObject.from_dict({}) assert bool(req) is True 

At the moment, the request object is empty, but it will come in handy to us as soon as we have the parameters for the script that produces the list of objects. The code for the StorageRoomListRequestObject class is in the rentomatic/use_cases/request_objects.py file and looks like this:

 class StorageRoomListRequestObject(object): @classmethod def from_dict(cls, adict): return StorageRoomListRequestObject() def __nonzero__(self): return True 

The request is also quite simple, since at the moment we need only a successful answer. Unlike the query , the answer is not associated with any particular scenario , so the test file can be called tests/shared/test_response_object.py

 from rentomatic.shared import response_object as ro def test_response_success_is_true(): assert bool(ro.ResponseSuccess()) is True 

and the actual response object is in the rentomatic/shared/response_object.py file

 class ResponseSuccess(object): def __init__(self, value=None): self.value = value def __nonzero__(self): return True __bool__ = __nonzero__ 

To be continued in Part 3 .

Source: https://habr.com/ru/post/319202/


All Articles