Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Django & Clean Architecture

Django & Clean Architecture

Presentation I did for Python Barcelona Meetup explaining how we mixed Django and Clean Architecture on 21 Buttons Backend.

Jairo Vadillo

January 17, 2019
Tweet

Other Decks in Programming

Transcript

  1. 21 Buttons is the first fashion social-commerce app in the

    world AD +22M Monthly visits +8M App download in 6 countries +25K Daily posts +3500 Influencers +50k Daily products tagged +2000 Brands tagged
  2. Django Django Django Django Django Django Django Django Django Django

    Django Django Django Django Django Django Django Django Django Django Django
  3. ✨ ENTITIES Entities encapsulate Enterprise wide business rules Entities vs.

    Django models @dataclass class User: id: str name: str email: str
  4. ✨ USE CASES Contains application specific business rules Responsibilities: Communicate

    with repositories Manage permissions Trigger side effects Handle or raise exceptions
  5. ✨ USE CASES class CreateUserUseCase: def __init__(user_repo, user_service): self._user_repo =

    user_repo self._user_service = user_service def execute(user_data: UserData) -> User: # do stuff user_repo.create(user_dto) # do more stuff
  6. ✨ USE CASES @patch('users.repositories.UsersRepo') @patch('users.events.EventManager') @patch('users.tasks.UserTasks') @patch('users.services.UsersService') def test_repo_called(): ...

    @pytest.mark.unit class TestCreateUserUseCase: def test_repo_called(): repo = Mock() event_manager = Mock() service = Mock() tasks = Mock() use_case = CreateUserUseCase(...)
  7. ✨ USE CASES # use_cases.py class CreateUserUseCase: ... def execute(user_dto)

    -> User: ... event_manager.trigger(UserCreatedEvent(user)) # users/events.py @dataclass class UserCreatedEvent: user: User
  8. ✨ USE CASES # download_registers/apps.py from django.apps import AppConfig class

    DownloadRegistersConfig(AppConfig): name = 'download_registers' verbose_name = "Download Registers" def ready(self): from .subscribers import compute_rewards from users.events import UserCreatedEvent EventManager.subscribe(compute_rewards, UserCreatedEvent) # download_registers/subscribers.py def compute_rewards(event: UserCreatedEvent): # run task to compute rewards
  9. ✨ FACTORIES & CONF Factories? In python? WHY? from .use_cases

    import CreateUserUseCase from .repositories import UsersRepo def build_create_user_use_case() -> CreateUserUseCase: return CreateUserUseCase(UsersRepo(db_connection))
  10. ✨ REPOSITORIES Only layer that knows about data sources Responsibilities:

    Guarantee the correct communication with data sources
  11. ✨ class UserRepository: def __init__(db_repo, elastic_repo): self._db_repo = db_repo self._elastic_repo

    = elastic_repo def get(self, id): return self._db_repo.get(id) def search(self, query): return self._elastic_repo.search(query) REPOSITORIES
  12. ✨ class UserDBRepository: def get(self, id): user = User.objects.get(id=id) #

    parse user to entity return user class UserElasticRepository: def search(self, query): users = elasticsearch.query() # parse elastic users to entities return users REPOSITORIES
  13. ✨ @pytest.mark.integration @pytest.mark.django_db class TestUserDBRepository: def test_get_user(): # create user

    to db (using fixtures for example) u = UserDBRepository().get(user_id) assert u == <your user> REPOSITORIES
  14. # dtos.py @dataclass class CreateUserRequestDTO: username: str email: str #

    validators.py from marshmallow import Schema, fields, validate, post_load class CreateUserSerializer(Schema): username = fields.String(required=True) email = fields.Email(required=True) @post_load def build_create_user_dto(self, data): return CreateUserRequestDTO(**data) ✨ VIEWS
  15. ✨ VIEWS class CreateUserView(APIView): def post(self, request): user_dto, errors =

    CreateUserSerializer().load(request.data) if errors: return Response(errors, status=status.HTTP_400_BAD_REQUEST) use_case = build_create_user_use_case() try: user = use_case.execute(user_dto) except (UsernameAlreadyExistsError) as e: return Response(str(e), status=status.HTTP_422_UNPROCESSABLE_ENTITY) except PermissionsInsuficientException as e: return Response(str(e), status=status.HTTP_403_FORBIDDEN) else: return Response(UserSerializer().dump(user).data, status=status.HTTP_201_CREATED)
  16. ✨ VIEWS class CreateUserView(APIView): def post(self, request): user_dto, errors =

    CreateUserSerializer().load(request.data) if errors: return Response(errors, status=status.HTTP_400_BAD_REQUEST) use_case = build_create_user_use_case() try: user = use_case.execute(user_dto) except (UsernameAlreadyExistsError) as e: return Response(str(e), status=status.HTTP_422_UNPROCESSABLE_ENTITY) except PermissionsInsuficientException as e: return Response(str(e), status=status.HTTP_403_FORBIDDEN) else: return Response(UserSerializer().dump(user).data, status=status.HTTP_201_CREATED) VALIDATE INPUT
  17. ✨ CONTROLLERS class CreateUserView(APIView): def post(self, request): user_dto, errors =

    CreateUserSerializer().load(request.data) if errors: return Response(errors, status=status.HTTP_400_BAD_REQUEST) use_case = build_create_user_use_case() try: user = use_case.execute(user_dto) except (UsernameAlreadyExistsError) as e: return Response(str(e), status=status.HTTP_422_UNPROCESSABLE_ENTITY) except PermissionsInsuficientException as e: return Response(str(e), status=status.HTTP_403_FORBIDDEN) else: return Response(UserSerializer().dump(user).data, status=status.HTTP_201_CREATED) CALL USE CASE VALIDATE INPUT VIEWS
  18. ✨ CONTROLLERS class CreateUserView(APIView): def post(self, request): user_dto, errors =

    CreateUserSerializer().load(request.data) if errors: return Response(errors, status=status.HTTP_400_BAD_REQUEST) use_case = build_create_user_use_case() try: user = use_case.execute(user_dto) except (UsernameAlreadyExistsError) as e: return Response(str(e), status=status.HTTP_422_UNPROCESSABLE_ENTITY) except PermissionsInsuficientException as e: return Response(str(e), status=status.HTTP_403_FORBIDDEN) else: return Response(UserSerializer().dump(user).data, status=status.HTTP_201_CREATED) HANDLE EXCEPTIONS CALL USE CASE VALIDATE INPUT VIEWS
  19. ✨ CONTROLLERS class CreateUserView(APIView): def post(self, request): user_dto, errors =

    CreateUserSerializer().load(request.data) if errors: return Response(errors, status=status.HTTP_400_BAD_REQUEST) use_case = build_create_user_use_case() try: user = use_case.execute(user_dto) except (UsernameAlreadyExistsError) as e: return Response(str(e), status=status.HTTP_422_UNPROCESSABLE_ENTITY) except PermissionsInsuficientException as e: return Response(str(e), status=status.HTTP_403_FORBIDDEN) else: return Response(UserSerializer().dump(user).data, status=status.HTTP_201_CREATED) HANDLE EXCEPTIONS CALL USE CASE VALIDATE INPUT RETURN SERIALISED DATA VIEWS
  20. ✨ CONTROLLERS @pytest.mark.unit class TestCreateUserView: @patch('users.views.build_create_user_use_case') def test_patch_failed_404(self, use_case_factory): use_case

    = MagicMock() use_case_factory.return_value = use_case use_case.execute.side_effect = BadContentUserDoesNotExist() request = MagicMock() response = BadContentUserDetailView().patch(request, user_id=1) assert response.status_code == 404 VIEWS