TypescriptでDDDを実装する(Step1：ATMの預金・引出し機能)

## はじめに
業務ロジックが複雑になっていき、開発生産性が低下してきているのが課題でした。
最初はさくさく進んでいたのですが、開発が進んでいくと機能間の連動の実装に時間がかかっていきます。下記図からもわかるのですが、だんだんと開発速度が低下していきます。そこで、そろそろドメインモデルに移ろうと思い、DDDの学習を始めることにしました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/599049/000f0f01-b6ff-30cb-a8fe-ebc1ddba9863.png)

## 成果物
ATMの預金と引き出し機能を実装したシンプルなものです。

```shell:ディレクトリ構成
~/develop/HITOTSU/vending_machine_ts$ tree -I node_modules 
.
├── domain
│   ├── BankAccount.ts
│   ├── BankAccountRepository.ts
│   ├── BankAccountService.ts
│   └── Cash.ts
├── index.ts
├── infra
│   └── InMemoryBankAccountRepository.ts
├── package.json
├── tsconfig.json
└── yarn.lock
```

https://github.com/kouji0705/vending_machine_ts

## クラス図

```mermaid
classDiagram
    class BankAccountService {
        - repository: BankAccountRepository
        + deposit(accountId: string, cash: Cash): void
        + withdraw(accountId: string, cash: Cash): void
        + getBalance(accountId: string): number
    }
    
    class BankAccountRepository {
        + save(bankAccount: BankAccount): void
        + find(id: string): BankAccount | null
    }
    
    class InMemoryBankAccountRepository {
        - storage: Map<string, BankAccount>
        + save(bankAccount: BankAccount): void
        + find(id: string): BankAccount | null
    }
    
    class BankAccount {
        - balance: number
        + deposit(cash: Cash): void
        + withdraw(cash: Cash): void
        + getBalance(): number
    }
    
    class Cash {
        - value: 10 | 50 | 100 | 500 | 1000
    }

    BankAccountService --> BankAccountRepository : uses
    InMemoryBankAccountRepository ..|> BankAccountRepository : implements
    BankAccountRepository --> BankAccount : manages
    BankAccount --> Cash : has

```

```shell:実行結果
~/develop/HITOTSU/vending_machine_ts$ ts-node index.ts    
Depositing 100...
Depositing cash: 100
Depositing 500...
Depositing cash: 500
Withdrawing 100...
Withdrawing cash: 100
Withdrawing 50...
Withdrawing cash: 50
Current balance: 450
```

## ソースコード
```ts:index.ts
import { Cash } from './domain/Cash';
import { BankAccount } from './domain/BankAccount';
import { BankAccountService } from './domain/BankAccountService';
import { InMemoryBankAccountRepository } from './infra/InMemoryBankAccountRepository';

// リポジトリの具体的な実装を作成
const repository = new InMemoryBankAccountRepository();

// 銀行口座を初期化してリポジトリに保存
const bankAccount = new BankAccount();
repository.save(bankAccount);

// サービスを初期化
const bankAccountService = new BankAccountService(repository);

// 銀行口座IDを設定（サンプル用に固定のIDを使用）
const bankAccountId = "default";

// 現金を預金
console.log('Depositing 100...');
bankAccountService.deposit(bankAccountId, 100 as Cash);
console.log('Depositing 500...');
bankAccountService.deposit(bankAccountId, 500 as Cash);

// 現金を出金
try {
    console.log('Withdrawing 100...');
    bankAccountService.withdraw(bankAccountId, 100 as Cash);
    console.log('Withdrawing 50...');
    bankAccountService.withdraw(bankAccountId, 50 as Cash);
} catch (error) {
    console.error(error);
}

// 残高の確認
try {
    const balance = bankAccountService.getBalance(bankAccountId);
    console.log(`Current balance: ${balance}`); // 現在の残高を表示
} catch (error) {
    console.error(error);
}
```

```ts:domain/BankAccount.ts
import { Cash } from './Cash';

class BankAccount {
    private balance: number;

    constructor() {
        this.balance = 0;
    }

    deposit(cash: Cash): void {
        console.log(`Depositing cash: ${cash}`);
        this.balance += cash;
    }

    withdraw(cash: Cash): void {
        console.log(`Withdrawing cash: ${cash}`);
        if (this.balance < cash) {
            throw new Error("Insufficient balance for withdrawal");
        }
        this.balance -= cash;
    }

    getBalance(): number {
        return this.balance;
    }
}

export { BankAccount };
```

```ts:domain/BankAccountRepository.ts
import { BankAccount } from './BankAccount';

interface BankAccountRepository {
    save(bankAccount: BankAccount): void;
    find(id: string): BankAccount | null;
}

export { BankAccountRepository };
```

```ts:domain/BankAccountService.ts
import { Cash } from './Cash';
import { BankAccountRepository } from './BankAccountRepository';

class BankAccountService {
    private repository: BankAccountRepository;

    constructor(repository: BankAccountRepository) {
        this.repository = repository;
    }

    deposit(accountId: string, cash: Cash): void {
        const account = this.repository.find(accountId);
        if (account) {
            account.deposit(cash);
            this.repository.save(account);
        } else {
            throw new Error("Bank account not found");
        }
    }

    withdraw(accountId: string, cash: Cash): void {
        const account = this.repository.find(accountId);
        if (account) {
            account.withdraw(cash);
            this.repository.save(account);
        } else {
            throw new Error("Bank account not found");
        }
    }

    getBalance(accountId: string): number {
        const account = this.repository.find(accountId);
        if (account) {
            return account.getBalance();
        } else {
            throw new Error("Bank account not found");
        }
    }
}

export { BankAccountService };
```

```ts:domain/Cash.ts
import z from "zod";

// 値オブジェクト
export const Cash = z.union([
    z.literal(10),
    z.literal(50),
    z.literal(100),
    z.literal(500),
    z.literal(1000),
]).brand<"Cash">();
export type Cash = z.infer<typeof Cash>;
```

```ts:infra/InMemoryBankAccountRepository.ts
import { BankAccount } from '../domain/BankAccount';
import { BankAccountRepository } from '../domain/BankAccountRepository';

class InMemoryBankAccountRepository implements BankAccountRepository {
    private storage: Map<string, BankAccount>;

    constructor() {
        this.storage = new Map();
    }

    save(bankAccount: BankAccount): void {
        // サンプル用に固定のIDを使用
        this.storage.set("default", bankAccount);
    }

    find(id: string): BankAccount | null {
        return this.storage.get(id) || null;
    }
}

export { InMemoryBankAccountRepository };

```