検索ガイド -Search Guide-

単語と単語を空白で区切ることで AND 検索になります。
例: python デコレータ ('python' と 'デコレータ' 両方を含む記事を検索します)
単語の前に '-' を付けることで NOT 検索になります。
例: python -デコレータ ('python' は含むが 'デコレータ' は含まない記事を検索します)
" (ダブルクオート) で語句を囲むことで 完全一致検索になります。
例: "python data" 実装 ('python data' と '実装' 両方を含む記事を検索します。'python data 実装' の検索とは異なります。)
当サイトのドメイン名は " getwebtips.net " です。
トップレベルドメインは .net であり、他の .com / .shop といったトップレベルドメインのサイトとは一切関係ありません。
practical_python_design_patterns

Practical Python Design Patterns - Python で学ぶデザインパターン: The Interpreter Pattern - Part. 2 「インタープリタパターンの実装」の巻 投稿一覧へ戻る

Published 2022年6月24日8:36 by T.Tsuyoshi

SUPPORT UKRAINE

- Your indifference to the act of cruelty can thrive rogue nations like Russia -

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
そして一連の解析の結果として、ある 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] = []

# メンバーは常に合計金額の 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 のコードでの実装、という実際のビジネスにおける一連の開発工程です。

この投稿をメールでシェアする

0 comments

コメントはまだありません。

コメントを追加する(不適切と思われるコメントは削除する場合があります)