SE(たぶん)の雑感記

一応SEやっている筆者の思ったことを書き連ねます。会計学もやってたので、両方を生かした記事を書きたいと考えています。 でもテーマが定まってない感がすごい。

CodeKataで遊ぶ Kata09: Back to the Checkout

CodeKataやってみた記事、第九弾です。

今回は、Kata09: Back to the Checkoutをやっていきます。

codekata.com

今回は、実際にコードを書く問題です。

以前書いた、CodeKata第一回の問題を元にしているため、Back to the Checkoutという題名のようです。

codekata.com

hiroronn.hatenablog.jp

概要

Checkoutとは、ここではスーパーのレジを指すようです。

ざっくり言うと、レジを作りましょう、という問題です。

スーパーで買い物する際、最も単純な価格決定方法は、単価 * 個数です。

この問題では、さらにまとめ買いが存在します。
例えば、50セントの商品をまとめて3つ購入した場合、130セント(1ドル30セント)になる、といった具合です。

こういう、特殊なルールにも対応できるコードを作成します。

価格表

Item   Unit      Special
       Price     Price
--------------------------
  A     50       3 for 130
  B     30       2 for 45
  C     20
  D     15

単価50セントのAを3個購入したら、本来150セントのところを130セントで買えます。 Bも似たような価格設定ですが、CとDは単純に単価 * 個数です。

実装指針

co = CheckOut.new(pricing_rules)
co.scan(item)
co.scan(item)
    :    :
price = co.total

サイトに載っているRubyの例です。

  • CheckOut(レジ)に金額ルールリストを渡す
  • 個々の商品を一つずつ渡す(順不同。A→B→A→C→Aのような順番でも、Aを3つ購入になるので、割引が適用される)
  • 最後に、レジから合計金額を取得

のように実装します。

肝は、scanに渡される商品に応じて、内部計算を切り替えるところです。

目的

チェックアウト(レジ)が、特定の商品や価格戦略(strategies)を知らない方法で、これを実装できますか?

とあります。いわゆる抽象化のことです。

また、価格戦略を簡単に追加できるように考慮して、作成します。

いつものように、続きは一応隠します。(直接見ると隠れません)

自分なりの答え

ドメイン駆動等を考えると、型定義はしっかりすべきですが、大変なので金額はint、商品識別はstrで行っています。

価格戦略

価格戦略(価格決定ルール)は、個数を渡すことで、個数に応じた金額を返します。

from abc import ABCMeta, abstractmethod

class PriceRule(metaclass=ABCMeta):
    """価格決定ルール"""
    @abstractmethod
    def calculate(self, count: int) -> int:
        pass

そのまんまの継承元(インターフェイス)を作成します。

シンプルなもの

from rules.pricerule import PriceRule

class SimpleRule(PriceRule):
    """単価 * 個数ルール"""
    def __init__(self, price: int):
        self._price = price
    
    def calculate(self, count: int) -> int:
        return count * self._price

コンストラクタで単価指定し、immutableなクラスにします。

複雑なもの

from rules.pricerule import PriceRule

class MultipleDiscountRule(PriceRule):
    """一定個数購入時割引ルール"""
    def __init__(self, price: int, discount_count: int, discount_price: int):
        self._price = price
        self._count = discount_count
        self._discount_price = discount_price
    
    def calculate(self, count: int) -> int:
        simple = count % self._count * self._price
        discount = int(count / self._count) * self._discount_price
        return simple + discount

コンストラクタで、本来の単価、割引を受ける個数、割引時の価格を受け取ります。 それを元に計算します。

ルール群

from rules.pricerule import PriceRule

class PriceRules(object):
    """価格決定ルール群"""
    def __init__(self):
        self._rules = {}
    
    def register(self, code: str, rule: PriceRule):
        """商品を登録します。"""
        self._rules[code] = rule
    
    def calculate(self, code: str, count: int) -> int:
        """個数に応じて、指定コードの商品金額を計算します。"""
        rule: PriceRule = self._rules.get(code)
        if rule == None:
            # 本来、例外で返すべきだが、省略
            return 0
        
        return rule.calculate(count)

価格ルールは、商品コード(この問題では、アルファベット一文字)ごとに管理します。

そして、当クラスに計算機能も持たせています。
ルールを返すだけ、としてもよいのですが、その場合はCheckOutで計算するだけです。

CheckOut実装

from rules.pricerules import PriceRules

class CheckOut:
    def __init__(self, pricing_rules: PriceRules):
        self._rules = pricing_rules
        self._counts = {}

    def scan(self, code: str):
        self._counts.setdefault(code, 0)
        self._counts[code] += 1

    @property
    def total(self) -> int:
        """合計金額を返します。"""
        prices = 0
        for code, count in self._counts.items():
            prices += self._rules.calculate(code, count)

        return prices

クラスが持つ役割は、

  • 価格表は外部から受け取る
  • 商品購入数量管理は自分で行う
  • 金額計算は、受け取った価格表のみを用いる

というところです。

totalが呼ばれると、その時点の購入状態に応じて計算が行われます。

ルール群の作成

from rules.pricerules import PriceRules
from rules.rule import SimpleRule, MultipleDiscountRule

def get_test_price() -> PriceRules:
    prices = PriceRules()
    prices.register('A', MultipleDiscountRule(50, 3, 130))
    prices.register('B', MultipleDiscountRule(30, 2, 45))
    prices.register('C', SimpleRule(20))
    prices.register('D', SimpleRule(15))

    return prices

ルール群に、各ルールを追加するだけです。

ここは、DB値を元に登録する等、様々な方法が考えられます。日々変わるものなら、登録手段は凝る必要があります。

方式の利点

こういう実装にする利点を、ケースごとに挙げます。

  • 新しい価格戦略を追加した場合、レジの修正は不要
    価格戦略そのものを増やし、ルール群に追加したら、動作が変わるためです。

  • ルールの追加は容易
    ルールが持つべき要件はPriceRuleに準拠すること、すなわち、商品個数を受け取ったら価格を返すことのみです。
    実装は簡単です。

もっとも、ルール群に渡す必要はあります。

  • レジは、受け取ったものにしか依存しない
    レジが自分で管理しているのは、商品の個数のみであり、価格は管理していません。
    よって、価格変更等が起こっても、レジは影響を受けません。

おわりに

今回は、例題を解く感じでした。
この手の問題は、わりとよくあると思うので、いい練習になります。

なんとなく実装は浮かぶけど、すぐに実装するのは難しいように思います。

練習問題としてはいいなぁ、と思いました。