やること
目的はAIにクラス設計をさせることができるか確認すること。利用したクラス設計本は以下
ミノ駆動本
やった手順は以下
1.ミノ駆動本を読んで要点をマークダウンでまとめる
本の要約は以下記事
余談だが、AIは要約を教えるまでもなく既に上の本について知っていた。私が読む必要すらなかった。恐るべし生成AI
2.生成AIに対してクラス設計をさせる
利用した生成AIは以下を無料枠以内で利用
3.クラス設計の結果をプログラミングの実装にさせる。
今回アウトプットとするプログラミングコードはPythonを選定した。フロント言語でも同じように実施可能。
クラス設計
以下プロンプト。利用したのはgemini2.0です。
あなたはソリューションアーキテクトです。
以下に書いた設計ルールを守ってクラス設計を行い、メルカリや楽天市場のようなEC2 webサイトを作成したいと思っています。クラス設計(インスタンス変数およびメソッド含む)をお願いしたいです。
なお次の機能はマスト、その他フリマサイトにありがちな一般的な仕様をいくつか提案して、満たしてください。
1.ユーザは購入と出品ができる。
2.ユーザは出品されているリストを閲覧でき、目的の出品物に辿り着くことができる。
3.品は出品中と購入済みのステータスを持つ
以下に満たすべきルールを掲載します...
""上記の要約を掲載""
あなたの設計した各クラスについてmermaid記法で表してください。
プロンプトは微調整をするため何度か対話を繰り返した。以下のサイトなどを利用してmermaid記法がバリデーションを突破するまで繰り返した。
クラス設計の結果は構造化したいのでmermaid形式で出力させた。qiitaの2000文字制限に入りきらないため一部割愛。
なかなか良さそうな図ができた。
設計したクラスをAIに実装させてみた
gemini2.0/ChatGPT4o/claude-sonnet3.5 を併用していい感じのコードができるまで繰り返しプロンプトした。
from dataclasses import dataclass
from typing import List, Self
from enum import Enum
from datetime import datetime
from abc import ABC, abstractmethod
@dataclass(frozen=True)
class ItemId:
itemId: str
def __post_init__(self):
if not self.itemId:
raise ValueError("itemIdは空であってはなりません")
def value(self) -> str:
return self.itemId
@dataclass(frozen=True)
class UserId:
userId: str
def __post_init__(self):
if not self.userId:
raise ValueError("userIdは空であってはなりません")
def value(self) -> str:
return self.userId
@dataclass(frozen=True)
class Price:
amount: int
def __post_init__(self):
if self.amount < 0:
raise ValueError("価格は0以上である必要があります")
def value(self) -> int:
return self.amount
def changeAmount(self, newAmount: Self) -> Self:
return Price(newAmount.value())
@dataclass(frozen=True)
class ItemName:
name: str
def __post_init__(self):
if not self.name:
raise ValueError("商品名は空であってはなりません")
def value(self) -> str:
return self.name
@dataclass(frozen=True)
class ItemDescription:
description: str
def value(self) -> str:
return self.description
@dataclass(frozen=True)
class Keyword:
value: str
@dataclass(frozen=True)
class ShippingAddress:
value: str
@dataclass(frozen=True)
class PaymentMethod:
value: str
@dataclass(frozen=True)
class BankAccount:
value: str
@dataclass(frozen=True)
class CommissionRate:
value: float
def __post_init__(self):
if not 0 <= self.value <= 1:
raise ValueError("手数料率は0から1の間である必要があります")
class User(ABC):
@abstractmethod
def getUserId(self) -> UserId:
pass
@dataclass(frozen=True)
class Buyer(User):
userId: UserId
shippingAddress: ShippingAddress
paymentMethod: PaymentMethod
def getUserId(self) -> UserId:
return self.userId
@dataclass(frozen=True)
class Seller(User):
userId: UserId
bankAccount: BankAccount
commissionRate: CommissionRate
def getUserId(self) -> UserId:
return self.userId
class ItemStatus(Enum):
FOR_SALE = "FOR_SALE"
SOLD = "SOLD"
def isForSale(self) -> bool:
return self == ItemStatus.FOR_SALE
def transitionToSold(self) -> "ItemStatus":
if self == ItemStatus.SOLD:
raise ValueError("すでにSOLD状態です")
return ItemStatus.SOLD
def __str__(self) -> str:
return self.value
@dataclass(frozen=True)
class Item:
itemId: ItemId
seller: Seller
name: ItemName
description: ItemDescription
price: Price
status: ItemStatus = ItemStatus.FOR_SALE
def transitionToSold(self) -> "Item":
return Item(
itemId=self.itemId,
seller=self.seller,
name=self.name,
description=self.description,
price=self.price,
status=self.status.transitionToSold(),
)
def getStatus(self) -> ItemStatus:
return self.status
@dataclass(frozen=True)
class SoldItem:
item: Item
buyer: Buyer
soldAt: datetime
def __post_init__(self):
if self.item.status.isForSale():
raise ValueError("まだ売り出し中です")
@dataclass(frozen=True)
class Items:
def __init__(self, initList: List[Item] = []):
self._items: List[Item] = initList
def add(self, item: Item) -> "Items":
_new_items: List[Item] = self._items
_new_items.append(item)
return Items(_new_items)
def asList(self) -> List[Item]:
return self._items[:]
def findByKeyword(self, keyword: Keyword) -> List[Item]:
return [item for item in self._items if keyword.value in item.name.value()]
def findBySeller(self, seller: UserId) -> List[Item]:
return [item for item in self._items if item.seller.userId == seller.userId]
def main():
sampleSeller: Seller = Seller(
UserId(1), BankAccount("xxxxxxxx"), CommissionRate(0.1)
)
sampleBuyer: Buyer = Buyer(
UserId(2), ShippingAddress("東京都1-1-1"), PaymentMethod("カード決算")
)
sampleItem: Item = Item(
ItemId(1),
sampleSeller,
ItemName("テスト商品"),
ItemDescription("テストで出品している商品です"),
Price(5000),
)
print(sampleSeller)
try:
sampleSeller.userId.userId = 3
except Exception as e:
print(f"Error: {e}")
sampleSoldItem = sampleItem.transitionToSold()
soldItem = SoldItem(sampleSoldItem, sampleBuyer, datetime.now())
print(soldItem)
if __name__ == "__main__":
main()
いくつか実践してみて思ったこと
大前提、アウトプット品質を自分が管理できないといけない
今回本を要約してみて生成させたが、要約自体の理解が怪しいところは評価しづらいので、やはり自分のエンジニアリング能力が大事だと感じている。
生成AIだとalchemyなどORMを提案してくる
クラス設計させているにも関わらず、コードにする部分でAlchemyなどORMの設計に書き換えてくることが何回かあった。
データベース設計との関係
ORMの提案をされるところで思ったがクラス設計とデータベース設計の切り分けってどうするのが良いのだろうか。このあたり自分がわかっていなかったので聞いてみた。
良いコード/悪いコードで学ぶ設計入門 という本について質問があります。
例えば今回は出品者と購入者でクラスを分けたりしましたが、実際にはデータベースに値を格納すると思います。
その場合、どうやって格納するのが正しいのでしょうか?クラス設計にのっとりデータベース設計するのが正しいのか、不一致を許すのかが気になります。
claude AIの回答
データベース設計とドメインモデルの関係について、主に3つのアプローチがあります:
1.Active Recordパターン
クラスとテーブルを1:1で対応させる
実装が簡単だが、ドメインロジックとデータアクセスが密結合になる
小規模なアプリケーションに適している
2.Data Mapperパターン
ドメインモデルとデータベースを分離
Repository層を設けて変換を行う
ドメインモデルの純粋性を保てる
DDD(ドメイン駆動設計)で推奨される
3.CQRS (Command Query Responsibility Segregation)
参照(Query)と更新(Command)で異なるモデルを使用
複雑な検索やレポート生成に適している
ドメインモデルは更新操作のみに使用
この場合、Data Mapperパターンを採用し、以下のような構造にすることを推奨します:
# データベーステーブル構造
users
- id
- type (seller/buyer) # 区別するための列
- shipping_address
- payment_method
- bank_account
- commission_rate
# Repositoryの例
class UserRepository:
def find_by_id(self, user_id: UserId) -> User:
user_data = self.db.query(...)
if user_data.type == 'seller':
return Seller(...)
return Buyer(...)
gemini2.0 の回答
「良いコード/悪いコードで学ぶ設計入門」で学んだクラス設計と、実際のデータベース設計との関係についての疑問ですね。出品者と購入者を別クラスに分けた場合、それをデータベースにどのように格納するのが適切か、という点は非常に重要な問いです。
結論から言うと、クラス設計とデータベース設計は目的が異なるため、完全に一致させる必要はありません。不一致を許容し、それぞれの目的に最適化することが重要です。
クラス設計の目的:
- オブジェクト指向プログラミングの原則に従う: 凝集度を高め、結合度を低くすることで、保守性,拡張性、再利用性の高いコードを作成すること。
- ドメインロジックを表現する: 現実世界の問題領域(この場合はECサイトの取引)をオブジェクトとして表現し、コードで扱いやすくすること。
データベース設計の目的:
- データの永続化: データを安全に保存し、必要に応じて効率的に取得できるようにすること。
- データの整合性: データの重複を排除し、矛盾がない状態を維持すること。
- パフォーマンス: 大量のデータを効率的に処理できるようにすること。
これらの目的の違いから、クラス設計とデータベース設計の間には以下のような不一致が生じることがあります。
- 正規化: データベースではデータの重複を避けるために正規化が行われます。これにより、複数のテーブルにデータが分割されることがありますが、クラス設計ではそのような分割は必ずしも必要ありません。
- 継承: オブジェクト指向プログラミングでは継承が重要な概念ですが、リレーショナルデータベースでは継承を直接表現する機能は限られています(例えば、テーブル継承などの手法はありますが、オブジェクト指向の継承とは完全に一致するわけではありません)。
- 多対多の関係: データベースでは中間テーブルを使って多対多の関係を表現しますが、クラス設計ではコレクション(Listなど)を使って表現することが一般的です。
では、出品者と購入者の例で具体的に考えてみましょう。
クラス設計:
出品者 (Seller) と購入者 (Buyer) を別クラスに分け、User インターフェースを実装することで、ポリモーフィズムを活用し、共通の処理を抽象化することができます。これはオブジェクト指向の原則に沿った設計です。
データベース設計:
この場合、以下のような方法が考えられます。
- 単一のユーザーテーブル: users テーブルに、user_type カラム(例えば、"buyer"、"seller")を追加し、出品者と購入者を区別します。これにより、テーブル数を減らし、シンプルな構造にすることができます。ただし、出品者と購入者で異なる属性(例えば、銀行口座情報、手数料率、配送先住所、支払い方法)がある場合、NULL を許容するカラムが増え、テーブルが肥大化する可能性があります。
- テーブル継承: users テーブルを親テーブルとし、buyers テーブルと sellers テーブルを子テーブルとして作成します。これにより、出品者と購入者で異なる属性をそれぞれのテーブルに格納することができます。ただし、クエリが複雑になる場合があります。
- 関連テーブル: users テーブルとは別に、buyer_details テーブルと seller_details テーブルを作成し、users テーブルと外部キーで関連付けます。これにより、テーブルの肥大化を防ぎ、クエリも比較的シンプルに保つことができます。
どの方法が最適かは、システムの要件(例えば、出品者と購入者の属性の差、データ量、パフォーマンス要件など)によって異なります。一般的には、関連テーブルを使う方法がバランスが良く、多くの場合で適切な選択肢となります。
重要なのは、クラス設計で表現したドメインロジックを、データベースで適切に表現することです。例えば、Seller クラスが持つ getItemsForSaleWithStatus() メソッドは、データベースへのクエリとして実装されることになります。このクエリは、users テーブルと items テーブル、item_status テーブルなどを結合して、必要なデータを取得することになるでしょう。
つまり、クラス設計はドメインロジックを表現し、データベース設計はデータを永続化するための基盤を提供するという役割分担があり、それぞれの目的に最適化することが重要です。不一致を恐れず、それぞれの設計原則に従って設計することで、より良いシステムを構築することができます。
「良いコード/悪いコードで学ぶ設計入門」で学んだことを活かし、上記の点を考慮してデータベース設計を行うことで、より実践的な設計ができるようになると思います。
両方のAIの回答を踏まえ自分の解釈として、データベース設計とクラス設計は不一致で良い(実際の会社ではそもそもチームが不一致しているケースが多いと思う)。今回はクラス設計をテーマにしているのでデータベース上での重複が発生することはあまり気にする必要はない。
偶然辿り着きましたが以下のサイトも色々参考になりました。
フロントフレームワークへの転用
例えばreactなどのフレームワークやTypeScriptなどのコードでオブジェクト指向設計を踏襲した場合、先のClaudeAIの回答のようなレポジトリレイヤーや3層アーキテクチャのAPI層段階で本クラス設計に変換して渡してあげるのが良いのだろうか。
最適な構成やチームビルディングは各自社内の状態に合わせ、考える必要があるのかなと思った。