ChatGPTで、なんちゃってドメイン駆動設計(DDD)で実装する。

ちまたのあれです

## 雑に、顧客登録を実装してみる

### 「ドメイン駆動設計を用いて、指定された顧客情報を登録する機能を作ります。アプリケーションサービスのJavaのコードをください」

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/29715/9229b80a-e9f0-06a1-cdac-f48868fead99.png)

```java
package com.example.app.service;

import com.example.app.domain.customer.Customer;
import com.example.app.domain.customer.CustomerRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class CustomerService {
    private final CustomerRepository customerRepository;

    @Autowired
    public CustomerService(CustomerRepository customerRepository) {
        this.customerRepository = customerRepository;
    }

    public void registerCustomer(Customer customer) {
        // 顧客情報を検証する (名前が空白でないか、電話番号が正しい形式かなど)
        customer.validate();

        // 顧客情報をデータベースに保存する
        customerRepository.save(customer);
    }
}
```

なるほど、ドメインモデルを受け取るタイプが好きなんですね。今回は変えてみましょう。

### 「引数はプリミティブにしてください。」
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/29715/9023de68-b78f-3b3e-0d46-e2f4a3d7dc57.png)

```java
package com.example.app.service;

import com.example.app.domain.customer.CustomerRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class CustomerService {
    private final CustomerRepository customerRepository;

    @Autowired
    public CustomerService(CustomerRepository customerRepository) {
        this.customerRepository = customerRepository;
    }

    public void registerCustomer(String name, String email, String phoneNumber) {
        // 顧客情報を作成する
        Customer customer = new Customer(name, email, phoneNumber);

        // 顧客情報を検証する (名前が空白でないか、電話番号が正しい形式かなど)
        customer.validate();

        // 顧客情報をデータベースに保存する
        customerRepository.save(customer);
    }
}
```

引数で指定された値からCusotmerモデルを作成するようにしてくれました。

### 「バリデーションは、ドメインサービスにして、そのアプリケーションサービスのコードをください」

なんちゃってなので、バリデーションはドメインサービスなのかは、置いといてください。はじっこに

```java
package com.example.app.service;

import com.example.app.domain.customer.Customer;
import com.example.app.domain.customer.CustomerRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class CustomerService {
    private final CustomerRepository customerRepository;
    private final ValidationService validationService;

    @Autowired
    public CustomerService(CustomerRepository customerRepository, ValidationService validationService) {
        this.customerRepository = customerRepository;
        this.validationService = validationService;
    }

    public void registerCustomer(String name, String email, String phoneNumber) {
        // 顧客情報を作成する
        Customer customer = new Customer(name, email, phoneNumber);

        // 顧客情報を検証する (名前が空白でないか、電話番号が正しい形式かなど)
        validationService.validate(customer);

        // 顧客情報をデータベースに保存する
        customerRepository.save(customer);
    }
}

```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/29715/d8385126-9c11-1ec4-d180-a0c201627542.png)

ドメインモデルを生成してからバリデーションするスタイルは変わらないようですね。

### 「既に顧客が登録されていた場合、エラーを返却してください」

```java
package com.example.app.service;

import com.example.app.domain.customer.Customer;
import com.example.app.domain.customer.CustomerRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class CustomerService {
    private final CustomerRepository customerRepository;
    private final ValidationService validationService;

    @Autowired
    public CustomerService(CustomerRepository customerRepository, ValidationService validationService) {
        this.customerRepository = customerRepository;
        this.validationService = validationService;
    }

    public void registerCustomer(String name, String email, String phoneNumber) {
        // 顧客情報を作成する
        Customer customer = new Customer(name, email, phoneNumber);

        // 顧客情報を検証する (名前が空白でないか、電話番号が正しい形式かなど)
        validationService.validate(customer);

        // 指定されたメールアドレスのユーザーが既に存在する場合は、エラーを返却する
        if (customerRepository.existsByEmail(email)) {
            throw new CustomerAlreadyExistsException("顧客情報が既に存在します: email=" + email);
        }

        // 顧客情報をデータベースに保存する
        customerRepository.save(customer);
    }
}

```

repositoryでチェックする派みたいですね。emaliでチェックしてくれるようです。順番が微妙に感じます。

### 「ドメインモデルを生成する前に、ユーザーの重複チェックをして下さい」

雑に頼む。

```java
package com.example.app.service;

import com.example.app.domain.customer.CustomerRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class CustomerService {
    private final CustomerRepository customerRepository;

    @Autowired
    public CustomerService(CustomerRepository customerRepository) {
        this.customerRepository = customerRepository;
    }

    public void registerCustomer(String name, String email, String phoneNumber) {
        // 指定されたメールアドレスのユーザーが既に存在する場合は、エラーを返却する
        if (customerRepository.existsByEmail(email)) {
            throw new CustomerAlreadyExistsException("顧客情報が既に存在します: email=" + email);
        }

        // 顧客情報を作成する
        Customer customer = new Customer(name, email, phoneNumber);

        // 顧客情報を検証する (名前が空白でないか、電話番号が正しい形式かなど)
        customer.validate();

        // 顧客情報をデータベースに保存する
        customerRepository.save(customer);
    }
}

```

よみとってくれました。すごい。

## 雑に、機能拡張をしていく

### 「指定された顧客で、ホテルの予約をとるアプリケーションサービスのコードをください」

```Java
package com.example.app.service;

import com.example.app.domain.customer.Customer;
import com.example.app.domain.hotel.Hotel;
import com.example.app.domain.hotel.HotelRepository;
import com.example.app.domain.hotel.Reservation;
import com.example.app.domain.hotel.ReservationRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class HotelService {
    private final HotelRepository hotelRepository;
    private final ReservationRepository reservationRepository;

    @Autowired
    public HotelService(HotelRepository hotelRepository, ReservationRepository reservationRepository) {
        this.hotelRepository = hotelRepository;
        this.reservationRepository = reservationRepository;
    }

    public void reserveHotel(Customer customer, long hotelId, String date) {
        // 指定されたホテルを取得する
        Hotel hotel = hotelRepository.findById(hotelId).orElse(null);

        // 指定されたホテルが存在しない場合は、エラーを返却する
        if (hotel == null) {
            throw new HotelNotFoundException("指定されたホテルが見つかりません: hotelId=" + hotelId);
        }

        // 予約を作成する
        Reservation reservation = new Reservation(customer, hotel, date);

        // 予約をデータベースに保存する
        reservationRepository.save(reservation);
    }
}
```

orElseというオシャ技以外は、基本に忠実な実装してくれます。普通によく見る感じ。

### 「Customerではなく、顧客IDを引数にして下さい」

```java
package com.example.app.service;

import com.example.app.domain.customer.Customer;
import com.example.app.domain.customer.CustomerRepository;
import com.example.app.domain.hotel.Hotel;
import com.example.app.domain.hotel.HotelRepository;
import com.example.app.domain.hotel.Reservation;
import com.example.app.domain.hotel.ReservationRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class HotelService {
    private final CustomerRepository customerRepository;
    private final HotelRepository hotelRepository;
    private final ReservationRepository reservationRepository;

    @Autowired
    public HotelService(CustomerRepository customerRepository, HotelRepository hotelRepository,
            ReservationRepository reservationRepository) {
        this.customerRepository = customerRepository;
        this.hotelRepository = hotelRepository;
        this.reservationRepository = reservationRepository;
    }

    public void reserveHotel(long customerId, long hotelId, String date) {
        // 指定された顧客を取得する
        Customer customer = customerRepository.findById(customerId).orElse(null);

        // 指定された顧客が存在しない場合は、エラーを返却する
        if (customer == null) {
            throw new CustomerNotFoundException("指定された顧客が見つかりません: customerId=" + customerId);
        }

        // 指定されたホテルを取得する
        Hotel hotel = hotelRepository.findById(hotelId).orElse(null);

        // 指定されたホテルが存在しない場合は、エラーを返却する
        if (hotel == null) {
            throw new HotelNotFoundException("指定されたホテルが見つかりません: hotelId=" + hotelId);
        }

        // 予約を作成する
        Reservation reservation = new Reservation(customer, hotel, date);

        // 予約をデータベースに保存する
        reservationRepository.save(reservation);
    }
}
```

顧客情報をRepositoryから取得してくれるようになりました。

### 「指定された期限に迫った予約があった場合に、通知サービスを利用して通知するようなアプリケーションサービスのコードをください」

```java
package com.example.app.service;

import com.example.app.domain.hotel.Reservation;
import com.example.app.domain.hotel.ReservationRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.time.LocalDate;
import java.util.List;

@Service
public class NotificationService {
    private final ReservationRepository reservationRepository;
    private final NotificationClient notificationClient;

    @Autowired
    public NotificationService(ReservationRepository reservationRepository, NotificationClient notificationClient) {
        this.reservationRepository = reservationRepository;
        this.notificationClient = notificationClient;
    }

    public void sendDepartureNotice() {
        // 二日前の日付
        LocalDate twoDaysBefore = LocalDate.now().minusDays(2);

        // 二日前の出発日を持つ予約の一覧を取得する
        List<Reservation> reservations = reservationRepository.findByDepartureDate(twoDaysBefore);

        // 二日前の出発日を持つ予約が存在する場合は、通知サービスを利用して通知する
        if (!reservations.isEmpty()) {
            for (Reservation reservation : reservations) {
                // 予約情報から通知内容を作成する
                String message = createMessage(reservation);

                // 顧客に通知を送信する
                notificationClient.send(reservation.getCustomer().getEmail(), message);
            }
        }
   }

```
LocalDateがかっこよくない。ので、頼んでみます。

### 「日付は出発日というドメインモデルにして下さい」

```java
package com.example.app.service;

import com.example.app.domain.hotel.DepartureDate;
import com.example.app.domain.hotel.Reservation;
import com.example.app.domain.hotel.ReservationRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class NotificationService {
    private final ReservationRepository reservationRepository;
    private final NotificationClient notificationClient;

    @Autowired
    public NotificationService(ReservationRepository reservationRepository, NotificationClient notificationClient) {
        this.reservationRepository = reservationRepository;
        this.notificationClient = notificationClient;
    }

    public void sendDepartureNotice() {
        // 二日前の出発日を表すドメインモデル
        DepartureDate twoDaysBefore = DepartureDate.twoDaysBefore();

        // 二日前の出発日を持つ予約の一覧を取得する
        List<Reservation> reservations = reservationRepository.findByDepartureDate(twoDaysBefore);

        // 二日前の出発日を持つ予約が存在する場合は、通知サービスを利用して通知する
        // ...
    }
}
```

おしゃれドメインモデル爆誕。

最後に仕上げ。

### 「AWSのEventbridgeとLambdaで、1日間隔でWebAPIをコールするPythonのコードをください。」

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/29715/76d96bbf-81ee-0026-f8bb-e611a2594911.png)

スケジュールを仕掛けて終わりです。

# 所感

~~かたちだけおぼえてるだけのDDDじゃ、もうかちがなくなりそう~~


DDDあるある「参考実装が探せない。」みたいな時に、普通に有用な感じがします。参考実装やドメインモデルのアイディアを考える時にぼーっと遊んでると結構使えるかも良いかなと思いました。
ただし、びしっとした答えではない時も多く、この辺りは使う側の力量も必要そうです。

あと、"ドメイン駆動設計"という言葉、AIプログラミング界の新海誠になれそうですね。っぽさがすごく出しやすい感じでした。