Practical Python Design Patterns - The Interpreter Pattern 編
Implementing the Interpreter Pattern(インタープリタパターンの実装)
ソフトウェアの利用者は2タイプに分類できます: そのままの状態で満足して使用するタイプと、自分たちの用途によりマッチさせるためにソフトウェアに何かしら手を加えようとするタイプです。インタプリタパターンが対象とするのは後者のタイプの人たちだけです。それは、このタイプの人たちが、ソフトウェアを自分たちの用途に合わせ込むためであれば DSL を習得するための手間を厭わないからです。
我々のレストランの話に戻れば、こちらが用意した「2つ買えば1つ無料 (two-for-one)」といったスペシャルサービスの基本的なテンプレートで満足するオーナーもいる一方で、独自のスペシャルサービスを定義し提供したい、と考えるオーナーもいるでしょう。
こういった将来的なニーズに対応できるように、前セクションで作成した DSL の基本的な実装をよりジェネリックなものにしていきます。
プロセスに関連するすべてのエンティティ、非終端記号 (ルール書式) には基本的に全てにクラスを割り当てる、という話をしました。そして、それぞれのクラスでは interpret (解釈・解読・解析) メソッドを定義します。また、グローバルコンテキストを保持しておくためのクラス、オブジェクトも必要となります。このコンテキストオブジェクトは、ルールを解釈する一連の処理 (interpretation stream) の中でそれを構成するそれぞれのエンティティクラスオブジェクトの interpret メソッドに次々に渡され順次解析、処理されていきます。
解析処理ではその問題の答え (終端) に達するまでコンテナオブジェクトを再帰的に処理します:
class NotTerminal:
def __init__(self, expression):
self.expression = expression
def interpret(self):
self.expression.interpret()
class Terminal:
def interpret(self):
pass
def __init__(self, expression):
self.expression = expression
def interpret(self):
self.expression.interpret()
class Terminal:
def interpret(self):
pass
そして一連の解析の結果として、ある tab (伝票、勘定明細) がスペシャルサービスの対象となり得るか、を判断します。
まず最初に tab と item クラスを定義し、続けて文法を満たすために必要なクラスの定義も行います。その後、定義した文法のセンテンス (構文) のいくつかを実装し、テスト用の tab データを使用してテストを行います。この例では ItemType や CustomerType などをハードコードしていますからテストをそのまま走らせることが可能です。ただ一般的には、これらのタイプデータはファイルやデータベースに保存しておき取り出して利用することになるでしょう:
import datetime
from math import floor
DAY_OF_WEEK = {
0: 'Monday',
1: 'Tuesday',
2: 'Wednesday',
3: 'Thursday',
4: 'Friday',
5: 'Saturday',
6: 'Sunday',
}
class ItemType:
def __init__(self, name: str):
self.name = name
class Item:
def __init__(self, name: str, item_type: ItemType, cost: int):
self.name = name
self.item_type = item_type
self.cost = cost
class ItemIsA:
def __init__(self, item_type: ItemType):
self.item_type = item_type
def evaluate(self, item: Item) -> bool:
return self.item_type == item.item_type
class CustomerType:
def __init__(self, customer_type: str):
self.customer_type = customer_type
class Customer:
def __init__(self, customer_type: CustomerType, name: str):
self.customer_type = customer_type
self.name = name
class Tab:
def __init__(self, customer: Customer):
self.items: list[Item] = []
self.discounts: list[int] = []
self.customer = customer
def calculate_cost(self) -> int:
return sum(x.cost for x in self.items)
def calculate_discount(self) -> int:
return sum(x for x in self.discounts)
class ConditionBase:
"""
スペシャルサービス適用を判断するための各種条件判定クラス定義ベース
タイプヒントを付けるための便宜上の抽象インターフェースクラス定義
(Python はダックタイピングのため本来必要ない)
"""
def evaluate(self, tab: Tab) -> bool:
pass
class CustomerIsA(ConditionBase):
def __init__(self, customer_type: CustomerType):
self.customer_type = customer_type
def evaluate(self, tab: Tab) -> bool:
return tab.customer.customer_type == self.customer_type
class DayOfTheWeek:
def __init__(self, name: str):
self.name = name
class TodayIs(ConditionBase):
def __init__(self, day_of_week: DayOfTheWeek):
self.day_of_week = day_of_week
def evaluate(self, tab: Tab) -> bool:
return DAY_OF_WEEK[datetime.datetime.now().weekday()] == self.day_of_week.name
class TimeIsBetween(ConditionBase):
def __init__(self, from_time: str, to_time: str):
self.from_time = from_time
self.to_time = to_time
def evaluate(self, tab: Tab) -> bool:
hour_now = datetime.datetime.now().hour
minute_now = datetime.datetime.now().minute
from_hour, from_minute = [int(x) for x in self.from_time.split(':')]
to_hour, to_minute = [int(x) for x in self.to_time.split(':')]
hour_in_range = from_hour <= hour_now < to_hour
begin_edge = hour_now == from_hour and minute_now > from_minute
end_edge = hour_now == to_hour and minute_now < to_minute
return any((hour_in_range, begin_edge, end_edge))
class TodayIsAWeekDay(ConditionBase):
def __init__(self):
pass
def evaluate(self, tab: Tab) -> bool:
week_days = [
'Monday',
'Tuesday',
'Wednesday',
'Thursday',
'Friday',
]
return DAY_OF_WEEK[datetime.datetime.now().weekday()] in week_days
class TodayIsAWeekendDay(ConditionBase):
def __init__(self):
pass
def evaluate(self, tab: Tab) -> bool:
weekend_days = [
'Saturday',
'Sunday',
]
return DAY_OF_WEEK[datetime.datetime.now().weekday()] in weekend_days
class Conditions(ConditionBase):
def __init__(self, expression: ConditionBase):
self.expression = expression
def evaluate(self, tab: Tab) -> bool:
return self.expression.evaluate(tab)
class And(ConditionBase):
def __init__(self, expression1: ConditionBase, expression2: ConditionBase):
self.expression1 = expression1
self.expression2 = expression2
def evaluate(self, tab: Tab) -> bool:
return self.expression1.evaluate(tab) and self.expression2.evaluate(tab)
class Or(ConditionBase):
def __init__(self, expression1: ConditionBase, expression2: ConditionBase):
self.expression1 = expression1
self.expression2 = expression2
def evaluate(self, tab: Tab) -> bool:
return self.expression1.evaluate(tab) or self.expression2.evaluate(tab)
class NumberOfItemsOfType(ConditionBase):
def __init__(self, item_type: ItemType, number_of_items: int):
self.item_type = item_type
self.number = number_of_items
def evaluate(self, tab: Tab) -> bool:
return len([x for x in tab.items if x.item_type == self.item_type]) >= self.number
class ServiceBase:
"""
各種スペシャルサービス適用条件が満たされた場合の
割引額算出クラス定義ベース
タイプヒントを付けるための便宜上の抽象インターフェースクラス定義
(Python はダックタイピングのため本来必要ない)
"""
def calculate(self, tab: Tab) -> int:
pass
class PercentageDiscount(ServiceBase):
def __init__(self, item_type: ItemType, percentage: int):
self.item_type = item_type
self.percentage = percentage
def calculate(self, tab: Tab) -> int:
if self.item_type.name == 'AnyItem':
f = lambda x: True
else:
f = lambda x: x.item_type == self.item_type
return floor((sum([item.cost for item in tab.items if f(item)]) * self.percentage) / 100)
class CheapestFree(ServiceBase):
def __init__(self, item_type: ItemType):
self.item_type = item_type
def calculate(self, tab: Tab) -> int:
try:
return min([x.cost for x in tab.items if x.item_type == self.item_type])
except:
return 0
class Rule:
def __init__(self, conditions: ConditionBase, discounts: ServiceBase):
self.conditions = conditions
self.discounts = discounts
def evaluate(self, tab) -> int:
if self.conditions.evaluate(tab):
return self.discounts.calculate(tab)
return 0
member = CustomerType('Member')
non_member = CustomerType('Non-member')
pizza = ItemType('Pizza')
burger = ItemType('Burger')
drink = ItemType('Drink')
any_item = ItemType('AnyItem')
fruit = ItemType('Fruit') # テストのために追加
monday = DayOfTheWeek('Monday')
def setup_demo_tab() -> Tab:
member_customer = Customer(member, 'John')
tab = Tab(member_customer)
tab.items.append(Item('ペスカトーレ', pizza, 1600))
tab.items.append(Item('フィッシュバーガー', burger, 360))
tab.items.append(Item('マンゴージュース', drink, 650))
tab.items.append(Item('ボスカイオーラ', pizza, 1500))
tab.items.append(Item('グァテマラブレンド', drink, 600))
tab.items.append(Item('海老カツバーガー', burger, 410))
tab.items.append(Item('桃', fruit, 500))
tab.items.append(Item('テリヤキバーガー', burger, 380))
return tab
if __name__ == '__main__':
tab = setup_demo_tab()
rules: list[Rule] = []
Rule(
CustomerIsA(member),
PercentageDiscount(any_item, 15)
)
)
Rule(
And(TimeIsBetween('17:00', '19:00'), TodayIsAWeekDay()),
PercentageDiscount(drink, 10)
)
)
Rule(
And(TodayIs(monday), NumberOfItemsOfType(burger, 2)),
CheapestFree(burger)
)
)
for rule in rules:
tab.discounts.append(rule.evaluate(tab))
print(
f'合計金額: {tab.calculate_cost()}\n'
f'割引金額: {tab.calculate_discount()}\n'
)
from math import floor
DAY_OF_WEEK = {
0: 'Monday',
1: 'Tuesday',
2: 'Wednesday',
3: 'Thursday',
4: 'Friday',
5: 'Saturday',
6: 'Sunday',
}
class ItemType:
def __init__(self, name: str):
self.name = name
class Item:
def __init__(self, name: str, item_type: ItemType, cost: int):
self.name = name
self.item_type = item_type
self.cost = cost
class ItemIsA:
def __init__(self, item_type: ItemType):
self.item_type = item_type
def evaluate(self, item: Item) -> bool:
return self.item_type == item.item_type
class CustomerType:
def __init__(self, customer_type: str):
self.customer_type = customer_type
class Customer:
def __init__(self, customer_type: CustomerType, name: str):
self.customer_type = customer_type
self.name = name
class Tab:
def __init__(self, customer: Customer):
self.items: list[Item] = []
self.discounts: list[int] = []
self.customer = customer
def calculate_cost(self) -> int:
return sum(x.cost for x in self.items)
def calculate_discount(self) -> int:
return sum(x for x in self.discounts)
class ConditionBase:
"""
スペシャルサービス適用を判断するための各種条件判定クラス定義ベース
タイプヒントを付けるための便宜上の抽象インターフェースクラス定義
(Python はダックタイピングのため本来必要ない)
"""
def evaluate(self, tab: Tab) -> bool:
pass
class CustomerIsA(ConditionBase):
def __init__(self, customer_type: CustomerType):
self.customer_type = customer_type
def evaluate(self, tab: Tab) -> bool:
return tab.customer.customer_type == self.customer_type
class DayOfTheWeek:
def __init__(self, name: str):
self.name = name
class TodayIs(ConditionBase):
def __init__(self, day_of_week: DayOfTheWeek):
self.day_of_week = day_of_week
def evaluate(self, tab: Tab) -> bool:
return DAY_OF_WEEK[datetime.datetime.now().weekday()] == self.day_of_week.name
class TimeIsBetween(ConditionBase):
def __init__(self, from_time: str, to_time: str):
self.from_time = from_time
self.to_time = to_time
def evaluate(self, tab: Tab) -> bool:
hour_now = datetime.datetime.now().hour
minute_now = datetime.datetime.now().minute
from_hour, from_minute = [int(x) for x in self.from_time.split(':')]
to_hour, to_minute = [int(x) for x in self.to_time.split(':')]
hour_in_range = from_hour <= hour_now < to_hour
begin_edge = hour_now == from_hour and minute_now > from_minute
end_edge = hour_now == to_hour and minute_now < to_minute
return any((hour_in_range, begin_edge, end_edge))
class TodayIsAWeekDay(ConditionBase):
def __init__(self):
pass
def evaluate(self, tab: Tab) -> bool:
week_days = [
'Monday',
'Tuesday',
'Wednesday',
'Thursday',
'Friday',
]
return DAY_OF_WEEK[datetime.datetime.now().weekday()] in week_days
class TodayIsAWeekendDay(ConditionBase):
def __init__(self):
pass
def evaluate(self, tab: Tab) -> bool:
weekend_days = [
'Saturday',
'Sunday',
]
return DAY_OF_WEEK[datetime.datetime.now().weekday()] in weekend_days
class Conditions(ConditionBase):
def __init__(self, expression: ConditionBase):
self.expression = expression
def evaluate(self, tab: Tab) -> bool:
return self.expression.evaluate(tab)
class And(ConditionBase):
def __init__(self, expression1: ConditionBase, expression2: ConditionBase):
self.expression1 = expression1
self.expression2 = expression2
def evaluate(self, tab: Tab) -> bool:
return self.expression1.evaluate(tab) and self.expression2.evaluate(tab)
class Or(ConditionBase):
def __init__(self, expression1: ConditionBase, expression2: ConditionBase):
self.expression1 = expression1
self.expression2 = expression2
def evaluate(self, tab: Tab) -> bool:
return self.expression1.evaluate(tab) or self.expression2.evaluate(tab)
class NumberOfItemsOfType(ConditionBase):
def __init__(self, item_type: ItemType, number_of_items: int):
self.item_type = item_type
self.number = number_of_items
def evaluate(self, tab: Tab) -> bool:
return len([x for x in tab.items if x.item_type == self.item_type]) >= self.number
class ServiceBase:
"""
各種スペシャルサービス適用条件が満たされた場合の
割引額算出クラス定義ベース
タイプヒントを付けるための便宜上の抽象インターフェースクラス定義
(Python はダックタイピングのため本来必要ない)
"""
def calculate(self, tab: Tab) -> int:
pass
class PercentageDiscount(ServiceBase):
def __init__(self, item_type: ItemType, percentage: int):
self.item_type = item_type
self.percentage = percentage
def calculate(self, tab: Tab) -> int:
if self.item_type.name == 'AnyItem':
f = lambda x: True
else:
f = lambda x: x.item_type == self.item_type
return floor((sum([item.cost for item in tab.items if f(item)]) * self.percentage) / 100)
class CheapestFree(ServiceBase):
def __init__(self, item_type: ItemType):
self.item_type = item_type
def calculate(self, tab: Tab) -> int:
try:
return min([x.cost for x in tab.items if x.item_type == self.item_type])
except:
return 0
class Rule:
def __init__(self, conditions: ConditionBase, discounts: ServiceBase):
self.conditions = conditions
self.discounts = discounts
def evaluate(self, tab) -> int:
if self.conditions.evaluate(tab):
return self.discounts.calculate(tab)
return 0
member = CustomerType('Member')
non_member = CustomerType('Non-member')
pizza = ItemType('Pizza')
burger = ItemType('Burger')
drink = ItemType('Drink')
any_item = ItemType('AnyItem')
fruit = ItemType('Fruit') # テストのために追加
monday = DayOfTheWeek('Monday')
def setup_demo_tab() -> Tab:
member_customer = Customer(member, 'John')
tab = Tab(member_customer)
tab.items.append(Item('ペスカトーレ', pizza, 1600))
tab.items.append(Item('フィッシュバーガー', burger, 360))
tab.items.append(Item('マンゴージュース', drink, 650))
tab.items.append(Item('ボスカイオーラ', pizza, 1500))
tab.items.append(Item('グァテマラブレンド', drink, 600))
tab.items.append(Item('海老カツバーガー', burger, 410))
tab.items.append(Item('桃', fruit, 500))
tab.items.append(Item('テリヤキバーガー', burger, 380))
return tab
if __name__ == '__main__':
tab = setup_demo_tab()
rules: list[Rule] = []
# メンバーは常に合計金額の 15% OFF
rules.append(Rule(
CustomerIsA(member),
PercentageDiscount(any_item, 15)
)
)
# 平日 17 時から 19 時のサービスタイムは全てのドリンクが 10% OFF
rules.append(Rule(
And(TimeIsBetween('17:00', '19:00'), TodayIsAWeekDay()),
PercentageDiscount(drink, 10)
)
)
# 月曜日はハンバーガー2つ以上購入で一番安いもの1つ無料
rules.append(Rule(
And(TodayIs(monday), NumberOfItemsOfType(burger, 2)),
CheapestFree(burger)
)
)
for rule in rules:
tab.discounts.append(rule.evaluate(tab))
print(
f'合計金額: {tab.calculate_cost()}\n'
f'割引金額: {tab.calculate_discount()}\n'
)
この章では、独自文法の構築と内部 DSL としての解釈、実装についてみてきました。まずコンポジットパターンについて学習し、レストランにおけるスペシャルサービスルールの実装に利用しました。続いて一般的なインタープリタパターンと組み合わせ、サービス条件が満たされているかを判断し適用する割引金額を算出するより完成形に近いインタープリターを開発しました。
結果としてこの章を通して行ってきたのは、あるビジネスドメインにおける課題の把握、それを基にした DSL の作成、DSL のコードでの実装、という実際のビジネスにおける一連の開発工程です。