LoginSignup
1
1

More than 1 year has passed since last update.

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

Last updated at Posted at 2022-07-31

概要

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

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

Factory Methodとは?

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

Factory Methodがある場合

(コーヒーショップにて)

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

(同じコーヒーショップにて)

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

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

(コーヒーショップにて)

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

(同じコーヒーショップにて)

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

Factory Methodがない場合

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

まとめると

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

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

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

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

実際に実装してみる

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

FactoryMethodを使わないパターン

@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を実装したクラスを実装してみます。
可変な部分を引数で受け取るだけで実装は同じです。

@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を使うように変更します。

@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で受け取ります。

@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を使ってインスタンス生成を行います。

@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();
    }
}

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

@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");
    }
}
1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1