取り組みの背景 — なぜクリーンアーキテクチャと DDD なのか
プロジェクトの規模が大きくなるにつれ、コードの複雑性は指数関数的に増えていきます。単なる「動くコード」では、数ヶ月後に保守できなくなり、新機能の追加やバグ修正に膨大な時間がかかるようになってしまいます。
クリーンアーキテクチャ(Clean Architecture) と ドメイン駆動設計(DDD: Domain-Driven Design) は、この問題に対する強力な解決策です。ただし、設計原則を理解しているだけでは十分ではありません。実装段階で依存関係が絡み合い、原則から外れたコードが増えていくのが現実です。
ここで Antigravity の AI エージェントが活躍します。架構造の検証、リファクタリングの提案、自動修正まで、AI の力を使って本番品質のアーキテクチャを実現できます。
クリーンアーキテクチャの基本原則
4層構造とその役割
クリーンアーキテクチャは、同心円構造の4つのレイヤーで成り立っています。
1. エンティティ層(Entity Layer)
- ビジネスロジックの最核である「ドメインモデル」を定義
- フレームワークやDB、UI に依存しない純粋な業務ルール
- 変更頻度が最も低いコード
2. ユースケース層(Use Case Layer / Application Layer)
- アプリケーション固有のビジネスロジックを実装
- エンティティを操作し、外部システムとの調整を行う
- 「どのようにビジネスゴールを達成するか」を定義
3. インターフェースアダプター層(Interface Adapter Layer)
- Web フレームワーク(Express、Next.js等)
- DB リポジトリの実装
- 外部 API クライアント
- 内側の層と外側の層の間を繋ぐ「翻訳」の役割
4. フレームワークと外部ツール層(Frameworks and Tools)
- Web フレームワーク本体
- DB ドライバ
- キャッシング、メッセージングなどのインフラ
依存性の方向ルール
クリーンアーキテクチャの最も重要なルール:依存性は必ず内側に向かう(中心に向かう)。
外側のレイヤーは内側に依存できるが、内側のレイヤーが外側に依存することは絶対に許されません。
// ✓ 許可される依存方向
// ユースケース層 → エンティティ層
// アダプター層 → ユースケース層 → エンティティ層
// ✗ 禁止される依存方向
// エンティティ層 → ユースケース層
// エンティティ層 → アダプター層これにより以下が実現されます:
- 外部変化への耐性: フレームワーク変更の影響が内側に波及しない
- テスト容易性: ビジネスロジックをフレームワークなしでテストできる
- ビジネスロジックの再利用: 複数のインターフェース(Web、CLI、API)で同じロジックを使える
SOLID 原則との関係
クリーンアーキテクチャは SOLID 原則の具体化です。
- S(単一責任原則): 各クラスは1つの理由によってのみ変更される
- O(開放閉鎖原則): 拡張には開かれ、修正には閉じられている
- L(リスコフ置換原則): 継承時、子クラスは親クラスの代わりになる
- I(インターフェース分離原則): クライアントは必要なメソッドのみに依存
- D(依存性逆転原則): 上位モジュールは下位モジュールに依存しない
プロジェクト構造の設計
ディレクトリ構成の最適化
src/
├── domain/ # エンティティ層
│ ├── entities/
│ │ ├── User.ts
│ │ ├── Order.ts
│ │ └── Product.ts
│ ├── value-objects/
│ │ ├── Email.ts
│ │ ├── Money.ts
│ │ └── UserId.ts
│ ├── repositories/
│ │ ├── IUserRepository.ts
│ │ ├── IOrderRepository.ts
│ │ └── IProductRepository.ts
│ └── services/
│ ├── PricingDomainService.ts
│ └── InventoryDomainService.ts
│
├── application/ # ユースケース層
│ ├── use-cases/
│ │ ├── CreateOrderUseCase.ts
│ │ ├── UpdateUserProfileUseCase.ts
│ │ └── GetProductDetailsUseCase.ts
│ ├── dto/
│ │ ├── CreateOrderRequest.ts
│ │ ├── CreateOrderResponse.ts
│ │ └── GetProductResponse.ts
│ ├── services/
│ │ ├── OrderApplicationService.ts
│ │ └── UserApplicationService.ts
│ └── ports/
│ ├── IEmailService.ts
│ ├── IPaymentGateway.ts
│ └── INotificationService.ts
│
├── infrastructure/ # アダプター・外部ツール層
│ ├── repositories/
│ │ ├── UserRepository.ts
│ │ ├── OrderRepository.ts
│ │ └── ProductRepository.ts
│ ├── external-services/
│ │ ├── StripePaymentGateway.ts
│ │ ├── SendgridEmailService.ts
│ │ └── TwilioNotificationService.ts
│ ├── persistence/
│ │ ├── prisma/
│ │ └── schema.prisma
│ └── config/
│ ├── database.ts
│ └── dependencies.ts
│
├── presentation/ # コントローラー・ハンドラー層
│ ├── http/
│ │ ├── controllers/
│ │ │ ├── OrderController.ts
│ │ │ └── UserController.ts
│ │ └── middleware/
│ │ ├── auth.ts
│ │ └── errorHandler.ts
│ └── cli/
│ └── commands/
│ └── MigrateCommand.ts
│
└── shared/ # 共有ユーティリティ
├── errors/
│ ├── DomainError.ts
│ └── ApplicationError.ts
├── utils/
│ ├── Logger.ts
│ └── Validator.ts
└── types/
└── index.ts
モジュール分割の戦略
プロジェクトが大規模な場合、機能領域(Bounded Context)ごとにモジュールを分割します。
src/
├── modules/
│ ├── user/
│ │ ├── domain/
│ │ ├── application/
│ │ ├── infrastructure/
│ │ └── presentation/
│ ├── order/
│ │ ├── domain/
│ │ ├── application/
│ │ ├── infrastructure/
│ │ └── presentation/
│ └── payment/
│ ├── domain/
│ ├── application/
│ ├── infrastructure/
│ └── presentation/
各モジュール内でクリーンアーキテクチャの4層が独立して成立します。モジュール間の通信は、公開インターフェース(ポート)を通じてのみ行われます。
ドメイン層の実装
エンティティの設計
エンティティは、ビジネス上の一意な識別子を持つオブジェクトです。時間とともに属性は変化しますが、識別子は変わりません。
// User エンティティの例
import { UserId } from './value-objects/UserId';
import { Email } from './value-objects/Email';
export class User {
private userId: UserId;
private email: Email;
private name: string;
private age: number;
private createdAt: Date;
private updatedAt: Date;
constructor(
userId: UserId,
email: Email,
name: string,
age: number,
createdAt: Date = new Date(),
updatedAt: Date = new Date()
) {
this.validate(name, age);
this.userId = userId;
this.email = email;
this.name = name;
this.age = age;
this.createdAt = createdAt;
this.updatedAt = updatedAt;
}
private validate(name: string, age: number): void {
if (!name || name.trim().length === 0) {
throw new Error('User name must not be empty');
}
if (age < 0 || age > 150) {
throw new Error('User age must be between 0 and 150');
}
}
// ビジネスロジック:年齢をチェック
isAdult(): boolean {
return this.age >= 18;
}
// ビジネスロジック:メールアドレスを変更
changeEmail(newEmail: Email): void {
this.email = newEmail;
this.updatedAt = new Date();
}
// ビジネスロジック:プロフィール更新
updateProfile(name: string, age: number): void {
this.validate(name, age);
this.name = name;
this.age = age;
this.updatedAt = new Date();
}
// ゲッター
getId(): UserId {
return this.userId;
}
getEmail(): Email {
return this.email;
}
getName(): string {
return this.name;
}
getAge(): number {
return this.age;
}
getCreatedAt(): Date {
return this.createdAt;
}
getUpdatedAt(): Date {
return this.updatedAt;
}
}値オブジェクトの実装
値オブジェクトは、識別子を持たず、不変性により同値性で区別されるオブジェクトです。例えば、金額、メールアドレス、住所などがあります。
// Email 値オブジェクト
export class Email {
private readonly value: string;
constructor(value: string) {
if (!this.isValid(value)) {
throw new Error(`Invalid email format: ${value}`);
}
this.value = value;
}
private isValid(email: string): boolean {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
getValue(): string {
return this.value;
}
equals(other: Email): boolean {
return this.value === other.value;
}
toString(): string {
return this.value;
}
}
// Money 値オブジェクト
export class Money {
private readonly amount: number;
private readonly currency: string;
constructor(amount: number, currency: string = 'USD') {
if (amount < 0) {
throw new Error('Money amount must not be negative');
}
if (!/^[A-Z]{3}$/.test(currency)) {
throw new Error('Currency must be a valid ISO 4217 code');
}
this.amount = amount;
this.currency = currency;
}
getAmount(): number {
return this.amount;
}
getCurrency(): string {
return this.currency;
}
add(other: Money): Money {
if (this.currency !== other.currency) {
throw new Error('Cannot add money in different currencies');
}
return new Money(this.amount + other.amount, this.currency);
}
multiply(factor: number): Money {
return new Money(this.amount * factor, this.currency);
}
equals(other: Money): boolean {
return this.amount === other.amount && this.currency === other.currency;
}
toString(): string {
return `${this.currency} ${this.amount.toFixed(2)}`;
}
}
// UserId 値オブジェクト
export class UserId {
private readonly value: string;
constructor(value: string) {
if (!value || value.trim().length === 0) {
throw new Error('UserId must not be empty');
}
this.value = value;
}
getValue(): string {
return this.value;
}
equals(other: UserId): boolean {
return this.value === other.value;
}
toString(): string {
return this.value;
}
}ドメインサービスの実装
複数のエンティティや値オブジェクトにまたがるビジネスロジックは、ドメインサービスで表現します。
// 価格設定に関するドメインサービス
export class PricingDomainService {
// ユーザーの会員種別に応じた割引率を計算
calculateDiscount(
user: User,
itemCount: number
): number {
let discountRate = 0;
// プレミアム会員は10%割引
if (user.isPremiumMember()) {
discountRate = 0.1;
}
// 大量購入で追加割引
if (itemCount >= 10) {
discountRate += 0.05;
}
return discountRate;
}
// 税金を計算
calculateTax(
price: Money,
taxRate: number
): Money {
const taxAmount = price.getAmount() * taxRate;
return new Money(taxAmount, price.getCurrency());
}
// 最終価格を計算(割引と税金を適用)
calculateFinalPrice(
basePrice: Money,
user: User,
itemCount: number,
taxRate: number
): Money {
const discountRate = this.calculateDiscount(user, itemCount);
const discountedPrice = basePrice.multiply(1 - discountRate);
const tax = this.calculateTax(discountedPrice, taxRate);
return discountedPrice.add(tax);
}
}
// 在庫管理に関するドメインサービス
export class InventoryDomainService {
// 在庫が十分であるか確認
hasEnoughStock(product: Product, requestedQuantity: number): boolean {
return product.getStock() >= requestedQuantity;
}
// 在庫を確保
reserveStock(product: Product, quantity: number): void {
if (!this.hasEnoughStock(product, quantity)) {
throw new Error('Insufficient stock');
}
product.reserveStock(quantity);
}
// 在庫を解放
releaseStock(product: Product, quantity: number): void {
product.releaseStock(quantity);
}
}リポジトリインターフェースの定義
リポジトリは、ドメイン層で定義されるインターフェースです。実装はインフラストラクチャ層で行います。これにより、ドメインロジックは DB 実装に依存しません。
// リポジトリインターフェース(ドメイン層)
export interface IUserRepository {
save(user: User): Promise<void>;
findById(userId: UserId): Promise<User | null>;
findByEmail(email: Email): Promise<User | null>;
delete(userId: UserId): Promise<void>;
findAll(): Promise<User[]>;
}
export interface IOrderRepository {
save(order: Order): Promise<void>;
findById(orderId: OrderId): Promise<Order | null>;
findByUserId(userId: UserId): Promise<Order[]>;
delete(orderId: OrderId): Promise<void>;
}
export interface IProductRepository {
save(product: Product): Promise<void>;
findById(productId: ProductId): Promise<Product | null>;
findAll(): Promise<Product[]>;
delete(productId: ProductId): Promise<void>;
}ユースケース層の実装
アプリケーションサービスとユースケースクラス
ユースケースは、「ユーザーが何をしたいのか」という観点からビジネスロジックを組み立てます。
// DTO: Data Transfer Object(データ転送オブジェクト)
export class CreateOrderRequest {
constructor(
readonly userId: string,
readonly productIds: string[],
readonly quantities: number[]
) {}
}
export class CreateOrderResponse {
constructor(
readonly orderId: string,
readonly totalPrice: string,
readonly status: string
) {}
}
// ユースケース:注文を作成する
export class CreateOrderUseCase {
constructor(
private userRepository: IUserRepository,
private productRepository: IProductRepository,
private orderRepository: IOrderRepository,
private pricingService: PricingDomainService,
private inventoryService: InventoryDomainService,
private paymentGateway: IPaymentGateway,
private notificationService: INotificationService
) {}
async execute(request: CreateOrderRequest): Promise<CreateOrderResponse> {
// ユーザーを取得
const user = await this.userRepository.findById(
new UserId(request.userId)
);
if (!user) {
throw new Error('User not found');
}
// 商品を取得して在庫確認
const products: Product[] = [];
for (const productId of request.productIds) {
const product = await this.productRepository.findById(
new ProductId(productId)
);
if (!product) {
throw new Error(`Product ${productId} not found`);
}
products.push(product);
}
// 在庫確保
for (let i = 0; i < products.length; i++) {
this.inventoryService.reserveStock(
products[i],
request.quantities[i]
);
}
// 注文オブジェクトを生成
const order = Order.create(
user,
products,
request.quantities,
this.pricingService
);
// 決済処理
const paymentResult = await this.paymentGateway.charge(
user.getEmail(),
order.getTotalPrice()
);
if (!paymentResult.success) {
// 決済失敗時は在庫を解放
for (let i = 0; i < products.length; i++) {
this.inventoryService.releaseStock(
products[i],
request.quantities[i]
);
}
throw new Error('Payment failed');
}
// 注文を永続化
order.markAsPaid(paymentResult.transactionId);
await this.orderRepository.save(order);
// 通知を送信
await this.notificationService.sendOrderConfirmation(
user.getEmail(),
order
);
return new CreateOrderResponse(
order.getId().getValue(),
order.getTotalPrice().toString(),
order.getStatus()
);
}
}
// アプリケーションサービス
export class OrderApplicationService {
constructor(
private createOrderUseCase: CreateOrderUseCase,
// 他のユースケース...
) {}
async createOrder(request: CreateOrderRequest): Promise<CreateOrderResponse> {
return this.createOrderUseCase.execute(request);
}
}インフラストラクチャ層
リポジトリの実装
Prisma を使った実装例です。ドメイン層で定義されたインターフェースを実装します。
// ドメイン層のインターフェースを実装
export class UserRepository implements IUserRepository {
constructor(private prisma: PrismaClient) {}
async save(user: User): Promise<void> {
await this.prisma.user.upsert({
where: { id: user.getId().getValue() },
update: {
email: user.getEmail().getValue(),
name: user.getName(),
age: user.getAge(),
updatedAt: user.getUpdatedAt(),
},
create: {
id: user.getId().getValue(),
email: user.getEmail().getValue(),
name: user.getName(),
age: user.getAge(),
createdAt: user.getCreatedAt(),
updatedAt: user.getUpdatedAt(),
},
});
}
async findById(userId: UserId): Promise<User | null> {
const record = await this.prisma.user.findUnique({
where: { id: userId.getValue() },
});
if (!record) {
return null;
}
return this.toDomain(record);
}
async findByEmail(email: Email): Promise<User | null> {
const record = await this.prisma.user.findUnique({
where: { email: email.getValue() },
});
if (!record) {
return null;
}
return this.toDomain(record);
}
async findAll(): Promise<User[]> {
const records = await this.prisma.user.findMany();
return records.map((record) => this.toDomain(record));
}
async delete(userId: UserId): Promise<void> {
await this.prisma.user.delete({
where: { id: userId.getValue() },
});
}
// DB レコードからドメインエンティティへ変換
private toDomain(record: any): User {
return new User(
new UserId(record.id),
new Email(record.email),
record.name,
record.age,
record.createdAt,
record.updatedAt
);
}
}外部サービス(ポート実装)
決済ゲートウェイ、メール送信サービスなどは、アプリケーション層で定義されたポートインターフェースをインフラストラクチャ層で実装します。
// ポートインターフェース(アプリケーション層で定義)
export interface IPaymentGateway {
charge(email: string, amount: Money): Promise<PaymentResult>;
refund(transactionId: string): Promise<RefundResult>;
}
export interface PaymentResult {
success: boolean;
transactionId?: string;
errorMessage?: string;
}
// 実装(インフラストラクチャ層)
export class StripePaymentGateway implements IPaymentGateway {
constructor(private stripeClient: Stripe) {}
async charge(email: string, amount: Money): Promise<PaymentResult> {
try {
const paymentIntent = await this.stripeClient.paymentIntents.create({
amount: Math.round(amount.getAmount() * 100), // 決済額(セント)
currency: amount.getCurrency().toLowerCase(),
receipt_email: email,
});
if (paymentIntent.status === 'succeeded') {
return {
success: true,
transactionId: paymentIntent.id,
};
} else {
return {
success: false,
errorMessage: `Payment status: ${paymentIntent.status}`,
};
}
} catch (error) {
return {
success: false,
errorMessage: (error as Error).message,
};
}
}
async refund(transactionId: string): Promise<RefundResult> {
try {
const refund = await this.stripeClient.refunds.create({
payment_intent: transactionId,
});
return {
success: refund.status === 'succeeded',
refundId: refund.id,
};
} catch (error) {
return {
success: false,
errorMessage: (error as Error).message,
};
}
}
}依存性注入と DI コンテナ
DI コンテナの設計
すべての依存関係を一箇所で管理します。
// DI コンテナ
export class Container {
private instances: Map<string, any> = new Map();
private factories: Map<string, () => any> = new Map();
// シングルトンを登録
registerSingleton<T>(key: string, factory: () => T): void {
this.factories.set(key, factory);
}
// インスタンスを直接登録
registerInstance<T>(key: string, instance: T): void {
this.instances.set(key, instance);
}
// インスタンスを取得(キャッシュ)
get<T>(key: string): T {
if (this.instances.has(key)) {
return this.instances.get(key);
}
const factory = this.factories.get(key);
if (!factory) {
throw new Error(`No factory registered for key: ${key}`);
}
const instance = factory();
this.instances.set(key, instance);
return instance;
}
}
// 初期化関数
export function setupDependencies(): Container {
const container = new Container();
const prisma = new PrismaClient();
// インフラストラクチャ層
container.registerInstance('prisma', prisma);
container.registerInstance(
'stripeClient',
new Stripe(process.env.STRIPE_SECRET_KEY || '')
);
// リポジトリ
container.registerSingleton('userRepository', () => {
return new UserRepository(container.get('prisma'));
});
container.registerSingleton('productRepository', () => {
return new ProductRepository(container.get('prisma'));
});
container.registerSingleton('orderRepository', () => {
return new OrderRepository(container.get('prisma'));
});
// ドメインサービス
container.registerSingleton('pricingDomainService', () => {
return new PricingDomainService();
});
container.registerSingleton('inventoryDomainService', () => {
return new InventoryDomainService();
});
// 外部サービス
container.registerSingleton('paymentGateway', () => {
return new StripePaymentGateway(container.get('stripeClient'));
});
container.registerSingleton('emailService', () => {
return new SendgridEmailService(process.env.SENDGRID_API_KEY || '');
});
container.registerSingleton('notificationService', () => {
return new NotificationService(container.get('emailService'));
});
// ユースケース
container.registerSingleton('createOrderUseCase', () => {
return new CreateOrderUseCase(
container.get('userRepository'),
container.get('productRepository'),
container.get('orderRepository'),
container.get('pricingDomainService'),
container.get('inventoryDomainService'),
container.get('paymentGateway'),
container.get('notificationService')
);
});
// アプリケーションサービス
container.registerSingleton('orderApplicationService', () => {
return new OrderApplicationService(
container.get('createOrderUseCase')
);
});
return container;
}Antigravity エージェントによるアーキテクチャ分析
アーキテクチャ違反の自動検出
Antigravity のエージェントにコードベースを分析させ、クリーンアーキテクチャの原則に違反するコードを検出できます。
Antigravity に以下を指示:
プロジェクトの依存関係を分析して、以下を検出してください:
1. 逆向きの依存関係:
- ドメイン層がアプリケーション層、インフラストラクチャ層に依存している箇所
- ユースケース層がインターフェースアダプター層に直接依存している箇所
2. 層の越え飛ばし:
- プレゼンテーション層がドメイン層に直接依存している箇所
- インフラストラクチャ層とドメイン層が直接通信している箇所
3. 混在したアーキテクチャ:
- ビジネスロジックが HTTP ハンドラーに混在している
- DB スキーマの構造がドメインモデルに漏れている
4. ポート・アダプターパターンの違反:
- 外部ライブラリがビジネスロジックに依存している
- リポジトリインターフェースが実装に依存している
Antigravity はコードの依存グラフを生成し、以下のような結果を返します:
【アーキテクチャ違反レポート】
【危険度 HIGH】
- OrderController (Presentation層) → Order (Domain層) に直接依存
↳ 原因: OrderController で new Order() をしている
↳ 修正: CreateOrderUseCase を経由させる
- User (Domain層) → UserRepository実装 (Infrastructure層) に依存
↳ 原因: domain/entities/User.ts で infrastructure パッケージをインポート
↳ 修正: インターフェースのみインポートする
【危険度 MEDIUM】
- OrderApplicationService → Stripe(外部ライブラリ)に直接依存
↳ 原因: StripePaymentGateway 実装がアプリケーション層にある
↳ 修正: インフラストラクチャ層に移動
自動リファクタリング提案
Antigravity は違反を検出するだけでなく、修正コードを提案できます。
Antigravity に以下を指示:
src/presentation/controllers/OrderController.ts の以下の違反を修正してください:
// 現在のコード(違反)
export class OrderController {
post(req: Request, res: Response) {
const order = new Order(req.body); // ← 直接インスタンス化(違反)
// ...
}
}
修正ポイント:
1. DI コンテナから CreateOrderUseCase を取得
2. UseCase を通じて注文を作成
3. 修正後のコードを提示してください
修正後のコード:
export class OrderController {
constructor(private orderApplicationService: OrderApplicationService) {}
async post(req: Request, res: Response) {
try {
const request = new CreateOrderRequest(
req.body.userId,
req.body.productIds,
req.body.quantities
);
const response = await this.orderApplicationService.createOrder(request);
res.json(response);
} catch (error) {
res.status(400).json({ error: (error as Error).message });
}
}
}AGENTS.md によるアーキテクチャルール定義
Antigravity のエージェント制御ファイル AGENTS.md で、プロジェクト固有のアーキテクチャルールを定義できます。
# Architecture Rules for Clean Architecture + DDD
## Layer Dependencies
- domain/* は application/*, infrastructure/*, presentation/* に依存しない
- application/* は infrastructure/* に依存しない(インターフェース経由のみ)
- presentation/* は infrastructure/* に直接依存しない(アプリケーションサービス経由)
## Module Imports
- domain/ から infrastructure/ へのインポート禁止
- domain/ から external/ へのインポート禁止
- 値オブジェクトと同じファイルでビジネスロジック(メソッド)を定義すること
## Repository Pattern
- リポジトリの実装は infrastructure/repositories/ にのみ配置
- アプリケーション層は IRepository インターフェースにのみ依存
- リポジトリはドメインエンティティのライフサイクルを管理
## Dependency Injection
- すべての外部依存関係は constructor injection で注入
- グローバル変数や Service Locator パターン禁止
- コンテナの初期化は config/ で集約Antigravity はこのルールを参照して、違反を検出したり提案したりします。
まとめ
Antigravity を使ったクリーンアーキテクチャ × DDD の実装は、以下の大きなメリットを提供します:
1. 保守性の向上
- レイヤー分離により、変更の影響範囲が限定される
- ドメインロジックがビジネス要件と直結し、理解しやすい
2. テスト容易性
- ビジネスロジックを DB や外部サービスなしでテスト可能
- テストコードの実行時間が短い
3. 再利用性
- ドメインロジックを複数のインターフェース(Web、CLI、API)で再利用できる
4. スケーラビリティ
- チーム規模の拡大に対応でき、コンフリクトが少ない
- 新人開発者もアーキテクチャの全体像を理解しやすい
5. Antigravity による自動化
- アーキテクチャ違反の自動検出
- リファクタリング提案と自動修正
- 継続的な設計品質の維持
本番環境で堅牢に動作し、チーム全体で保守できるアプリケーションを目指すなら、クリーンアーキテクチャと DDD は投資する価値がある設計手法です。Antigravity のエージェント機能を活用すれば、原則を守りながら効率よく開発を進められます。