🐕

Pythonでクリーンアーキテクチャ実装

2024/12/18に公開

クリーンアーキテクチャとは

端的に言うと、アプリケーションを作成する際に自分たちの(アプリケーション作成側)のロジックを中心におく事を目的としたアーキテクチャのことです。

ではここでいうロジックとは何でしょうか?

ロジックとは...

ロジックについて話す前に用語を説明しましょう。

用語 説明
Controller requestを受け取りUseCaseへ渡すところ。requestからユーザーの入力情報を展開してInputDtoという形式でUseCase層へデータを渡す役割 Presentation層
Infrastructure層から(View側から)の要望に応える領域
Presenter UseCase層から渡されたOutputDtoを処理し、ViewModelという形式に変換しView側へ渡す処理をする Presentation層
ViewModel View側がどのような形式で受け取るのかなど、View側に合わせた形式 Presentation層
InputDto(InputData) Controller -> UseCaseへデータを渡す際のデータ形式 Application層
ビジネスのロジック(後で説明)を記述する領域。このアプリではこういったリクエストがあってそれをどう処理してどう返すかを決定する。Entityなどアプリで使用されるオブジェクトを通してリクエストに応える
InputBoundary UseCaseのインターフェース(Controllerがこのインターフェースを使用) Application層
OutputBoundary Presenterのインターフェース(UseCaseがpresenterのインターフェースを使用) Application層
Repository(DataAccessInterface) DataAccessが実装しているインターフェース(UseCaseがこのインターフェースを使用している) Application層
Entity ロジック(後で説明)の中で、Application層が使用する形式。これを中心として処理を書いて行く Domain層
アプリケーションで使用するオブジェクト(Enitity)を定義する層
DataAccess データベースへアクセスし生のデータを、EntityなどApplication層が扱いやすいオブジェクトへ変換してUse Caseへ渡す役割。Repositoryを実装 Infrastructure層
外部とのやりとりをする際に最初に通過する領域
DataBase DBを使用するところ。sqlを書いたり、ORMを使用してデータを扱うような役割 Infrastructure層
View いわゆるフロント部分である。json形式などでデータを受け取ってブラウザやスマホ画面に表示させるところ Infrastructure層

Entityについて(Domain層)

他のサイトではビジネスロジックなどと抽象的に述べられているが、ここでは具体例を出して説明していきます。

例えば、オンライン書店の注文処理システムを考えてみます。

本を注文する際にまずコード上で"本"というオブジェクトが必要なのでまず本オブジェクトを作ります

entity.py
import datetime
class Author:
    def __init__(self, name: str, birth: datetime) -> None:
        self.name = name
        self.birth = birth

class Book:
    def __init__(self, title: str, price: int, author: Author) -> None:
        self.title = title
        self.price = price
        self.author = author

ここでAuthorがあるのは、アプリケーションを開発する前にある程度、"Bookがあるならその著者もいるか"などと決めておいて作るためここでもAuthorというクラスを定義しておきました。

次にこれらのアプリケーションで使用するオブジェクトに機能を持たせましょう。

entity.py
import datetime
from typing import List, Dict, Any


class Author:
    def __init__(self, name: str, birth: datetime, is_active: bool) -> None:
        self.name = name
        self.birth = birth
        self.is_active = is_active

    def deactivate(self) -> None:
        self.is_active = False

    def activate(self) -> None:
        self.active = True

    def introduce(self) -> Dict[str, Any]:
        if not self.is_active:
            return {}
        return {
            "name": self.name,
            "birth": self.birth,
        }

class Book:
    def __init__(self, title: str, price: int, author: Author) -> None:
        self.title = title
        self.price = price
        self.author = author


    def set_price(self, price) -> None:
        if price <= 0:
            raise ValueError("price must be positive")
        self.price = price

    def info(self) -> Dict[str, Any]:
        return {
            "title": self.title,
            "price": self.price,
            "author": self.author.introduce()
        }

と、このように機能を付け加えました。実際、アプリケーション作成中に度々機能は追加していくものなので、必ずしも機能を予め全て考えておく必要はありません。しかし、アプリケーション作成前にAという機能を作るにはこのclassにこのメソッドは持たせておく必要があるなど予め考えておくほうが良いでしょう。

ここで重要なのは何か機能やオブジェクトをアプリケーションに組み込みたい時はこのロジックのもとになるオブジェクトから作成し始める必要があるという点出る。

クリーンアーキテクチャの文脈において、このようなオブジェクトをEntity(実態)と呼びます。

UseCaseについて(Application層、こいつがロジック)

次にユースケースとは何か?そもそもユースケースとは日本語では利用用途という意味でsy。ならば、アプリケーションにおけるユースケースとは”そのアプリケーションにおける利用用途”と解釈して間違い無いでしょう。先ほどのオンライン書店の注文処理システムの利用用途について具体的に考えてみましょう。一つ良い例があります。ある著者が書いた本の一覧を取得するケースです。

get_book_use_case.py
from typing import List, Dict, Any

from .i_get_book_usecase import IGetBooksUseCase
from .book_repository import BookRepository
from .author_repository import AuthorRepository

class GetBooksDetailUseCase(IGetBooksUseCase):
    def __init__(self,
            book_repository: BookRepository, # interface
            author_repository: AuthorRepository, # interface
            presenter: IGetBooksPresenter # interface
        ) -> None:
        self.book_repository = book_repository
        self.author_repository = author_repository
        self.presenter = presenter

    def handle(self, input_dto: GetBooksInputDto) -> Response:

        try:
            author = self.author_repository.get_author_by_id(author_id=input_dto.author_id)
            books: List[Book] = self.book_repository.get_by_author(author=author)
            output_dto: List[GetBooksOutputDto] = [GetBooksOutputDto(**book.info()) for book in books]
            return self.presenter.present(output_dto)
        except Exception as e:
            return self.presenter.present_error(e)

このようにUseCaseを記述しているが、book_repositoryとpresenterはインタフェースを参照しているので実態はまだありません。ただ、authorやbooksなどのオブジェクトがself.repository.get_by_nameメソッドから得られるということがUseCaseから分かるだけです。presenterも同様にインターフェースです。

言葉で語るより実際のコードをみてインターフェースを理解していきます

i_get_book_use_case.py
import abc
from .get_books_input_dto import GetBooksInputDto
from .get_books_output_dto import GetBooksOutputDto
from typing import List, Dict, Any

class IGetBooksUseCase(metaclass=abc.ABCMeta):
    @abstractmethod
    def handle(self, input_dto: GetBooksInputDto) -> MockResponse:
        raise NotImplementedError
i_get_books_presenter.py
from typing import List, Dict, Any
from abc import ABCMeta, abstractmethod

from .get_books_output_dto import GetBooksOutputDto

class IGetBooksPresenter(metaclass=abc.ABCMeta):
    @abstractmethod
    def present(self, output_dto: List[GetBooksOutputDto]) -> MockResponse:
        raise NotImplementedError

    @abstractmethod
    def present_error(self, error: Exception) -> MockResponse:
        raise NotImplementedError
book_repository.py
from .entity import Author, Book
from abc import ABCMeta, abstractmethod

class BookRepository(metaclass=abc.ABCMeta):
    @abstractmethod
    def get_by_author(self, author: Author) -> Book:
        raise NotImplementedError

class AuthorRepository(metaclass=abc.ABCMeta):
    @abstractmethod
    def get_by_id(self, author_id: str) -> Author:
        raise NotImplementedError

Dto(DataTransferObject)の実装は以下の通りです

get_books_dto.py
from typing import List, Dict, Any
class GetBooksInputDto:
    def __init__(self, author_id: str):
        self.author_id = author_id

class GetBooksOutputDto:
    def __init__(self, title: str, price: int, author: Dict[str, Any]):
        self.title = title
        self.price = price
        self.author = author

    def to_json(self):
        return {
            "title": self.title,
            "price": self.price,
            "author": self.author
        }

このようにインターフェースを定義することで、UseCase層ではインターフェースだけを参照してロジックを記述することができます。今回の例では、著者が書いた本の一覧を取得するとき

  1. ユーザーからinput_dtoとして、著者に関する情報(今回はid)を取得する
  2. 著者idから著者Entity(Author)のインスタンスを取得する(autor_repositoryのインターフェースからAuthorオブジェクトが取得できることが分かる)
  3. そのEntityをself.book_repositoryに渡す事でList[Book]という形で本の一覧を取得する
  4. List[Book]をpresenterが受け取るオブジェクト(output_dto)へ変換してpresenterに渡す

これが著者情報から本の一覧を取得するというユースケースに対応するロジックです。

ロジックとしては、UseCase層で入力形式としてGetBooksInputDtoを受け取り、presenter(presentation層にある)への入力形式としてGetBooksOutputDtoを渡すという流れになっています。

DataAccessと DB(Infra層)

先ほどロジックを形作るApplication層を実装しました。このままではまだ抽象的概念を記述したにすぎません。なぜ抽象的かというと、repositoryとpresenterはインターフェースであるが故に、あくまで、例えばAというオブジェクトがinputとして入力された時に、Bというオブジェクトを返却するというルールがあると仮定しているすぎないからです。実際にインタフェースであるrepositoryやpresenterに何かinputを入力しても、そもそも返却する仕組みを記述していないのだから何も返却はされません。
言葉だけではわかりづらいので、先ほどのコードを再考してみましょう。

class BookRepository(metaclass=abc.ABCMeta):
	@abstractmethod
	def get_by_author(self, author: Author) -> Book:
            raise NotImplementedError

このBookRepositoryのget_by_authorメソッドには入力としてauthorというEntityを受け取りBookというEntityを返却するとしているが、実際にこのBookRepositoryのインスタンを作ってget_by_authorメソッドを呼び出してもBookオブジェクトは返却されません。これは、ただこのメソッドのinputとoutputの型を定義付けているだけでその仕組み(inputを受け取って、さまざまな処理を行いBookを返却する仕組み)を記述していないからです。ここで、DataAccessの出番というわけです。

DataAccessクラスでは先ほどの中身も何もない抽象的なBookRepositoryの中身を実装するのが目的です。この際DataAccessクラスはBookRepositoryを継承する必要があります。こうすることで、DataAccessクラスに、強制的にBookRepository内で書かれているメソッドの実装を行わせます。実装を行わないとインスタンス化できないというエラーが生じます。このある種の縛りが強力な柔軟性を生みます。

その理由

  1. Application層で記述したGetBooksUseCaseはBookRepositoryのメソッドしか見ていいない -> 逆に言えばこのBookRepository通りのメソッドをDataAccessでしっかり記述していればApplication層からするとその記述の仕方はどうでもよい
  2. DataAccessからすれば、記述の仕方は自由であるのでここはDataAccessクラスが自由に変更できる

ここでも先ほどの例を基に実例を示します

book_access.py
from my_database import my_db
from .entity import Book, Author


class InMemoryDB:
    def __init__(self):
        self.authors = {}  # author_id をキーとする辞書
        self.books = []  # Book のリスト

    def add_author(self, author_id: str, author: Author):
        if author_id in self.authors:
            raise ValueError("Author with this ID already exists.")
        self.authors[author_id] = author

    def get_author_by_id(self, author_id: str) -> Author:
        if author_id not in self.authors:
            raise ValueError("Author not found.")
        return self.authors[author_id]

    def add_book(self, book: Book):
        if not any(author for author in self.authors.values() if author.name == book.author.name):
            raise ValueError("Author for this book does not exist.")
        self.books.append(book)

    def get_books_by_author_name(self, author_name: str) -> List[Book]:
        return [book for book in self.books if book.author.name == author_name]

my_db = InMemoryDB()
your_db = InMemoryDB()

class MyBookDataAccess(BookRepository):
    def __init__(self, db: InMemoryDB):
        self.db = db

    def get_by_author(self, author: Author) -> List[Book]:
        books = self.db.get_books_by_author_name(author_name=author.name)
        if not books:
            raise ValueError(f"No books found for author {author.name}")
        return books

class YourBookDataAccess(BookRepository):
    def __init__(self):
        self.db = your_db

    def get_by_author(self, author: Author) -> Book:
        book_data = self.db.book.get({"name": author.name})
        # ここでデータベースから取得したbook_dataから必要な情報を取り出しBookEntityを作成して返却する
        return Book(title=book_data["title"], price=book_data["price"], author=author)

このようにいずれのBookDataAccessもBookRepositoryを実装しているので使用するデータベースが異なれど、Application層からすると全く問題ないのです。これが柔軟性です。

Application層のUseCaseではインターフェースであるRepositoryを参照しており、そのインターフェースの内部実装を担っているのがInfra層のDataAccessです。図だと、下図にあたる

UseCaseがDataAccessInterface(Repository)を参照しており、DataAccessがRepositoryを実装しています。(下図の黒矢印は参照を意味し、白矢印は実装を意味します。)処理の流れはUseCase-> DataAccessであるが、依存関係はInfra層のDataAccessが Application層のRepositoryに縛られるため、DataAccessがUseCaseに依存している形になる。これを依存関係の逆転という。(難しそうに聞こえるが大したことはやっていません)。このメカニズムがPresentation層とApplication層の境界でも使用されています。

次にPresentation層の説明に行きます

ControllerとPresenter(Presentation層)

ControllerとPresenterの役割について解説していきます。
Controllerはフロントからのrequestを受け取って、リクエストからデータを受け取りユースケースに適したオブジェクトへ変換し、UseCase層へそのオブジェクトを渡す役割があります。ユースーケース層へ渡されるオブジェクトは先ほど定義したInputDtoです。GetBooksという本一覧を取得するユースケースでは、author情報(author_id)をrequestオブジェクトから取得しこれをGetBooksInputDtoというオブジェクトへ変換してGetBooksUseCaseへそのオブジェクトを渡します。この際、requestのbodyまたはheader、urlparamsに今回のユースケースに必要とされる情報があるか?や型があっているか?などvalidationの処理も書きます。
次にPresenterの役割について説明します。これはControllerとは逆で、requestを送信してきたフロントの期待通りの情報をjsonなど適切な形式にしてresponseを返す役割があります。また、errorが生じた際に何を返却するかなどもここで処理します。(400 Bad Requestなのか、404 Not Found、500 Internal Server Erroなど)
実際にコードを見て理解しましょう

BookController.py
from .i_get_books_use_case import IGetBooksUseCase
from .get_books_dto import GetBooksInputDto

class BooksController:
    def __init__(self, get_books_use_case: IGetBooksUseCase) -> None:
        self.get_books_use_case = get_books_use_case

    def get(self, request: Request) -> Response:
        '''
            validation処理など必要に応じて処理
        '''
        input_dto = GetBooksInputDto(**request.data)
        return self.get_books_use_case.handle(input_dto)
get_books_presenter.py
from typing import List

from .i_get_books_presenter import IGetBooksPresenter
from .get_books_output_dto import GetBooksOutputDto

class GetBooksPresenter(IGetBooksPresenter):
    def present(self, output_dto: List[GetBooksOutputDto]) -> Response:
        res_data = output_dto.to_json()
        return Response(res_data, status=200)

    def present_error(self, e: Exception) -> Response:
        return Response(e.message(), status=400)

BooksControllerではgetメソッドが実装されていて、requestを受け取り、requestオブジェクトからdataを取得し、input_dtoに変換しています。これによってUseCase層へデータを渡すことができるようになります。ここでも、Controller層はUseCaseを直接参照しているのではなくInterfaceを通して間接的にUseCase層を参照しています。
PresenterはインターフェースであるIGetBooksPresenterを実装しています。実際に受け取ったOutputDtoをjsonに変換して、Responseオブジェクトに渡せる形式に変換してResponseを返しています。さらにpresent_errorメソッドもインターフェースの縛りによって実装されています(されなければなりません)。これによってエラーが生じた場合はエラーを返却するようになっています。

DI(Dependency Injection)

最後に、今までインターフェースを参照している箇所に、そのインターフェースを実装しているクラスのインスタンスを渡す必要があります。具体的に以下のようなことをしなければなりません。

from abc import ABCMeta, abstractmethod
class InterfaceReorio(metaclass=ABCMeta):
    @abstractmethod
    def speak(self) -> str:
        raise NotImplementedError

class Reorio(InterfaceReorio):
    def speak(self) -> str:
        return "レオリオ:薄汚いクルタ族とかを絶やしてやるぜ"

class Kurapika:
    def __init__(self, reorio: InterfaceReorio) -> None:
        self.reorio = reorio

    def speak(self) -> str:
        return "クラピカ:品性は金で買えないよ、レオリオ。 " + self.reorio.speak()

reorio = Reorio()
kurapika = Kurapika(reorio)
kurapika.speak()

これと同じようなことを今までの実装を全て記述して、DIも行ってみます。

all_in_one.py
import datetime
from typing import List, Dict, Any
import abc
from requests import Response, Request
from abc import ABCMeta, abstractmethod


# モックリクエストとレスポンスを定義 (実際のフレームワークに応じて変更可能)
class MockRequest:
    def __init__(self, data: Dict[str, Any]):
        self.data = data


class MockResponse(Response):
    def __init__(self, json_data: Any, status: int):
        self.json_data = json_data
        self.status_code = status

    def json(self):
        return self.json_data


class Author:
    def __init__(self, name: str, birth: datetime, is_active: bool) -> None:
        self.name = name
        self.birth = birth
        self.is_active = is_active

    def deactivate(self) -> None:
        self.is_active = False

    def activate(self) -> None:
        self.active = True

    def introduce(self) -> Dict[str, Any]:
        if not self.is_active:
            return {}
        return {
            "name": self.name,
            "birth": self.birth,
        }

class Book:
    def __init__(self, title: str, price: int, author: Author) -> None:
        self.title = title
        self.price = price
        self.author = author

    def set_price(self, price) -> None:
        if price <= 0:
            raise ValueError("price must be positive")
        self.price = price

    def info(self) -> Dict[str, Any]:
        return {
            "title": self.title,
            "price": self.price,
            "author": self.author.introduce()
        }

class GetBooksInputDto:
	def __init__(self, author_id: str):
            self.author_id = author_id

class GetBooksOutputDto:
    def __init__(self, title: str, price: int, author: Dict[str, Any]):
        self.title = title
        self.price = price
        self.author = author

    def to_json(self):
        return {
            "title": self.title,
            "price": self.price,
            "author": self.author
        }


class IGetBooksUseCase(metaclass=abc.ABCMeta):
    @abstractmethod
    def handle(self, input_dto: GetBooksInputDto) -> MockResponse:
        raise NotImplementedError


class IGetBooksPresenter(metaclass=abc.ABCMeta):
    @abstractmethod
    def present(self, output_dto: List[GetBooksOutputDto]) -> MockResponse:
        raise NotImplementedError

    @abstractmethod
    def present_error(self, error: Exception) -> MockResponse:
        raise NotImplementedError


class BookRepository(metaclass=abc.ABCMeta):
    @abstractmethod
    def get_by_author(self, author: Author) -> Book:
        raise NotImplementedError

class AuthorRepository(metaclass=abc.ABCMeta):
    @abstractmethod
    def get_by_id(self, author_id: str) -> Author:
        raise NotImplementedError


class GetBooksDetailUseCase(IGetBooksUseCase):
    def __init__(self,
            book_repository: BookRepository, # interface
            author_repository: AuthorRepository, # interface
            presenter: IGetBooksPresenter # interface
        ) -> None:
        self.book_repository = book_repository
        self.author_repository = author_repository
        self.presenter = presenter

    def handle(self, input_dto: GetBooksInputDto) -> Response:

        try:
            author = self.author_repository.get_author_by_id(author_id=input_dto.author_id)
            books: List[Book] = self.book_repository.get_by_author(author=author)
            output_dto: List[GetBooksOutputDto] = [GetBooksOutputDto(**book.info()) for book in books]
            return self.presenter.present(output_dto)
        except Exception as e:
            return self.presenter.present_error(e)

class InMemoryDB:
    def __init__(self) -> None:
        self.authors = {}  # author_id をキーとする辞書
        self.books = []  # Book のリスト

    def add_author(self, author_id: str, author: Author):
        if author_id in self.authors:
            raise ValueError("Author with this ID already exists.")
        self.authors[author_id] = author

    def get_author_by_id(self, author_id: str) -> Author:
        if author_id not in self.authors:
            raise ValueError("Author not found.")
        return self.authors[author_id]

    def add_book(self, book: Book):
        if not any(author for author in self.authors.values() if author.name == book.author.name):
            raise ValueError("Author for this book does not exist.")
        self.books.append(book)

    def get_books_by_author_name(self, author_name: str) -> List[Book]:
        return [book for book in self.books if book.author.name == author_name]

#dbのインスタンス化(実際はfirebaseのfirestoreのdbなどを使用することも)
my_db = InMemoryDB()
your_db = InMemoryDB()

class MyBookDataAccess(BookRepository):
    def __init__(self, db: InMemoryDB):
        self.db = db

    def get_by_author(self, author: Author) -> List[Book]:
        books = self.db.get_books_by_author_name(author_name=author.name)
        if not books:
            raise ValueError(f"No books found for author {author.name}")
        return books

class YourBookDataAccess(BookRepository):
    def __init__(self):
        self.db = your_db

    def get_by_author(self, author: Author) -> Book:
        book_data = self.db.book.get({"name": author.name})
        # ここでデータベースから取得したbook_dataから必要な情報を取り出しBookEntityを作成して返却する
        return Book(title=book_data["title"], price=book_data["price"], author=author)

# Controllerの実装
class BooksController:
    def __init__(self, get_books_use_case: IGetBooksUseCase) -> None:
        self.get_books_use_case = get_books_use_case

    def get(self, request: MockRequest) -> MockResponse:
        '''
            validation処理など必要に応じて処理
        '''
        input_dto = GetBooksInputDto(**request.data)
        return self.get_books_use_case.handle(input_dto)

# Presenter の具体実装
class GetBooksPresenter(IGetBooksPresenter):
    def present(self, output_dto: List[GetBooksOutputDto]) -> Response:
        res_data = [dto.to_json() for dto in output_dto]
        return MockResponse(res_data, status=200)

    def present_error(self, e: Exception) -> Response:
        return MockResponse({"error": str(e)}, status=400)

次はDIを行います。

di.py
from requests.models import Request, Response

# モックリクエストとレスポンスを定義 
class MockRequest:
    def __init__(self, data: Dict[str, Any]):
        self.data = data


class MockResponse(Response):
    def __init__(self, json_data: Any, status: int):
        self.json_data = json_data
        self.status_code = status

    def json(self):
        return self.json_data


# データセットアップ
author_1 = Author(name="Author A", birth=datetime.datetime(1980, 1, 1), is_active=True)
author_2 = Author(name="Author B", birth=datetime.datetime(1990, 1, 1), is_active=True)

book_1 = Book(title="Book 1", price=1000, author=author_1)
book_2 = Book(title="Book 2", price=1500, author=author_1)
book_3 = Book(title="Book 3", price=2000, author=author_2)

# データベース初期化
my_db = InMemoryDB()
my_db.add_author("1", author_1)
my_db.add_author("2", author_2)

my_db.add_book(book_1)
my_db.add_book(book_2)
my_db.add_book(book_3)

# 各クラスの初期化
book_repository = MyBookDataAccess(my_db)
author_repository = my_db  # InMemoryDBをAuthorRepositoryとして利用
presenter = GetBooksPresenter()

# ユースケースの初期化
get_books_use_case = GetBooksDetailUseCase(
    book_repository=book_repository,
    author_repository=author_repository,
    presenter=presenter
)

# コントローラーの初期化
controller = BooksController(get_books_use_case=get_books_use_case)

# リクエストを作成して実行
mock_request = MockRequest(data={"author_id": "1"})
response = controller.get(request=mock_request)

# 結果を出力
print(f"Status Code: {response.status_code}")
print(f"Response JSON: {response.json()}")

結局何が嬉しいの?

クリーンアーキテクチャを採用するメリット

  1. 実装する際にEntity -> Application -> Infra または presenterという順に実装するためコードの中心にEntityやApplicationといったロジックが置かれる
  2. interfaceでpresentationとInfrastructureとapplicationを分離しているため、interfaceにさえ準拠していれば、InfraだけテストするとかMockを使用するとかできる
  3. UseCaseごとにロジックを記述するため、特定のユースケースの変更が別のユースケースの変更に影響を与えづらい(Entityを変更するとかだと影響する)
  4. 技術変更が容易。MySQLからPostgreSQLへの変更など
  5. 依存関係の明確化によって、全体構造を把握しやすい
    私が個人的に考えるデメリットは以下です
    デメリット
  6. コードが冗長的になる。ユースケースごとに記述するため一つのユースケースに対するコード量が増す。->短期的な開発効率は落ちる
  7. 開発者の思想(層の分離など)を遵守しないと、依存関係が曖昧になりがち
  8. 学習コスト。今までMVCなどで開発していた人がいきなりクリーンアーキテクチャに則って開発するのは難しいかも
  9. MTVやMVCに基づいたフレームワーク(Djangoなど)に導入しようとすると少し面倒

以上のことを踏まえると、必ずしもクリーンアーキテクチャが良いとは言えません。作成者がどの程度の規模のアプリケーションをどの程度の期間で作りたいかなどケースバイケースであるということです。しかし、クリーンアーキテクチャの依存関係の明確化という概念は、interfaceやDIなどをフルに活用していて、pythonだけでなくtypescriptなどの言語でよく出てくるので理解しておくことは大切だと思います。

今回はここまでです。DIの部分などpythonのinjectorライブラリなど使用すればもっと綺麗にコードを書くことができるのですが、ここでは割愛します。

何か解釈違いなどあれば遠慮なく指摘していただけると幸いです。github上にもソースコードを載せていく予定なので、随時本記事は更新されていくかと思います。質問などあればお気軽に。

Discussion