個人的な経験として、要素の似通ったオブジェクトを特定の条件で判別し、判別結果ごとで異なる処理を実装する際、その部分のコードは可読性が低く、のちの修正でバグが生じがちになることが多々ありました。
例えば、以下のようなユースケースの実装をする場合にそれが当てはまります。
ユースケース
社内の社員の情報を取得し、部署ごとに社員の情報を出力する機能の作成
- 社員は名前、生年月日、部署の情報を持つ
- 部署は事務員、エンジニア
- 事務員は社員の情報に加えて
特技の情報を持つ - エンジニアは社員の情報に加えて
プログラミング言語の情報を持つ - 社員の情報を出力する際、事務員・エンジニアはそれぞれ自分の持つ情報のみを出力する(事務員が
プログラミング言語を出力してはいけない。エンジニアは特技を出力してはいけない)
とりあえずユースケースを実装してみる
とりあえず何も意識しないで上記のユースケースを実装してみます(使用言語はKotlin)
社員を表す社員クラスは以下です
data class Employee(
val name: String,
val birth: LocalDate,
val position: String,
val skill: String?,
val programmingLanguage: String?,
)
positionにはそれぞれの部署が入り、事務員だったらskillに特技、エンジニアだったらprogrammingLanguageに使用言語が入ります。
この社員クラスを使用してユースケースを満たすプログラムは以下です。
// 事務員の情報を出力
fun introduceClerk(employee: Employee) {
println("事務員, 名前: ${employee.name}, 生年月日: ${employee.birth}, 特技: ${employee.skill}")
}
// エンジニアの情報を出力
fun introduceEngineer(employee: Employee) {
println("エンジニア, 名前: ${employee.name}, 生年月日: ${employee.birth}, 使用するプログラミング言語: ${employee.programmingLanguage}")
}
fun main() {
// 社員情報を取得
val employee = getEmployee()
when(employee.position) {
"Clerk" -> introduceClerk(employee)
"Engineer" -> introduceEngineer(employee)
else -> throw Error("そんな部署はない")
}
}
上記のプログラムには以下の問題があります
- 社員クラスに部署によっては不要なプロパティが存在する
- もし部署が増えた場合、判別の抜け漏れが発生する1
- 部署ごとに社員の情報を出力するメソッドに異なる部署の社員が渡されてもエラーとならない
上記の問題に対してどうすれば良いか?
代数的データ型の和集合を使用したデータ型の使用することで上記の問題の解決することができます。
代数的データ型 is 何
代数的データ型(Algebraic Data Type, ADT)とは、代数学が扱う集合概念をデータ型に応用したもの。
積集合と和集合の二つがある
- 積集合
全ての要素が含まれている構造をさす(JavaやKotlinでいうところのクラス) - 和集合
いずれかの要素が含まれている構造を指す ← 今回使用する
和集合のデータ型を使用してもう一度ユースケースの実装する
クラスに和集合を適用したい場合、Kotlinであればsealed interface/class2が使用できます。実際に和集合を適用した場合、社員のデータは以下のようにあらわすことができます。
sealed interface Employee{
val name: String
val birth: String
data class Clerk(
override val name: String,
override val birth: LocalDate,
val skill: String,
): Employee
data class Engineer(
override val name: String,
override val birth: LocalDate,
val programmingLanguage: String,
): Employee
}
上記は名前と生年月日を持つ社員インターフェースが存在し、それを継承して特技を持つ事務員とプログラミング言語を持つエンジニアが存在していることを表しています。
これによって、社員クラスに部署によっては不要なプロパティが存在するという問題は解決しました。
次にこの社員インターフェースと部署ごとのクラスを使用し、ユースケースを実装してみます。
// 事務員の情報を出力
fun introduceClerk(employee: Employee.Clerk) {
println("事務員, 名前: ${employee.name}, 生年月日: ${employee.birth}, 特技: ${employee.skill}")
}
// エンジニアの情報を出力
fun introduceEngineer(employee: Employee.Engineer) {
println("エンジニア, 名前: ${employee.name}, 生年月日: ${employee.birth}, 使用するプログラミング言語: ${employee.programmingLanguage}")
}
fun main() {
// 社員情報を取得
val employee = getEmployee()
when(employee) {
is Employee.Clerk -> introduceClerk(employee)
is Employee.Engineer -> introduceEngineer(employee)
}
}
上記のコードを見てわかる通り、和集合のデータ型を使用することで使用前の実装で発生していた他の二つの問題も解決できます。
- もし部署が増えた場合、判別の抜け漏れが発生する
→ 部署のクラスでパターンマッチを行うため、もし新たな部署クラスを追加し、その部署の処理の実装を忘れてもコンパイルエラーとなり事前に気づくことができる - 部署ごとに社員の情報を出力するメソッドに異なる部署の社員が渡されてもエラーとならない
→ 部署ごとの処理にはそれぞれの部署クラスを引数の型として指定しているため、部署ごとに社員の情報を出力するメソッドに異なる部署の社員が渡された場合はコンパイルエラーとなり事前に気づくことができる
まとめ
オブジェクトのクラスに代数的データ型の和集合を適用することで以下のメリットを得られることが分かりました。
- 必要なプロパティだけが存在するクラスを作成できる
- オブジェクトの判別の抜け漏れを防げる(あっても実行前にわかる)
- 意図したオブジェクトをメソッドに渡すことができる(あっても実行前に分かる)
sealedを利用したクラスはややこしいと感じるかもしれませんが、保守においてその効果が分かると思います。ぜひ使用してみてください。
-
Kotlinの場合、部署にEnum Classを使用するとこの問題は防げる ↩