Featured image of post AIサービスの推論APIのシステム設計の備忘録

AIサービスの推論APIのシステム設計の備忘録

目次

概要

  • FastAPIとPydanticでDDD likeなAIのシステム設計をした時の備忘録
  • Djangoや他のMVC-likeなFWとは違い、コアにはPydanticのみを使用した
  • サーバー構成は重い処理に対処するため非同期分散処理のアーキテクチャを採用した
  • その時の実装のメモ(特にDDD周り)を備忘録として残した
  • ADR(Architectural Decision Records)に近いWhyを残す

構成

処理内容

  • AIの推論処理は次のような特徴がある
    • 重い処理
    • 場合に応じてマシンにGPUが必要
    • CPU/GPUやRAM/VRAMバウンディングな処理
    • 実行時間が長い
    • 並列実行数の制限が必要
  • そこで、推論処理は別サーバーに分離し非同期で行う事とした
  • また、個別の推論処理に合わせてマイクロサービスに分離した
    • 理由は推論処理に合わせて、インスタンスタイプを最適化するため

DBの選定

DBは次の理由からPostgresを採用した。

  • 垂直スケールアップ型で対応可能な負荷だったため
  • Pythonと相性のいいため
  • 推論結果のリレーションが必要だったため
  • NoSQL程のパフォーマンスは必要なかったため

テーブル設計

テーブルは次のようなエンティティを定義した。

  • ユーザー(users)
  • 画像データ(images)
  • 分析結果(analysis)
  • 推論結果(faces, quotes, etc..)

ID発番は将来的な分散処理を見越してUUIDv4を採用した。
NOTE: UUIDv3は別ホストで衝突する可能性がある。

アーキテクチャ

アーキテクチャには、プロデューサー / コンシューマーアーキテクチャを採用した。
マイクロサービスは20個ほど分離して処理毎に別インスタンスを用意した。

アーキテクチャ図

アクターは次になる。

  • Frontend Application
  • Producer Server(BFF)
  • Consumer Server(Batch)
  • Microservice Servers

大まかにフローは次のような流れである。

  1. Frontend ApplicationからProducer Serverにリクエストを投げる
  2. Producer ServerがJob Queue ServerにMessageをEnqueuする
  3. Consumer ServerがそのMessageをDequeueする
  4. Consumer ServerがMicroserviceの依存関係を解決して推論処理をMicroserviceにPostする
  5. Consumer ServerがDBに結果を記録する
  6. Producer ServerがDBの結果を取得する

プログラミング言語の選定

  • GoかPythonが候補にあったがPytonに決定
  • 理由はAIサービスと相性がいいため
  • Go + gRPCもありだったがあくまで推論処理なのでPythonを選んだ
    • 殆どのワークロードはAIの推論処理
      • その推論処理はONNXを使うだけ
    • プログラミング言語の差は問題ではない
      • GPU, Memoryバウンディングの処理が多いため
    • Goのようなcorutine(gorutine)で同時並行に処理する必要がなかった
    • アーキテクチャで処理ごとに分散処理しているため
  • またgRPCのようにスキーマをmicroservice毎に定義せず、1つにまとめた

FWの選定

  • Pydanticを採用した
  • また、Web FWはPydanticと相性のよいFastAPIを採用
  • OpenAPIを利用してドキュメント化できるのが良かった
  • また、Web APIはNginxとそのローカルキャッシュを利用した
  • テストはpytestで行った

Manager-Workerでの通信規格の統一化

  • Microserviceの推論の結果としてMicroserviceRepresentationDTOに統一した
  • 具体的には以下のようにPydanticでクラスを作り、それをベースにFastAPIのRequest型とした
    • corutineで並列処理をする必要がないため、uvicornではなくgunicornなどのAPサーバーでも良かったのかもしれない
  • そしてそれぞれの規格でタグ付き共用体でLiteralを利用する
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
from pydantic import BaseModel, EmailStr, Field, field_validator, root_validator, ValidationError
from typing import Optional, NewType, Any, TypeAlias, Literal, Union
from PIL import Image, JpegImagePlugin, PngImagePlugin
import base64
import io
import json
from enum import Enum
import numpy.typing as npt
import numpy as np

class XXXEntity(BaseModel):
    pass

class MicroserviceInferenceRepresentationDTOV1(BaseModel):
    version: Literal[1]
    xxx: list[XXXEntity]

class MicroserviceInferenceRepresentationDTOV2(BaseModel):
    version: Literal[2]
    xxx: list[XXXEntity]
    zzz: list[XXXEntity] | None = None

MicroserviceInferenceRepresentationDTO = MicroserviceInferenceRepresentationDTOV1 | MicroserviceInferenceRepresentationDTOV2


def parse_microservice_inference_representation(body: dict[str, Any]) -> MicroserviceInferenceRepresentationDTO:
    version = body.get("version", -1)
    if version == 1:
        return MicroserviceInferenceRepresentationDTOV1.parse_obj(body)
    elif version == 2:
        return MicroserviceInferenceRepresentationDTOV2.parse_obj(body)
    else:
        raise ValueError("Unsupported version provided")

# 使用例
body = {
    "version": 2,
    "xxx": [],
}

try:
    dto = parse_microservice_inference_representation(body)
    print(f"DTO parsed successfully: {type(dto)}")
except (ValidationError, ValueError) as e:
    print(f"Error parsing DTO: {e}")

モジュール化

  • 機能・責務ごとに別pythoin packageに分けてモジュールとして分割統治する
  • DDDの流儀でまとめたビジネスロジックもそのモジュールの一つである
  • 例えば、CVの共通処理はxxxpj-cvみたいな形である
  • それらをxxxpj/cv/ようにimplicit namespaceを置いて、別々モジュールも一つのnamespaceで一元管理する

分散システム

分散処理の通信パターン

上述のアーキテクチャ図からわかるように、今回は次の2つのパターンを採用した。

プロデューサー・コンシューマー

  • プロデューサ・コンシューマパターンは、プロセスの役割をデータを登録(生産)するものと処理(消費)するものに区別しているパターン
  • プロデューサーとコンシューマの間には仲介する有限のバッファで間接的に通信となり、両者間で非同期な通信をする
  • ほかの言い方だと、メッセージキュー(Message Queue)パターン
  • MessageはTaskとも呼ばれたりする

Producer Consumer Pattern

マスタ・ワーカー

  • マスタ・ワーカーパターンはマスタ・スレーブ(Master Slave)モデルとも呼ばれるパターン
  • 1つのマスタがプログラムの実行を管理し、そのプログラム全体や一部の処理の実行をワーカーに割り当てる

Master Worker Pattern

通信パターンの比較

クライアント・サーバーパターンとマスタ・ワーカーパターンの違いは次。

  • クライアント : サーバー = N : 1
  • マスター : ワーカー = 1 : N

また、N:Nの通信は次の2つのパターンがある。

  • Hubパターン
    • NodeはHubと通信するのでNodeの処理は楽になる
    • ただし、Hubが単一障害点になる問題がある
  • P2Pパターン
    • BitcoinなどのCryptoのClientのパターン
    • 非中央集権でありスケーラビリティに優れている

P2P Pattern

分散トランザクション

  • トランザクションに関してはManagerのインスタンスでシングルトンのように一元的に状態を管理する
  • つまり、Managerからのみ、RDBにのみ対してAtomicな処理が走るので、分散トランザクションにはならない
  • そのため、TCCパターンとSagaパターンのような分散トランザクションのパターンの適用は不要だった

Sageパターン

DDD関連

ドメイン

ドメインの目的とは何か?

  • ドメインとはビジネスモデルをカプセル化したもの
  • OOPでアクセッサーを経由してフィールドにアクセスするように、ビジネスモデルを独立するのが狙い
  • その実態なドメインのモデル(エンティティ+値オブジェクト)やサービス(ドメインサービス)である
  • エンティティはドメイン層の中で、JavaのPOJOのような基本的にどこにも依存しない作りにする
  • DDDの思想としてビジネスロジックをFWから独立させるのが目的
    • 例えば、RailsのModelにビジネスロジックを書き込むと、そのFWが終了した時に直せなくなる
    • つまり、FWよりビジネスモデルの方がライフサイクルが長いので、ビジネスモデルをFWから切り離した構造になっている
  • 故にデータを取得するのはRepository層が行い、データはエンティティとし、ドメインサービスでビジネスロジックを実行する

他のアーキテクチャとの比較

  • コアにあるのはビジネスモデルをFWに依存させない事
  • 故に、DDDでもクリーンアーキテクチャでもオニオンアーキテクチャでもドメインがコアにある

DDDのアーキテクチャ

戦術的DDD

クリーンアーキテクチャ

オニオンアーキテクチャ

エンティティと値オブジェクト

エンティティ vs. 値オブジェクト

  • エンティティ
    • 値オブジェクトを複数もつストラクト(社員、記事、商品など)
  • 値オブジェクト
    • ストラクトの中の要素(名前、誕生日、体重など)

Personの例

エンティティ

  • 識別
    • IDの識別子を持つ
    • IDによる等価性
  • フィールド
    • フィールド名は自由
    • フィールドの値の更新はできる
      • ただし、値オブジェクトのフィールドの場合は、その値オブジェクトを再生成して詰める
    • ユーザー、注文、製品など識別する必要があるもの

値オブジェクト

  • 識別
    • IDの識別子を持たない
    • 値による等価性
  • フィールド
    • フィールドの値
      • イミュータブル
      • 更新はできない
    • valueのフィールドを持つ
    • 作成時にバリデーションが走る
    • 住所、金額、日付範囲などの識別する必要がない値

モデルとは

DDDの定義だとモデルは大まかに次の関係にある。

$$ モデル = エンティティ + 値オブジェクト $$

ドメインイベントとドメインサービス

ドメインサービス (Domain Service)

  • ドメインサービスは、エンティティや値オブジェクトに自然に属さないビジネスロジックをカプセル化するために使用されるサービス
  • 具体的にはInputとOutputのDTOと、各種リポジトリを引数に撮り、ビジネスロジックを実行する
  • ドメインサービスは、ドメインモデルの一部であり、アプリケーションのビジネスルールやビジネスプロセスの事

ドメインイベント (Domain Event)

  • ドメインイベントは、システム内で発生した重要なビジネスイベントや状態の変化を表すオブジェクト
  • 「注文が完了した(OrderCompleted)」や「顧客が登録された(CustomerRegistered)」など
  • ドメインイベントは、その情報をシステムの他の部分に伝えるために使用されるDTO的なもの

DDDのレイヤーの名前の違い

層は次のような物がDDDやクリーンアーキテクチャでは利用される。

  • プレゼンテーション層
  • アプリケーション / ユースケース層
  • ドメイン層
  • インフラストラクチャ層

PoEAAでの鉄板

DAO vs. DTO

  • DAO(Data Access Object)
    • データベースやその他の永続化メカニズムへのアクセスを抽象化し、データアクセスロジックをカプセル化する事が目的
    • 故にメソッドはCRUD関連のメソッドなどになる
  • DTO(Data Transfer Object)
    • 異なるソフトウェアの層やシステム間でデータを転送するためのコンテナ
    • ネットワーク越しにデータを効率的に送受信するため、または異なる層間でのデータの受け渡しを簡素化するために使用される

DTO vs. エンティティ

  • 一言で言うと、DTOはデータを移動差せる為のオブジェクト
  • 他方、DDDのEntityはビジネスのモデルの写像
  • 故に、レイヤー間をまたいで一時的に利用する用途の場合はDTOを使う
  • 他方、ビジネスモデルを表現したドメインエンティティなどはエンティティを使う
  • あくまで、DTOはコンテナにすぎない

アグリゲート

アグリゲートとは

  • アグリゲート(Aggregate)は一貫性と整合性を維持するために一緒に管理されるべきエンティティと値オブジェクトのクラスター
  • 簡単に言うと、RDBでいう値オブジェクトがカラム、エンティティがテーブルに対応するとき、アグリゲートはRDBでいうデータベースに対応する
  • DBでも複数のテーブルで不整合がないように外部キー制約をかけるが、アグリゲートでもそれに近いことを行う

ルートエンティティ

  • アグリゲートはエンティティの集合だがアクセスポイントとしてルートエンティティを持つ
  • アグリゲートの一貫性を保証するためのビジネスルールを強制する
  • RDBのようにグラフ構造ではないので、DDDでのアグリゲートはクラスター構造となる
  • 故にアクセスポイントが必要となる

ルートアグリゲート

具体的な例

ドメインエンティティと値オブジェクトの例

前提

  • 下はCustomerAnalysisのDomain Entityの例
  • DBにはCustomer、User、UserProfileテーブルなどがある
  • それらをドメインオブジェクト化したものが以下のEntity
  • python 3.10を使用
  • 画像解析も実際には下のような形でビジネスモデルを定義した

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
from pydantic import BaseModel, EmailStr, Field, field_validator, model_validator
from typing import Optional, NewType, Any, TypeAlias
from PIL import Image, JpegImagePlugin, PngImagePlugin
import base64
import io
import json
from enum import Enum
import numpy.typing as npt
import numpy as np

UserId = NewType('UserId', int)
CustomerAnalysisId = NewType('CustomerAnalysisId', int)

ImageNp: TypeAlias = npt.NDArray[np.uint8]
ImagePil: TypeAlias = Image.Image

class ImageUtil:
    @staticmethod
    def base64_to_image(base64_str: str):
        image_data = base64.b64decode(base64_str)
        image = Image.open(io.BytesIO(image_data))
        return image

    @staticmethod
    def image_to_byte_array(image: ImagePil):
        img_byte_arr = io.BytesIO()
        image_format = 'JPEG' if isinstance(image, JpegImagePlugin.JpegImageFile) else 'PNG'
        image.save(img_byte_arr, format=image_format)
        return img_byte_arr.getvalue()

    @classmethod
    def image_to_base64_str(cls, image: ImagePil):
        return base64.b64encode(cls.image_to_byte_array(image)).decode('utf-8')


class MyBaseModel(BaseModel):
    class Config:
        # デフォルトでは、Pydanticは標準のデータ型しか型が使えないため
        # 例えば、int、str、float、list、dict など)およびPydanticモデルのインスタンス
        arbitrary_types_allowed = True

class UserProfileCategory(str,Enum):
    STANDARD = "standard"
    ADMIN = "admin"
    GUEST = "guest"

class UsernameValue(BaseModel):
    value: str

    @field_validator('value')
    def validate_username(cls, v):
        if not v.isalnum() or len(v) < 3:
            raise ValueError('Username must be at least 3 characters long and contain only alphanumeric characters')
        return v

class EmailValue(BaseModel):
    value: EmailStr

class ProfileImageValue(MyBaseModel):
    value: ImagePil
    mime_type: str

    class Config:
        json_encoders = {
            ImagePil: ImageUtil.image_to_base64_str,
        }

class CustomerAnalysisEntity(BaseModel):
    id: CustomerAnalysisId
    user_id: UserId
    username: UsernameValue | None = None
    email: EmailValue | None = None
    profile_image: ProfileImageValue | None = None
    user_type: UserProfileCategory

    @model_validator(mode="after")
    def validate_root(cls, values):
        if not (values.username or values.email):
            raise ValueError('Either username or email must be provided.')
        return values

    def to_json(self):
        return self.json(exclude_none=True)

    def to_dict(self):
        return self.dict(exclude={})

    @classmethod
    def from_dict(cls, data: dict[str, Any]):
        if 'profile_image' in data and data['profile_image'] is not None and 'value' in data['profile_image']:
            base64_str = data['profile_image']['value']
            image = ImageUtil.base64_to_image(base64_str)
            data['profile_image']['value'] = image
        return cls(**data)

    @classmethod
    def from_json(cls, data: str):
        json_data = json.loads(data) # str -> dict
        return cls.from_dict(json_data)

次のように使用する。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
class Test:
    img_path = 'image.png'
    @classmethod
    def to_json(cls):
        img = Image.open(cls.img_path)
        customer_analysis = CustomerAnalysisEntity(
            id=CustomerAnalysisId(111),
            user_id=UserId(12345),
            username=UsernameValue(value="JohnDoe"),
            email=EmailValue(value="johndoe@example.com"),
            profile_image=ProfileImageValue(
                value=img,
                mime_type="image/jpeg"
            ),
            user_type=UserProfileCategory.STANDARD
        )

        user_profile_json = customer_analysis.json()
        file_path = 'customer_analysis.json'
        with open(file_path, 'w') as file:
            file.write(user_profile_json)

    @staticmethod
    def from_json():
        file_path = 'customer_analysis.json'
        with open(file_path, 'r') as file:
            user_profile_json = file.read()
        customer_analysis = CustomerAnalysisEntity.from_json(user_profile_json)
        print(customer_analysis)

    @staticmethod
    def to_dict():
        file_path = 'customer_analysis.json'
        with open(file_path, 'r') as file:
            user_profile_json = file.read()
        customer_analysis = CustomerAnalysisEntity.from_json(user_profile_json)
        user_json = customer_analysis.to_dict()
        print(user_json)

    @staticmethod
    def from_dict():
        user_profile_dict = {
            "id": 111,
            "user_id": 12345,
            "username": {"value": "JohnDoe"},
            "email": {"value": "johndoe@example.com"},
            "profile_image": None,
            "user_type": "standard"
        }
        customer_analysis = CustomerAnalysisEntity.from_dict(user_profile_dict)
        print(customer_analysis)



if __name__ == "__main__":
    Test.to_json()
    Test.to_dict()
    Test.from_json()
    Test.from_dict()

ドメインサービスの例

前提

エンティティ

  • エンティティはドメインオブジェクトなので、基本的にどこにも依存しない作りにする
  • DDDの思想としてビジネスロジックをFWから独立させるのが目的
    • 例えば、MVCのようなFWの、Modelにビジネスロジックを書き込むとMVCのFWが終了した時に直せなくなる
  • そこで。ビジネスモデルはドメイン層としてFWから分離したのがDDDの思想

リポジトリ

  • インフラ層で定義するデータアクセッサー
  • 実装はドメイン層のrepository_interfaceを参照する
  • ドメインサービスの入力となる
  • リポジトリ層の戻り値はエンティティになる

サービス

  • DDDのサービスはいわゆるドメインサービスの事
  • ドメインサービスでは少し特殊な形で用意する
  • 具体的には、Repositoryに依存しないようにRepositoryを引数にとる関数となる
    • ただし、型定義の際にRepositoryをImportするとRespository層に依存してしまうため、あくまでDomain層のRepository Interfaceを型にする
  • また、入力と出力が煩雑になるため、InputDTOとOutputDTOを用意する
  • 入力とシンプルにリポジトリとInput(DTO)を利用して実装された関数
  • 具体的には外部のリポジトリを利用するが、ドメインサービスがリポジトリに依存しないようにIFとして型をとる
  • 内部では、引数で渡った関数の入力とリポジトリーから取得したドメインエンティティを元に処理を行う
  • サービスは基本的にビジネスロジックなのでビジネスロジックの言葉で書くのが正しい
  • 例えばデータを保存するドメインサービスなどでは、トランザクションなどはドメインサービスを呼び出す側が処理するのが正しい

具体例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
class XXXInputDTO(BaseModel):
    aaa: str
    bbb: str

class XXXOutputDTO(BaseModel):
    xxx: str
    yyy: str

def domain_subdomain1_xxx_serveice(
  xxx_input_dto: XXXInputDTO, 
  xxx_repo: IXXRepository, 
  yyy_repo: IyyyRepository
) -> XXXOutputDTO:
  pass

DDDのLibのフォルダ設計例

前提

  • AWSのCodeartifactを利用し、共通ライブラリをpython packageに分離した
  • マイクロサービスで共通して使うため、libとして各サービスでimportするようにした
  • 便宜上形式的に__init__.pyは省略している

実例

フォルダの構成の具体例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
$ tree -L 3
└── common
    ├── models
    │   ├── representation_dto.py
└── domain
    ├── subdomain1
    │   ├── model
    │   │   ├── entity.py
    │   │   ├── value_object.py
    │   ├── service.py
    │   ├── repository_interface.py
    ├── subdomain2
    │   ├── model
    │   │   ├── entity.py
    │   │   ├── value_object.py
    │   ├── service.py
    │   ├── repository_interface.py
└── application
    ├── use_cases
    │   ├── subdomain1_use_case.py
    │   ├── subdomain2_use_case.py
└── infrastructure
    ├── repository
    │   ├── subdomain1_repository_impl.py
    │   ├── subdomain2_repository_impl.py
    │   ├── dao
    │   │   ├── xxx_dao.py
    ├── external_services
    │   ├── xxx_service_adapter.py
    ├── database
    │   ├── orm_models.py
    │   ├── database_accessor.py
└── presentation
    ├── web
    │   ├── controllers
    │   ├── views
    │   ├── viewmodels
    ├── api
    │   ├── controllers
    │   ├── dtos

DDDのUsecaseの利用例

使い方は次のように使う。

1
2
3
4
5
6
7
8
9
# app/use_cases/subdomain1_use_case.py
from domain.subdomain1.service import SomeDomainService
from infrastructure.repository.subdomain1_repository_impl import Subdomain1RepositoryImpl

def execute_some_use_case():
    repository = Subdomain1RepositoryImpl()
    domain_service = SomeDomainService(repository)
    result = domain_service.perform_some_operation()
    return result
  • usecase層では引数に撮らないが、ドメインサービスは独立するために、repositoryやinputなどを入力としてとるのがキモである
  • なぜなら、ドメイン層はコアであり、他の層に依存してはいけないから

名前空間の設計例

前提

  • 複数のlibの外部パッケージに分割し、共通の名前空間を利用した
  • 具体的には、pythonのImplicit Namespace Packages機能を利用した
  • pythonはフォルダに__init__.pyを設置する必要がある
  • それを設置しないフォルダは、packageとして読み込めない
  • ただし、__init__.pyを設置すると、そのフォルダの初期化がかかってしまう
  • 故に複数のライブラリ、aaa_libbbb_libがあったとき共通のフォルダに__init__.pyを置くと先勝ちしてしまう
    • xxx_pj/aaa_lib/main.pyxxx_pj/bbb_lib/main.pyがあるとする
    • これらは別々のpythonのpackageであり、それぞれpip install aaa_lib bbb_libとしてインストールする
    • 両方の共通フォルダであるxxx_pjには__init__.pyが設置してある
    • 故に、import aaa_libをすると、bbb_libは読み込めなくなってしまう
    • なぜなら、すでにimport aaa_libxxx_pj/__init__.pyが初期化されるから
    • つまり、同じパッケージ名のものがあった場合は、先勝ちになってしまう
  • そこで、xxx_pjはimplicit namespace packageとして、xxx_pj/__init__.pyを置かず、pj全体の共通の名前空間を利用する
  • すると、import aaa_libimport bbb_libが問題なく行えるようになる
  • これは開発時のIDEの補完にもxxx_pjから複数の別々のライブラリを読み込めるので、dx(dev experience)にも効果的である

具体例

下のように、共通するnamespaceには__init__.pyをおかないようにしている。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
$ cd aaa_lib
$ tree -L 3
└── xxx_pj
    ├── aaa_lib
    │   ├── __init__.py
    │   ├── main.py

$ cd bbb_lib
$ tree -L 3
└── xxx_pj
    ├── bbb_lib
    │   ├── __init__.py
    │   ├── model
    │   │   ├── __init__.py
    │   │   ├── main.py

結論

  • 上記の通り、DDDでAIの解析システムを作るのは、以外と考える事が多い
  • 他にもPrecommit、Lint、pytest、Dockernize、FastAPIのエラーの共通化やら色々なトピックがある
  • ただ、少なくとも、プロダクトのライフサイクルに合わせて、ソフトウェア設計を見極めるべき
    • ライフサイクルが短い => MVCでサクッと作る
    • ライフサイクルが長い => DDDで重厚に作る
  • 特に、必要最低限、疎結合かつDRYかつKISSに作るには一貫した設計が必要

参考文献

Built with Hugo
テーマ StackJimmy によって設計されています。