はじめに
良いコード/悪いコードで学ぶ設計入門を読んでいて、「ストラテジーパターンってこんなに便利なのか!」と驚いたのでまとめてみます。
0. ストラテジーパターンとは
すごい大雑把に言うと、ifやwhenなどを使わず、interfaceを用いることで、条件分枝処理を実装するような設計です。詳しい説明やクラス図についてはTECHSCOREさんやIT専科さんに書いてありますので、そちらを見てくださいね。
今回は、映画館で料金(大人:1800, 子供:1000, ペア:2500)とプレゼント(大人:プレゼントなし, 子供:ぬいぐるみ, ペア:花束)を返却するような処理を考えてみます。
1. ストラテジーパターンなしで実装するとどうなるか
ストラテジーパターンなしでの実装例です。
class Cinema(val type: Type){
fun price(): Int {
val price =
when(type){
Type.ADULT -> 1800
Type.KID -> 1000
Type.PAIR -> 2500
}
return price
}
fun present() {
when(type){
Type.ADULT -> println("なし")
Type.KID -> println("ぬいぐるみ")
Type.PAIR -> println("花束")
}
}
}
enum class Type{
ADULT,
KID,
PAIR
}
fun main() {
val cinema = Cinema(Type.PAIR)
val price = (cinema.price())
println(price)
cinema.present()
}
この程度の処理でしたら、正直なところストラテジーパターンを使う必要はないかもしれません。
しかし、属性(type)が"シニア", "大人", "大学生", "小中高生", "幼稚園以下", "ペア", "特別パスポート会員" and more...と増え、メソッドもprice(), present(), numberOfSeat() and more...と増えると話は別です。あっという間に下のようなクソコードが出来上がるでしょう。
class Cinema(val type: Type){
fun price(): Int {
val price =
when(type){
Type.SENIOR -> 1500
Type.ADULT -> 1800
Type.UNIVERSITY_STUDENT -> 1000
Type.KID -> 500
Type.BABY -> 0
Type.PAIR -> 2500
Type.SPECIAL -> 1000
}
return price
}
fun present() {
when(type){
Type.SENIOR -> println("なし")
Type.ADULT -> println("なし")
Type.UNIVERSITY_STUDENT -> println("うまい棒")
Type.KID -> println("ぬいぐるみ")
Type.BABY -> println("ぬいぐるみ")
Type.PAIR -> println("花束")
Type.SPECIAL -> println("記念写真")
}
}
fun numberOfSeat(): Int {
val numberOfSeat =
when(type){
Type.SENIOR -> 1
Type.ADULT -> 1
Type.UNIVERSITY_STUDENT -> 1
Type.KID -> 1
Type.BABY -> 0
Type.PAIR -> 2
Type.SPECIAL -> 1
}
return numberOfSeat
}
}
これでは
①コードの重複が多数あるため、属性が追加された時に実装漏れが起こりやすい
②メソッドが追加されるたびに、重複コードが生じる
など保守性に乏しいコードになってしまいます。ちなみに上のコードだと、presentメソッドでType.SPECIALを実装し忘れたとしてもコンパイルは通ってしまいます。あな恐ろしや恐ろしや。
そこでストラテジーパターンの登場です。
2. ストラテジーパターンを用いて実装するとどうなるか
ストラテジーパターンでは、アルゴリズムをStrategyインターフェイスに記述し、これを属性毎の具体的なStrategyクラスで実装します。
interface Strategy{
fun price(): Int
fun present()
}
class AdultStrategy: Strategy{
override fun price(): Int {
return 1800
}
override fun present(){
println("なし")
}
}
class KidStrategy: Strategy{
override fun price(): Int {
return 1000
}
override fun present(){
println("ぬいぐるみ")
}
}
class PairStrategy: Strategy{
override fun price(): Int {
return 2500
}
override fun present(){
println("花束")
}
}
その後、Strategyを切り替え役をContextクラスで行います。
class Context(val strategy: Strategy){
fun price(): Int {
return strategy.price()
}
fun present() {
return strategy.present()
}
}
fun main() {
val cinema = Cinema(AdultStrategy())
val price = (cinema.price())
println(price)
cinema.present()
}
上ではmain()でAdultStrategyをインスタンス化していますが、以下のようにmapを使えばenumを渡すだけで処理ができます。
fun main() {
val adultStrategy = AdultStrategy()
val kidStrategy = KidStrategy()
val pairStrategy = PairStrategy()
val maps = hashMapOf<Type, Strategy>()
maps.put(Type.ADULT, adultStrategy)
maps.put(Type.KID, kidStrategy)
maps.put(Type.PAIR, pairStrategy)
val context = Context(maps.get(Type.ADULT) ?: adultStrategy)
val price = (context.price())
println(price)
context.present()
}
Strategyパターンでは、アルゴリズムをStrategyインターフェイスに記述するということがミソです。
属性が増えた場合、具体的なStrategyクラスを作成してインターフェースを実装すれば良いので、機能追加が簡単になり、コードの修正漏れも大幅に減少します!
参考にさせていただきました
https://zenn.dev/nekoniki/articles/396ebc523930c196dc13
ここ間違っているよーなど、コメントありましたら、ぜひお願いします!