Featured image of post コードウィザード【Strategy】会計呪文に割引を継ぎ足すたび、符が増えて式が暴れ出す〜割引の算法を一枚の呪符に切り出し、差し替え可能にする〜

コードウィザード【Strategy】会計呪文に割引を継ぎ足すたび、符が増えて式が暴れ出す〜割引の算法を一枚の呪符に切り出し、差し替え可能にする〜

収穫祭の前夜、雑貨店の会計呪文に割引券を一枚足すと、季節割引のはずの客に別の割引が紛れ込んだ。見習い魔術師ルークは継承の大魔法陣で盛って自滅し、師匠の問いから割引の算法を一枚の呪符に切り出すStrategyへ。本体を変えず割引を足せる設計をKotlinで描く。

収穫祭の前夜、釣り銭が合わない

私はマレン。街角で小さな雑貨店を営んでいる。糸も蝋燭も、鍋も箒も、暮らしに要るものを少しずつ並べて、ついでに安物の魔法道具も置いている——そういう、どこにでもある店だ。

商売を続けるうちに、割引の数は自然と増えていった。季節ごとのセール、長く通ってくださる常連さんへのお礼の割引、祭りのときに配る割引券。ただ、ウチには昔からの決めごとがある。一度の会計につき、当てる割引はひとつだけ。お客ごとに、そのとき一番お得になる割引をひとつ選んで差し引く。あれもこれもと重ねていたら、いくら値引きしているのか自分でも分からなくなってしまうからだ。

その「どの割引を当てるか」を計算してくれるのが、会計の水晶に仕込んだ会計呪文だった。私は魔術の心得などまるでないが、商売を広げるたびに、見よう見まねで割引の計算をこの一つの符に少しずつ書き足してきた。継ぎ足すたびにちゃんと動いていたから、何も疑っていなかった。

ところが、収穫祭を明後日に控えたその晩、新しく配る割引券の分を符に書き足したとたん、会計がおかしくなった。季節割引を当てたはずのお客に、なぜか割引券のほうの値引きが出る。書き足したはずの一行が、関係ないはずのお客にまで紛れ込んでいる。気づいたときには、今日だけで三人のお客に釣り銭を多く渡してしまっていた。手元の算盤と、会計呪文のはじく答えが、どうしても合わない。

符を一枚足しただけだ。なのに、どうして「よそ」の割引まで狂うんだ——。明後日は一年で一番の書き入れ時。このままでは、店が回らない。

丘の上のアカデミー

藁にもすがる思いで、私は丘の上の魔法アカデミーの門を叩いた。古い石造りの工房に、天井まで届く本棚、そこへぎっしり詰まった魔導書、宙にちらちらと浮かぶ光の粒。商売人の私には、何もかもが場違いに見える。

応対に出てきたのは、ローブの裾を踏みそうなほど若い見習いだった。ルーク、と名乗る。声の調子は丁寧だが、頬にはまだ少年の線が残っている。正直に言えば、私は内心ひるんだ。見習い……このお人で、本当に大丈夫だろうか。けれど、背に腹は代えられない。

私は事情を話し、会計の符を見せた。とはいえ符に書かれているのは、私にも半分は分からない呪文の言葉だ。ルークの説明によれば、まず会計に出てくる「もの」が、こんなふうに決められているのだという。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
data class Money(val amount: Int) {
    init {
        require(amount >= 0) { "金額は0以上でなければなりません: $amount" }
    }

    fun percent(n: Int): Money = Money(amount * n / 100)

    operator fun minus(other: Money): Money = Money(amount - other.amount)

    companion object {
        val ZERO = Money(0)
    }
}

data class Customer(val loyaltyPoints: Int)

data class Order(val total: Money, val customer: Customer)

Money がお代の額(単位はこの国のお金、グルド)、Customer がお客で、loyaltyPoints というのが常連度合いを表す点数らしい。Order がひとつの会計だ。どれも頭に付いた data class は、値をひとまとめに持つための器を手早くこしらえる書き方で、こうして作った器は「中身が同じなら同じもの」として扱える——金額の等しい二つの Money は、別々に作っても等しいものと見なされる、とルークは言い添えた。後で答え合わせ(模擬詠唱)をするとき、この性質が効いてくるらしい。

percent(10) と書けば一割引きの額が出る、と、ルークは指で符をたどりながら教えてくれた(fun percent(...): Money = Money(...) のように、本体を波括弧で囲わず = の後ろに式を一つだけ書くのは、その答えをそのまま返す省略形だそうだ)。operator fun minus というのは、お代から割引を引く -(マイナス)の引き算を、この Money どうしでもできるようにするための一行——あとで会計が お代 - 割引 と書けるのは、これがあるからだそうだ。金額に負の数は入れない約束になっていて、Money の頭にある init { } は、その器が生まれる一瞬に一度だけ走る検めで、中の require という見張りが、負の額をその場ではじく。値引きのない会計のために ZERO(ゼログルド)も最初から用意してある。この ZERO を収めた companion object は、その型にひとつだけ備え付けておく定数や道具の置き場で、だから実体を作らずとも Money.ZERO と型の名から直に取り出せるのだという。

そして、肝心の「どの割引を当てるか」を決めているのが、この一本の呪文だった。

1
2
3
4
5
6
7
8
9
fun calculateDiscount(order: Order, type: String): Money {
    return when (type) {
        "SEASONAL" -> order.total.percent(10)
        "MEMBER"   -> if (order.customer.loyaltyPoints >= 1000) order.total.percent(5) else Money.ZERO
        "COUPON"   -> order.total.percent(8)
        // 割引を足すたび、ここに分岐を「足す」しかない
        else       -> Money.ZERO
    }
}

type というのが「今回はどの割引か」を表す合言葉で、when というのが、その合言葉によって行き先を振り分ける仕掛け——ほかの言語の switch に当たるものだそうだ。季節割引なら一割、常連割引なら点数が千以上のお客に五分、割引券なら八分。いちばん下の else は、どの合言葉にも当てはまらなかったときの行き先らしい。私が割引を増やすたびに、この一本の呪文の中へ新しい行き先を書き足してきた。今度の割引券も、そうやって足したばかりだった。

とはいえ、こうして一本に書き出すと、存外すっきりして見える。けれど私の手元の符は、割引を継ぎ足すたびにこの一本を開いて手で書き加えてきたぶん、もっと込み入っていた。今度の割引券を足したときも、すぐ隣にあった季節割引の行を、どうやら巻き込んでしまったらしい。季節割引のお客に券の値引きが紛れたのは、それでだ。一本に全部を詰め込んで、足すたびに本体そのものへ手を入れる——その造りでいる限り、書き足しの一手は、いつ隣を巻き込んでもおかしくなかった。

ルークは符を覗き込みながら、「なるほど……割引を増やすたびに、この大きな一つの呪文へ書き足してきたんですね」と確認した。その手つきは存外に丁寧で、私はほんの少しだけ、不安がやわらぐのを感じた。

「あらゆる割引に備えます!」

ところが、次の瞬間。ルークの目が、きらりと輝いたように見えた。

「マレンさん、ご安心を。このスペルパターンなら、あらゆる割引に備えられます!」

そう言うが早いか、彼は習いたての高位魔法を使いたくてたまらない様子で、宙に大きな魔法陣を描き始めた。私の目に映ったのは、枝分かれしていく一本の系統樹だった。「基本の割引」という幹から「季節の割引」が伸び、そこへさらに「季節かつ常連の割引」「季節かつ常連かつ割引券の割引」と、掛け合わせの枝が次々と生えていく。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
abstract class Discount {
    abstract fun calc(order: Order): Money
}

abstract class SeasonalDiscountBase : Discount()
class SeasonalDiscountImpl : SeasonalDiscountBase() { /* 季節割引の計算 */ }
class SeasonalMemberDiscount : SeasonalDiscountBase() { /* 季節かつ常連 */ }
class SeasonalMemberCouponDiscount : SeasonalMemberDiscount() { /* 季節かつ常連かつ券 */ }
class MemberCouponDiscount : Discount() { /* 常連かつ券 */ }
// …この調子で、掛け合わせのぶんだけクラスが増えていく…

abstract と付いたクラスは、それ自体では実体を作れず、継承して中身を埋めるための骨組みなのだと、後でルークが教えてくれた。けれど、その骨組みばかりが、見ているそばから増えていく。

それだけでは終わらなかった。「これらを生み出す大魔法陣も用意しておきましょう」と、ルークはもう一つ別の図形を描き足す。割引の組み合わせを受け取って、どの割引を作るかを決める、生成役の魔法陣だという。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
abstract class DiscountFactory {
    abstract fun create(flags: Set<String>): Discount
}

class HarvestFestivalFactory : DiscountFactory() {
    override fun create(flags: Set<String>): Discount {
        // どの組み合わせのときどのクラスを作るか、ここでまた大きく振り分ける
        TODO()
    }
}

create の頭に付いた override は、親に当たる骨組み(DiscountFactory)が「こう作れ」と形だけ決めた約束を、この工房が自分のやり方で引き受けて果たす、という宣言なのだと、これも後でルークが教えてくれた。そして、いちばん奥にある TODO() は「中身は後で埋める」という仮置きの呪文で、つまりこの大魔法陣は、形ばかりで、まだ何ひとつ動いていない。工房の壁が、見る間に図形で埋まっていく。私にはもう、どれがどの割引なのか、まるで追えなかった。

壁を埋めていく系統樹

割引はたったの三つ——季節と、常連と、割引券。それだけのはずだ。なのに、壁に並ぶ枝(ルークの言う「クラス」)は、見ているうちに十数本にまで膨れ上がっていた。

——ふと、書斎の隅に置かれた水晶玉の中で、ため息のように、光がゆらりと揺らいだ。かすかに、ちりりと音が鳴った気もする。けれど、ルークは枝を描き足す手を止めない。私も、古い道具がきしんだのだろうと、それきり忘れてしまった。

私は、明後日のことを尋ねてみた。「あの、ルークさん。明後日から始める、時間限定のフラッシュセールの割引は、その図のどこに足せばいいんでしょう」

ルークの手が、ぴたりと止まった。

「えっと、それは季節の枝に……いえ、常連のお客にも掛かりますから、こちらの枝に新しく……あれ、でもこの枝はもう、券とも掛かっていて……」

彼は、自分で描いた系統樹の中で、すっかり迷子になっていた。新しい割引を一つ足そうとするだけで、どの枝につなげばいいのか、本人にも分からなくなっている。やがて魔法陣を起動しようとすると、絡まり合った魔導式どうしが光をぶつけ合って、ばちばちと火花が散った。

「あっ、魔導式が……暴走し始めました!?」

私は思わず後ずさった。正直に言わせてもらえば、最初に私が見せた符より、ずっと複雑になっている。私の小さな店の会計が、こんなに大層なものだっただろうか。ルークは頭を抱えてしまった。これでは、収穫祭どころではない。

水晶玉の声

そのときだった。さっき光が揺らいだあの水晶玉から、しわがれた、けれど不思議と穏やかな声が響いた。ルークが慌てて「し、師匠のマグナスです」と教えてくれる。さきほどのは、道具のきしみではなかったらしい。私はこのとき初めて、その声の主の存在を知った。姿は見えず、声だけが水晶玉から届いてくる。

声の主は、答えを言わなかった。ただ、問いをひとつだけ置いた。

「ルーク。お前さんはさっきから、割引の『種類』を増やそう増やそうとしておるな。だがの——店主どのが本当に困っておるのは、割引の種類が増えること自体かね? それとも、ひとつ増やすたびに、会計『全部』へ手を入れねばならんことかね?」

短い沈黙があった。ルークが、はっと目を見開いたように見えた。

「……増やすたびに、本体に、手を入れること、です」

彼はしばらく宙をにらんで黙っていたが、やがて、自分の言葉でゆっくりと続けた。

「そうか……割引の『やり方』そのものを、一つずつ、別々の小さな符に切り分けて持たせればいいんだ。どの符を使うかは、会計を始めるときに外から手渡せばいい。そうすれば会計の本体は、渡された符を受け取って唱えるだけ——どれを使うか選ぶ役目すら、本体は背負わずに済む。割引を足すときも、本体には触らずにいられる」

水晶玉の声は「ふむ」とだけ返して、先を急かさなかった。ように聞こえた。

三枚の呪符

ルークは、壁一面に広がった系統樹も、生成役の大魔法陣も、すうっと消してしまった。代わりに、机の上へ小さな呪符を三枚だけ並べる。季節の符、常連の符、割引券の符。

「どの符も、『注文を受け取って、割引額を一つ返す』。約束しているのは、その同じ一点だけです」とルークは言った。三枚は、中の計算こそ違うけれど、外から見た「形」はそろっている。この「形だけを決めた約束」のことを、インターフェース(中身の実装ではなく、「こういう呼び出しに応える」という形だけを取り決めた設計図)と呼ぶのだと、ルークは教えてくれた。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
interface DiscountStrategy {
    fun calculate(order: Order): Money
}

class SeasonalDiscount : DiscountStrategy {
    override fun calculate(order: Order) = order.total.percent(10)
}

class MemberDiscount : DiscountStrategy {
    override fun calculate(order: Order) =
        if (order.customer.loyaltyPoints >= 1000) order.total.percent(5) else Money.ZERO
}

class CouponDiscount(private val percent: Int) : DiscountStrategy {
    init { require(percent in 0..100) { "クーポン割引率が不正です: $percent" } }
    override fun calculate(order: Order) = order.total.percent(percent)
}

DiscountStrategy というのが、さっきの「形だけの約束」だ。calculate という呼び出しに割引額で応える、ということだけを決めている。季節の符・常連の符・割引券の符は、それぞれその約束を守りつつ、中の計算だけを自分のやり方で書いている。各符の頭にある override は、「この約束を、自分のやり方で果たします」という宣言なのだそうだ。割引券の符だけは CouponDiscount(private val percent: Int) と書いて、割引率を外から受け取れるようにしてある——同じ券の仕組みのまま、率だけを差し替えられるようにだ。こんなふうに、丸括弧の中へ private val と書いておくと、受け取った値はそのままその符の持ち物になり、あとから calculate の中で使える、ともルークは言い添えた。その符には require という見張りもついていて、割引率がおかしな値(百分率なのに範囲外の数)だと、その場ではねつけるようになっていた。

そして、会計の本体はこれだけになった。

1
2
3
class OrderProcessor(private val discount: DiscountStrategy) {
    fun checkout(order: Order): Money = order.total - discount.calculate(order)
}

会計呪文は、渡された符が季節のものか券のものか、もう気にしない。ただ受け取って、一度だけ「割引額を出しておくれ」と唱え、その分を総額から引く。それだけだ。あの巨大な when の振り分けは、跡形もなく消えていた。

「これで、会計の本体とそれぞれの割引が、きれいに切り離されました」

ルークがそう言って杖を軽く振ると、机の上に光のスクロールが浮かび上がった。そこには、新しく整えられた呪文たちの関係が、整然と描き出されていた。

Strategy Pattern Class Diagram

「見ての通り、会計の本体である OrderProcessor は、具体的な割引の符には直接触れていません。ただ一枚の約束事である DiscountStrategy だけを見つめています。そして、それぞれの割引の符は、その約束事を果たす形で独立しています」

ルークがいったん手を止めて、私のほうを振り返った。

「マレンさん。この『やり方を符に切り出して、丸ごと差し替えられるようにする』定石を、Strategyと呼ぶそうです」

Strategy——割引のような「やり方」の一群をそれぞれ部品に切り出して、本体を変えないまま交換したり、新しく足したりできるようにする、設計の型なのだという。魔術師の世界には、ずいぶん物々しい名前があるものだ。

模擬詠唱

「本番の前に、安全な結界の中で試します」

ルークはそう言って、模擬詠唱を始めた。私にも分かるように、一つずつ声に出して確かめてくれる。模擬詠唱というのは、魔法を本番で使う前に、結界の中で「ちゃんと狙いどおりに効くか」を試しておく作法のことらしい。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith

fun main() {
    val regularCustomer = Customer(loyaltyPoints = 500)
    val memberCustomer  = Customer(loyaltyPoints = 1000)
    val almostMember    = Customer(loyaltyPoints = 999)
    val order1000       = Order(total = Money(1000), customer = regularCustomer)
    val memberOrder1000 = Order(total = Money(1000), customer = memberCustomer)
    val almostOrder1000 = Order(total = Money(1000), customer = almostMember)

    // 各符が、正しい割引額を返すか
    assertEquals(Money(100), SeasonalDiscount().calculate(order1000))
    assertEquals(Money(50),  MemberDiscount().calculate(memberOrder1000))
    assertEquals(Money(0),   MemberDiscount().calculate(order1000))
    assertEquals(Money(80),  CouponDiscount(8).calculate(order1000))

    // 会計の本体が、総額から割引を引いた額を返すか
    assertEquals(Money(900), OrderProcessor(SeasonalDiscount()).checkout(order1000))
    assertEquals(Money(950), OrderProcessor(MemberDiscount()).checkout(memberOrder1000))
    assertEquals(Money(920), OrderProcessor(CouponDiscount(8)).checkout(order1000))

    println("ALL TESTS PASSED")
}

先頭の二行の import は、Kotlin に元から備わっている模擬詠唱の道具を取り込む宣言だ——assertEquals は「答えが思ったとおりか」を、assertFailsWith は「狙いどおりに失敗するか」を確かめる道具だという。千グルドの会計なら、季節の符は百引き、常連の符は——点数が千ある常連さんなら——五十引き、八分の券なら八十引き。会計の本体に季節の符を渡せば、ちゃんと九百グルドが返ってくる。一つずつ、私の算盤と同じ答えが出た。

ルークは、常連の境目もわざわざ試して見せてくれた。あと一歩で常連、という点数が九百九十九のお客には割引がつかず、ちょうど千になったお客から五分引きになる。境目のすぐ両側を、一つずつ確かめておくのが大事なのだという。

1
2
3
4
5
6
7
8
    // 常連の境目(999 では割引なし、1000 から割引)
    assertEquals(Money(0),  MemberDiscount().calculate(almostOrder1000))
    assertEquals(Money(50), MemberDiscount().calculate(memberOrder1000))

    // ありえない割引率の符は、その場ではねつける
    assertFailsWith<IllegalArgumentException> {
        CouponDiscount(-5)
    }

最後の一つは、わざと壊れた符を作ろうとする試しだった。割引率に「引く五分」などというありえない数を渡すと、assertFailsWith ——「ちゃんと失敗するはずだ」と念を押す詠唱——のとおり、符はその場で作られることを拒んだ。おかしな符が会計に紛れ込む前に、入り口で止まるわけだ。

符を一枚、足すだけ

ここまでは、今ある割引を整え直しただけだ。私が本当に知りたかったのは、その先だった。明後日のフラッシュセール——新しい割引を足したとき、また会計全部が狂ったりしないのか。

ルークは、新しい符を一枚、すっと描いてみせた。

1
2
3
class FlashSaleDiscount : DiscountStrategy {
    override fun calculate(order: Order) = order.total.percent(15)
}

それだけだった。会計の本体にも、季節の符にも、券の符にも、彼は指一本触れなかった。新しい符は、ほかの三枚と同じ「形」を守っているから、会計の本体はそれを今までどおり受け取って唱えるだけでいい。

1
2
3
    val flashOrder = Order(total = Money(2000), customer = regularCustomer)
    assertEquals(Money(300),  FlashSaleDiscount().calculate(flashOrder))
    assertEquals(Money(1700), OrderProcessor(FlashSaleDiscount()).checkout(flashOrder))

模擬詠唱でも、二千グルドのお代に一割五分で三百引き、会計は千七百グルド。新しい符を足すために書いたのは、この符そのものと、それを試す詠唱だけ。前からあった符も会計の本体も、一文字も書き換えていない。それでも、ちゃんと動く。

「割引を足したいときは、新しい符を一枚書くだけ。この会計の本体には、もう二度と触りません。だから、足しても、ほかの割引は壊れないんです」

ルークは、それを少し誇らしそうに——いや、それはどうだろう、ともかく落ち着いた声で、そう言った。会計の本体は、書き換えに対して閉じている——新しい割引が来ても、もう触らなくていい。その一方で、割引の種類を増やすことには開いている——新しい符を一枚書けば足せる。この「閉じているのに、開いてもいる」設計の心得を、開放閉鎖の原則(既存の部分には手を入れず=閉じ、機能の追加はできる=開く、という指針)と呼ぶのだと、彼は付け加えた。あの取り違えの事故は、一つの呪文に全部を詰め込んで、足すたびに本体をいじっていたから起きた。本体に触らなくてよくなれば、よそへ紛れ込みようがない。

私は、ここでようやく、半信半疑だった気持ちが、すっと軽くなるのを感じた。

整えられた会計呪文を、私は受け取った。さっきまで壁一面を埋めていたあの図形の群れは跡形もなく、手元にあるのは三枚——いや、明後日のぶんを入れて四枚の小さな符と、驚くほど短い会計の式だけ。同じ仕事をするのに、こんなに減るものなのか。ごちゃついた帳簿を一冊、白紙から付け直したような、妙にすがすがしい気持ちになった。

試しに、私自身の手で、フラッシュセールの符を会計に差してみる。会計呪文は何事もなかったように、正しい釣り銭をはじいた。新しい符を足しても、季節の符も券の符も、もう「よそ」へ紛れ込まない。あの取り違えは、起きない。

これなら、私一人でも、安心して割引を足していける。その実感が、何よりありがたかった。

同じことを、もっと小さく

帰り支度をしていると、ルークが「ちなみに」と付け加えた。

「割引が、これくらい単純な——注文を受け取って額を一つ返すだけ、というものなら。わざわざ符(クラス)にしなくても、呪文そのものを一つの道具として手渡す書き方もあるんです」

彼が見せてくれたのは、こんな形だった。

1
2
3
4
5
6
7
8
typealias DiscountSpell = (Order) -> Money

class OrderProcessor2(private val discount: DiscountSpell) {
    fun checkout(order: Order): Money = order.total - discount(order)
}

// 使うときは、計算そのものを直接手渡す
val seasonal: DiscountSpell = { order -> order.total.percent(10) }

先頭の typealias は、長ったらしい型の名前に短い別名をつける書き方で、ここでは (Order) -> Money——「注文を受け取って額を返す」という計算の型——に DiscountSpell という呼びやすい名前をつけている。いちばん下の { order -> order.total.percent(10) } は、波括弧でくくった計算の塊を、そのまま一つの値として手渡しているのだそうだ。これが、さっきまで符(クラス)でやっていたことの、もっと身軽な書き方になる。こんなふうに、計算を値として受け渡せる仕掛けを、高階関数(関数を、数や文字と同じように引数で受け取ったり返したりできる仕組み)と呼ぶらしい。符に名前をつけて一枚仕立てるか、計算だけを身軽に手渡すか。形は違っても、やっていることは「割引のやり方を、丸ごと差し替えられるようにする」——さっきと同じことだ、とルークは言った。

私には、どちらがどう違うのか、正直なところよく分からない。けれど、同じ考え方が大小二つの形で書ける、ということだけは、なんとなく腑に落ちた。

水晶玉の声は、いつのまにか「今日のところは及第点かの」とだけ言い残して、気配を消していた。ように感じた。ルークは少しだけほっとした顔で、自分の魔導書に何か書きつけている。

私は礼を言って、店へ戻った。明後日の収穫祭は、きっと忙しい。けれど今は、会計のことを恐れずに済む。それだけで、足取りが軽かった。


📖 見習い魔術師ルークの魔導書メモ(Luke’s Spellbook Notes)

  • 修復した呪文(クラス/モジュール名): OrderProcessor(会計呪文)と DiscountStrategy 一族(季節・常連・割引券の各呪符)
  • 乱れの要因(アンチパターンの課題):
    • 季節・常連・割引券の割引を、一つの巨大な when に詰め込んでいた。割引を足すたびに本体の when を開いて改変するため、一箇所の追記が既存の割引へ波及し、季節割引のお客に券の割引が出る「取り違え」(釣り銭事故)を招いた。
  • 過剰設計の反省(背伸びの過ち):
    • 「あらゆる割引に備える」と意気込み、割引の掛け合わせぶんだけ継承クラスを増やす多階層の系統樹と、それを生む生成役まで盛ってしまった。割引は三種類なのにクラスは十数個に膨れ、新しい割引をどこへ足すかすら分からなくなって自滅した。Before より複雑にしては本末転倒だった。
  • 魔術の定石(学んだ設計パターン):
    • Strategy。割引の「やり方」を DiscountStrategy という同じ形(インターフェース)の符に一枚ずつ切り出し、会計本体は符を受け取って唱えるだけにした。新しい割引は符を一枚足すだけで済み、本体は修正に対して閉じ、拡張に対して開く(開放閉鎖の原則)。単純な割引なら高階関数 (Order) -> Money でも同じ差し替えが書ける。
  • 模擬詠唱の記録(検証のポイント):
    • 各符の割引額と会計結果、常連の境目(999 と 1000)、ありえない割引率を入り口ではねること(assertFailsWith)を確認。極めつけに、新しい FlashSaleDiscount を既存コード無改変で足しても会計が正しく動くことを模擬詠唱で示し、「足しても壊れない」を目で確かめた。
  • 師匠マグナスの格言:
    • 「種類が増えるのは罪ではない。増やすたびに、すべてに触れねばならぬ造りが、罪なのだよ。」
comments powered by Disqus
Hugo で構築されています。
テーマ StackJimmy によって設計されています。