現場でよく見るデザインパターン　〜FactoryMethodパターン〜

# 概要

デザインパターンの「Factory Method」について、現場で見かけた実装をベースにどんなものなのかまとめてみます。

> デザインパターンで提唱されているものと正確には違うと思いますが、こんな考え方もありかくらいで読んでいただけると😇

# Factory Methodとは？

まずは日常で置き換えてFactory Methodがある場合とない場合を比べてみます。

## Factory Methodがある場合

> （コーヒーショップにて）

　お客さん：「コーヒーの **S** サイズください」
　店員さん：「かしこまりました」

> （同じコーヒーショップにて）

　お客さん：「コーヒーの **M** サイズください」
　店員さん：「かしこまりました」

## Factory Methodがあるけど、作りがイケてない場合

> （コーヒーショップにて）

　お客さん：「コーヒーの **S** サイズください」　
　店員さん：「かしこまりました」

> （同じコーヒーショップにて）

　お客さん：「コーヒーの **M** サイズください」
　店員さん：「Mサイズは注文は〇〇店でお願いします」
　お客さん：「🤯」

## Factory Methodがない場合

　自分「コーヒーが飲みたいなぁ」
　自分「じゃあまずはコーヒー豆の栽培... orz」

## まとめると

Factory Methodが完全にない場合では、
目的のコーヒー（インスタンス）を作るために１から作業をしなければならず、
コストもかかりますし、正確な手順を覚えなければなりません。。。
こんなのやってやれませんw

Factory Methodがあるけど、作りがイケてない場合では、
お店側は同じコーヒー（クラス）でも別のサイズ（インスタンス）を提供するごとにお店（メソッド）を増やさなければならず、お客さんもサイズごとに行くお店を分けなければなりません。
これでもまだやってられませんw

Factory Methodがある場合だと、
同じコーヒー（クラス）であれば、同じお店（メソッド）で、サイズ（引数）だけ伝えれば良いので、お店側もお客さん側も楽ですね！

つまり、FacotryMethodパターンを使うと
利用者側は「必要な情報さえ渡せば、欲しいインスタンスが手に入る」ようになります！
※提供側も「提供方法を自由に変更できる」ようになります！

# 実際に実装してみる

良くあることとして、HTTP通信に使うクラス（RestTemplateとかWebClientとか）を通信先ごとに生成するケースを考えています。

## FactoryMethodを使わないパターン

```java
@Configuration
public class WebClientAutoConfiguration {
    @Bean
    public WebClient webClientForA() {
        var httpClient = HttpClient.create()
                .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 1_000)
                .responseTimeout(Duration.ofMillis(5_000));

        return WebClient.builder()
                .baseUrl("baseurlA")
                .clientConnector(new ReactorClientHttpConnector(httpClient))
                .build();
    }

    @Bean
    public WebClient webClientForB() {
        var httpClient = HttpClient.create()
                .responseTimeout(Duration.ofMillis(10_000));

        return WebClient.builder()
                .baseUrl("baseurlB")
                .clientConnector(new ReactorClientHttpConnector(httpClient))
                .build();
    }
}
```

まぁ普通に実装はできますが、、、

- 通信先が増える度に同じような実装が増えていく
- 通信先ごとに絶対実装して欲しいことが漏れる可能性がある（サンプルだとBの方がコネクションタイムアウトの設定漏れてたり　※しかもパッと見気付けない）
- 共通の設定（Interceptorとか）を増やそうとしたらそれぞれに修正をする必要がある

## FactoryMethodを使うパターン

まずはFacotryMethodを実装したクラスを実装してみます。
可変な部分を引数で受け取るだけで実装は同じです。

```java
@Component
public class WebClientFactory {
    public WebClient create(
            String baseurl,
            Duration readTimeout,
            Duration connectTimeout
    ) {
        var httpClient = HttpClient.create()
                .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, (int)connectTimeout.toMillis())
                .responseTimeout(readTimeout);

        return WebClient.builder()
                .baseUrl(baseurl)
                .clientConnector(new ReactorClientHttpConnector(httpClient))
                .build();
    }
}
```

利用側はFactoryMethodを使うように変更します。

```java
@Configuration
@RequiredArgsConstructor
public class WebClientAutoConfiguration {
    private final WebClientFactory webClientFactory;
    
    @Bean
    public WebClient webClientForA() {
        return webClientFactory.create(
                "baseurlA",
                Duration.ofMillis(1_000),
                Duration.ofMillis(5_000)
        );
    }

    @Bean
    public WebClient webClientForB() {
        return webClientFactory.create(
                "baseurlB",
                Duration.ofMillis(2_000),
                Duration.ofMillis(10_000)
        );
    }
}
```

FactoryMethodに細かい実装が隠蔽されたことでだいぶスッキリしました！！！

# SpringBootで

Beanに登録するようなものだと設定値がpropertyから読み込めると便利ですよね！

では早速
まず設定値をpropertyに移動します。

```application.yaml
my-pj:
  web-client:
    a:
      baseurl: "baseurlA"
      read-timeout: 1s
      connect-timeout: 5s
    b:
      baseurl: "baseurlB"
      read-timeout: 2s
      connect-timeout: 10s
```

次にpropertyを読み込むクラスを用意します。
接続先ごとのpropertyはMapで受け取ります。

```java
@Data
@ConfigurationProperties(prefix = "my-pj")
public class ApplicationProperties {
    private Map<String, WebClientProperties> webClient;

    @Data
    public static class WebClientProperties {
        private String baseurl;
        private Duration readTimeout;
        private Duration connectTimeout;
    }
}
```

次にFactoryMethodの実装クラスを修正します。
引数で設定値を受け取る代わりにどのキーを受け取り、
キーに紐づくpropertyを使ってインスタンス生成を行います。

```java
@Component
@RequiredArgsConstructor
@EnableConfigurationProperties(ApplicationProperties.class)
public class WebClientFactory {
    private final ApplicationProperties webClientProperties;

    public WebClient create(String name) {
        // 設定値はPropertiesクラスから受け取る
        var property = webClientProperties.getWebClient().get(name);

        var httpClient = HttpClient.create()
                .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, (int)property.getConnectTimeout().toMillis())
                .responseTimeout(property.getReadTimeout());

        return WebClient.builder()
                .baseUrl(property.getBaseurl())
                .clientConnector(new ReactorClientHttpConnector(httpClient))
                .build();
    }
}
```

最後に利用クラスです。
キーを指定するだけなのでめちゃめちゃスッキリしました✨✨✨

```java
@Configuration
@RequiredArgsConstructor
public class WebClientAutoConfiguration {
    private final WebClientFactory webClientFactory;

    @Bean
    public WebClient webClientForA() {
        return webClientFactory.create("a");
    }

    @Bean
    public WebClient webClientForB() {
        return webClientFactory.create("b");
    }
}
```