shaoxyz

你看上去像是体面人

0%

Python实现策略模式

这是最近求职碰到的一道真实笔试题,HR把题目发来后,完整的编码花了大半个下午,格式化输出实在是有点麻烦
话不多说,直接上题

架构小操练

需求描述

某快餐品牌推出了它独家的外卖应用,用户可以在手机上直接下单。该应用会根据用户选择的菜品(Item)、数量(Count)和优惠方式(Promotion)进行计算,告诉用户需要支付的金额(Charge)。

优惠活动有多种形式。假设用户一次只能使用一种优惠,那么使用哪种优惠省钱最多就会是一个让用户头疼的问题。所以该外卖应用为了方便用户,在用户下单时,会自动选择最优惠的方式并计算出最终金额让用户确认。

我们需要实现一个名为bestCharge的函数,它能够接收用户选择的菜品和数量(以特定格式呈现)作为输入,然后返回计算后的汇总信息。

已知:

  • 该店的菜品每一个都有一个唯一的id
  • 当前的优惠方式有:
    • 满30减6元
    • 指定菜品半价
  • 除菜品外没有其它收费(如送餐费、餐盒费等)
  • 如果两种优惠方式省钱一样多,则使用前一种优惠方式

输入样例

1
["ITEM0001 x 1", "ITEM0013 x 2", "ITEM0022 x 1"]

输出样例

1
2
3
4
5
6
7
8
9
10
============= 订餐明细 =============
黄焖鸡 x 1 = 18元
肉夹馍 x 2 = 12元
凉皮 x 1 = 8元
-----------------------------------
使用优惠:
指定菜品半价(黄焖鸡,凉皮),省13元
-----------------------------------
总计:25元
===================================

使用另一种优惠的样例

输入:

1
["ITEM0013 x 4", "ITEM0022 x 1"]

输出:

1
2
3
4
5
6
7
8
9
============= 订餐明细 =============
肉夹馍 x 4 = 24元
凉皮 x 1 = 8元
-----------------------------------
使用优惠:
满30减6元,省6元
-----------------------------------
总计:26元
===================================

如果没有优惠可享受

输入:

1
["ITEM0013 x 4"]

输出:

1
2
3
4
5
============= 订餐明细 =============
肉夹馍 x 4 = 24元
-----------------------------------
总计:24元
===================================

答案

看到题就感觉似曾相识,参考流畅的Python第六章

好了,把朕的🐎牵上来!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
# -*- coding: utf-8 -*-
"""
@version: python3.7
@author: shaoxyz
@file: strategy.py
@time: 2020-07-01 18:54
@desc: 操练策略模式
"""
import re
import textwrap
import threading
from decimal import Decimal
from abc import ABC, abstractmethod
from functools import wraps
from typing import Dict

"""
菜品属性说明
item = {
_id: <INCR>, # 自增id
name: str, # 菜品名
price: int, # 单价,单位:分
is_half_price: bool # 是否半价
}
"""
data = {
"ITEM0001": {"_id": 1, "name": "黄焖鸡", "price": 1800, "is_half_price": True},
"ITEM0002": {"_id": 2, "name": "蛋炒饭", "price": 1500, "is_half_price": False},
"ITEM0003": {"_id": 3, "name": "牛肉面", "price": 2200, "is_half_price": True},
"ITEM0004": {"_id": 4, "name": "东北饺子", "price": 1800, "is_half_price": False},
"ITEM0013": {"_id": 13, "name": "肉夹馍", "price": 600, "is_half_price": True},
"ITEM0022": {"_id": 22, "name": "凉皮", "price": 800, "is_half_price": True},
"ITEM0023": {
"_id": 23,
"name": "超级无敌海鲜霸王担担面之超级无敌海鲜霸王担担面",
"price": 3000,
"is_half_price": True,
},
}

## Exception


class ObjectDoesNotExist(Exception):
"""The requested object does not exist"""

pass


class InvalidParams(Exception):
"""Invalid parameters"""

pass


class EmptyInput(Exception):
"""Invalid parameters"""

pass


## Obj


class Item:
def __init__(self, item_id, count):
self.item_info = data.get(item_id)
if self.item_info is None:
raise ObjectDoesNotExist(f"不存在菜品: {item_id}")
self.count = count

def __repr__(self):
return self.name

@property
def price(self) -> int:
return self.item_info.get("price")

@property
def name(self) -> str:
return self.item_info.get("name")

@property
def is_half_price(self) -> bool:
return self.item_info.get("is_half_price", False)

@property
def total(self) -> int:
return self.price * self.count


class Order:
_lock = threading.RLock() # 无论类有多少个实例,都用一个锁

def __init__(self, promotion=None):
self.basket = []
self.promotion = promotion
self.half_price_items = set()

self._final_promotion = None
self._discount = 0
self._finished = False

def add_to_basket(self, *items):
with Order._lock:
self._finished = False
for item in items:
self.basket.append(item)

@property
def final_promotion(self) -> str:
if not self._finished:
self.charge()
return self._final_promotion

@property
def discount(self) -> int:
if not self._finished:
self.charge()
return self._discount

@property
def total(self) -> int:
total = 0
for item in self.basket:
total += item.total
return total

def charge(self):
if not self.promotion:
discount = 0
else:
with Order._lock:
discount = self.promotion.discount(self)
self._finished = True

return self.total - discount


## Promotion
PROMOS = set()


def add_to_promos(func):
"""注册到优惠机制集合"""

PROMOS.add(func)

@wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)

return wrapper


class Promotion(ABC):
"""优惠策略抽象基类"""

@abstractmethod
def discount(self, order):
pass


@add_to_promos
class FullReductionPromo(Promotion):
"""满30减6元"""

def discount(self, order):
return 600 if order.total > 3000 else 0


@add_to_promos
class HalfPricePromo(Promotion):
"""指定菜品半价"""

def discount(self, order):
discount = 0
for item in order.basket:
if item.is_half_price:
order.half_price_items.add(item.name)
discount += item.total / 2
return discount


class BestPromo(Promotion):
"""最佳优惠"""

def discount(self, order):

# 使用装饰器注册代替
# all_promotion = [
# globals()[name]
# for name in globals()
# if name.endswith("Promo") and name != "BestPromo"
# ]

for promo in PROMOS:
discount = promo().discount(order)
if discount != 0 and discount > order._discount:
order._discount = discount
order._final_promotion = promo().__doc__

return order._discount


## utils


def format_output(order):
"""
:return:
============= 订餐明细 =============
黄焖鸡 x 1 = 18元
肉夹馍 x 2 = 12元
凉皮 x 1 = 8元
-----------------------------------
使用优惠:
指定菜品半价(黄焖鸡,凉皮),省13元
-----------------------------------
总计:25元
===================================
"""
head = "============= 订餐明细 =============\n"
divide = "\n-----------------------------------\n"
bottom = "==================================="

items_output = "\n".join(
[
"\n".join(
textwrap.wrap(f"{i.name} x {i.count} = {Decimal(i.total / 100)}元", 20)
)
for i in order.basket
]
)
output = head + items_output + divide
charge_output = f"总计:{Decimal(order.charge() / 100)}元\n"

best_promo = order.final_promotion
if best_promo == "指定菜品半价":
lines = "\n".join(
textwrap.wrap(
f"{best_promo}({','.join(order.half_price_items)}), 省{Decimal(order.discount/100)}元",
20,
)
)
promotion_output = f"使用优惠: \n{lines}"
output += promotion_output + divide
elif best_promo == "满30减6元":
promotion_output = f"使用优惠: \n{best_promo}, 省{Decimal(order.discount/100)}元"
output += promotion_output + divide

output += charge_output + bottom

return output


def verify_input_format(input_item: str) -> None:
pattern = re.compile(r"^ITEM\d+( x ){1}[1-9]+")
if not pattern.match(input_item):
raise InvalidParams("输入格式有误")


def parse_input(input_items: list) -> Dict[str, int]:
if not input_items:
raise EmptyInput("未挑选菜品")

items_map = {}
for i in input_items:
verify_input_format(i)
item_id, count = i.split(" x ")
if item_id in items_map:
items_map[item_id] += int(count)
else:
items_map[item_id] = int(count)

return items_map


## main


def bestCharge(input_items: list) -> str:
items_map = parse_input(input_items)

order = Order(promotion=BestPromo())

order.add_to_basket(
*[Item(item_id, items_map[item_id]) for item_id in items_map.keys()]
)

return format_output(order)


if __name__ == "__main__":
input_demo3 = ["ITEM0001 x 1", "ITEM0001 x 1", "ITEM0013 x 4", "ITEM0013 x 4"]

print(bestCharge(input_demo3))

当时还写了单元测试一起给HR发过去,就不在这写了 : )