はじめに
最近、Fluent Interfaceについて学びながら、このデザインパターンが何であるのか、
そしてどのような場合に使用すべきなのか、自分の考えを整理してみました。
- 韓国人として、日本語とコンピュータの勉強を同時に行うために、ここに文章を書いています
- 翻訳ツールの助けを借りて書いた文章なので、誤りがあるかもしれません
Fluent Interface
Fluent Interface とは
- Fluent Interface(フルーエントインターフェイス)は、メソッドチェインを活用して、オブジェクトの設定や動作を直感的で自然な文章のように表現できるAPIスタイルです
このAPIスタイルは、まるでドメイン固有言語(Domain Specific Language, DSLに似た形態をとります。
マーティン・ファウラーは自身のブログで以下のように述べています。
Probably the most important thing to notice about this style is that the intent is to do something along the lines of an internal DomainSpecificLanguage. Indeed this is why we chose the term 'fluent' to describe it, in many ways the two terms are synonyms. The API is primarily designed to be readable and to flow.
このスタイルで最も重要なのは、内部ドメイン固有言語の流れに沿って何かを実行しようとする意図があるという点です。
実際、この点から私たちはこれを説明するために「フルーエント」という用語を選びました。
両者は多くの点で同義語と言えるでしょう。
このAPIは主に可読性が高く、流れるように設計されています。
DSL - Domain Specific Language(ドメイン固有言語)
- ドメイン固有言語とは、特定の分野に最適化されたコンピュータ言語のことです。
- もう少し簡単に言えば、ドメイン(問題領域)を表現することに特化した言語と考えると良いでしょう
その例として、SQL
を挙げます。
SELECT grade FROM students WHERE grade > 3;
次のような構文は、「学生」の中から「3学年以上」の学生を検索するという表現をそのまま理解できます。
まるで文章のように、そのドメインを表現しています。
SQL
を使って一般的なプログラムを作ることはできませんが、このようにデータベースから特定の条件でデータを検索することに特化したドメイン固有言語です。
どのようなものか、コードで確認してみましょう。
コードで見てみよう
一般的なAPIコード
public class Student {
private String name;
private Intger grade;
public Student(String name, Integer grade){
this.name = name;
this.grade = grade;
}
}
次のように学生が定義されており、
public class ClassRoom {
private List<Student> students = new ArrayList<>();
public void addStudent(Student student) {
this.students.add(student);
}
次のように学級が定義されている場合、
ClassRoom classroom = new ClassRoom();
Student student1 = new Student("John", 1);
classroom.addStudent(student1);
Student student2 = new Student("Emily", 2);
classroom.addStudent(student2);
// 複数の学生を追加するには、それぞれ個別に追加する必要があります
Student student3 = new Student("Michael", 1);
Student student4 = new Student("David", 2);
classroom.addStudent(student3);
classroom.addStudent(student4);
次のように作業を行います。
Fluent Interface APIへの転換
public class Student {
private String name;
private Integer grade;
public Student(String name, Integer grade) {
this.name = name;
this.grade = grade;
}
}
学生は以前と同じです。
Fluent Interfaceに変更する部分は、まさにこのClassRoom
クラスです。
public class ClassRoom {
private List<Student> students = new ArrayList<>();
// メソッドチェイニングを実現するためにClassRoomを返すよう変更
public ClassRoom addStudent(String name, Integer grade) {
students.add(new Student(name, grade));
return this;
}
// 既存のメソッドをチェイニング可能に変更
public ClassRoom addStudent(Student student) {
students.add(student);
return this;
}
}
以下のように、自身を返すようにメソッドを設定しました。
では、定義した学級クラスと学生クラスを使用してみましょう。
ClassRoom classroom = new ClassRoom()
.addStudent("John", 1)
.addStudent("Emily", 2)
.addStudent("Michael", 1);
// Studentオブジェクトを明示的に使用
classroom
.addStudent(new Student("David", 2))
.addStudent(new Student("Chris", 1));
次のように変化しました。
以前のコードに比べ、はるかに文章のような形になり、ドメインをより理解しやすくなりました。
変化した点
どの点が変わったのでしょうか?
それはaddStudent()
の使用方法です。
一般的なコーディングスタイルでは、アクセサメソッドは基本的にvoid
を返します。
これはコマンド・クエリ分離の原則(Command Query Separationに従っています。
しかし、Fluent Interfaceの形式では、void
の代わりに自分自身を返す規則に従うことで、
一般的なコーディングスタイルとは異なることが確認できました。
マーティン・ファウラーのブログには、次のような一文があります。
Building a fluent API like this leads to some unusual API habits. One of the most obvious ones are setters that return a value. (In the order example with adds an order line to the order and returns the order.) The common convention in the curly brace world is that modifier methods are void, which I like because it follows the principle of CommandQuerySeparation. This convention does get in the way of a fluent interface, so I'm inclined to suspend the convention for this case.
このようなFluent APIを構築することは、通常とは異なるAPI習慣を生み出します。その最も顕著な例の1つが、値を返すセッターメソッドです。(注文の例では、注文ラインを注文に追加し、注文を返す形になります。)中括弧を多用するプログラミングの世界では、修飾メソッドがvoid
を返すのが一般的な慣習です。この慣習はコマンド・クエリ分離の原則(CommandQuerySeparation)に従っているので私は好んでいます。しかし、この慣習はFluent Interfaceの妨げになるため、このケースでは例外として適用しない傾向があります。
Fluent Interfaceの利点
Fluent Interfaceを用いてコードを書くことで得られる利点は、大きく分けて3つあると考えられます。
- コードを文章のように記述することで、明確で理解しやすいコードを作成できる
- 特定のドメインに合わせた表現が可能になる
- IDEの自動補完機能を活用することで、次に呼び出すメソッドを簡単に理解でき、ミスを防ぐことができる
このように、次に呼び出すべきメソッドをIDEの自動補完機能を通じて簡単に推測できます。
Fluent Interfaceの欠点
Fluent Interfaceには致命的な欠点も存在します。
- コマンド・クエリ分離の原則(Command Query Separation)の違反
- 独立したメソッドの可読性が低い
- ドメインに依存したAPIによる実装の複雑性の増加
私は以下のような問題点を考えました。
これを1つずつ見ていきましょう。
コマンド・クエリ分離の原則(Command Query Separation)の違反
public ClassRoom addStudent(String name, Integer grade) {
students.add(new Student(name, grade));
return this;
}
現在のコードでは、命令(状態の変更)とクエリ(オブジェクトの返却)が1つのメソッドで同時に実行されています。
その結果、メソッドがどのような動作をするのかが不明確になり、予期しない動作が発生する可能性があります。
さらに、1つのメソッドで命令とオブジェクトの返却が同時に行われることで、単一責任の原則(Single Responsibility Principle, SRP)に違反しています。
もしClassRoomにさらに多くの動作が追加される場合、保守性や拡張性に悪影響を与え、1つの動作が追加されるたびにすべてのメソッドを変更しなければならない可能性があります。
コマンド・クエリ分離の原則(Command Query Separation)とは?
コマンド・クエリ分離の原則は、通常「CQS」とも呼ばれます。
CQSはBertrand Meyerによって提案された原則で、プログラミングにおいて命令とクエリを分離することを強調します。
特に単一責任の原則(Single Responsibility Principle, SRP)のように、1つのメソッドが1つの動作(責任)のみを持つことを目指します。
独立したメソッドの可読性が低い
現在のコードはシンプルなので問題が明確には見えませんが、
追加の動作が増えた場合はどうなるでしょうか?
public class Student {
// 以前と同様のプロパティやメソッド
private List<String> subjects;
public Student learn(String subject) {
this.subjects.add(subject);
return this;
}
public Student and(String subject) {
this.subjects.add(subject);
return this;
}
}
例えば、学生が受講している科目を表示するメソッドが追加された場合、
以下のように記述されます。
Student student = new Student("John", 1)
.learn("Science")
.and("Math")
.and("English");
非常に自然に読み取ることができます。
学生(John, 1)が勉強する 科学、数学、英語を
このように文章のように表現されると、非常に理解しやすくなります。
しかし、これを単独で見るとどうでしょうか?
public Student and(String subject) {
this.subjects.add(subject);
return this;
}
このメソッドはその意味が曖昧です。
外部から見ると、student.add("科学")
の形式になっていますが、
何を追加しているのか、何のために存在するのかが非常に分かりにくいです。
自然な文章を追求する一方で、「明確な動作」を失ってしまったFluent Interfaceと言えます。
ドメインに依存したAPI、それによる実装の複雑性の増加
以前に追加した以下のメソッドは、非常にドメインに依存しています。
public Student learn(String subject) {
this.subjects.add(subject);
return this;
}
public Student and(String subject) {
this.subjects.add(subject);
return this;
}
もしビジネスルールとして、受講登録時に検証ロジックが必要になった場合はどうでしょうか?
public Student learn(String subject) {
if (isAlreadyRegistered(subject)) {
throw new IllegalArgumentException("既に登録されている科目です: " + subject);
}
if (subjects.size() >= 10) {
throw new IllegalArgumentException("最大登録可能科目数を超えています");
}
subjects.add(subject);
return this;
}
// and メソッドにも同じ検証ロジックが重複しています
public Student and(String subject) {
if (isAlreadyRegistered(subject)) {
throw new IllegalArgumentException("既に登録されている科目です: " + subject);
}
if (subjects.size() >= 10) {
throw new IllegalArgumentException("最大登録可能科目数を超えています");
}
subjects.add(subject);
return this;
}
private boolean isAlreadyRegistered(String subject) {
return subjects.contains(subject);
}
その結果、複雑性はさらに増大することが明らかです。
では、解決策を考えてみましょう。
マーティン・ファウラーは次のように述べています。
- Fluent Interfaceは値オブジェクト(Value Object)により適している
- 複雑なドメインロジックは、別のビルダーオブジェクトに分離するのが良い
// 従来の曖昧な方法
public class Student {
public Student with(String subject) { ... }
}
// ビルダーオブジェクトを使用した明確な方法
public class StudentBuilder {
private Student student;
public SubjectBuilder withSubject(String subject) {
// 登録関連ロジックを担当する別ビルダーに移動
return new SubjectBuilder(student);
}
}
public class SubjectBuilder {
private Student student;
public SubjectBuilder and(String subject) {
// 科目追加関連ロジック
return this;
}
}
次のように分離することで、それぞれの責任がより明確になるでしょう。
public class CourseRegistration { // 受講登録は値オブジェクト
private final List<String> subjects; // 不変データ
public CourseRegistration with(String subject) {
// 新しいオブジェクトを生成して返却
return new CourseRegistration(addSubject(subject));
}
}
また、次のようにValue Objectで使用する場合、
既存のオブジェクトの値を変更せず、新しいオブジェクトを返すというValue Objectの原則に適合します。
実際にはどのように適用され、使用されているのでしょうか?
さまざまな場面でFluent Interfaceを確認することができます。
Spring Security
多くの部分で自身を返す設計が採用されています。
以下のコードはその一例です。
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
.cors(cors -> {})
.authorizeHttpRequests(auth -> auth
.requestMatchers(ALLOWED_PATHS).permitAll()
.requestMatchers("/admin/**", "/v3/api-docs/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.exceptionHandling(exceptionHandling -> exceptionHandling
.authenticationEntryPoint(new SwaggerAuthenticationEntryPoint())
)
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
)
.formLogin(AbstractHttpConfigurer::disable)
.httpBasic(AbstractHttpConfigurer::disable)
.addFilterBefore(new JwtFilter(tokenProvider), UsernamePasswordAuthenticationFilter.class);
return http.build();
}
こちらは、私が使用しているSpring Securityの設定コードです。
セキュリティフィルタチェインを作成する際、次のようにFluent Interfaceを適用することで、
設定の流れを文章のように直感的に把握できることが確認できました。
また、メソッドチェイニングを活用することで、開発者のミスを防ぎ、
IDEの自動補完機能を利用して使用可能なメソッドを簡単に確認できます。
JavaのStringBuilderメソッド
append()
メソッドがFluent Interfaceの構造を採用していることが確認できます。
このように、一時変数を使用せず、連続した1つの文章として表現することが可能です。
結論
私が導き出した結論は、結局のところトレードオフであるということです。
前述のように、Fluent Interfaceには明確な長所と短所があります。
- ドメインが重要である場合
- 順序性が必要な場合
- 明確に意味を伝えたい場合
これらの場合には、Fluent Interfaceを使用するのが適切でしょう。
一方で、以下のような場合には混乱を招く可能性があります。
- 複雑で手続き的な処理が必要な場合(前述のビジネスロジックの検証コードのように)
- ドメインが曖昧なロジックの場合
Fluent Interfaceを導入する際には、一度真剣に検討し、慎重に判断する必要があると感じます。