概要
デザインパターンの「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に移動します。
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");
}
}