背景介绍
据我所知,几乎所有的互联网公司都带有和电商有关的项目,而且在大多数公司里面还是举足轻重的重头戏,比如京东,淘宝。既然有电商项目,必然会涉及到商品,一旦有商品就会有各种促销活动,比如 满 100 减 20, 三八妇女节 9 折等等类似活动。作为一个 coder 怎么才能在实现产品狗的需求下,最小改动代码,最优雅的实现呢。今天菜菜不才,就 D 妹子的问题献丑一番。以下以.netCore c# 代码为例,其他语言类似。
D 妹子版本
首先 D 妹子有一个商品的对象,商品里有一个价格的属性,价格的单位是分
class Product
{
// 其他属性省略
public int Price {get; set;}
}
下面有一个满 100 减 20 的活动,在结算价格的时候代码是这样的
public int GetPrice()
{Product p = new Product();
int ret = p.Price;
if (p.Price >= 100*100)
{ret = ret - 20 * 100;}
return ret;
}
有问题吗?按照需求来说没有问题,而且计算的结果也正确。但是从程序艺术来说,其实很丑陋。现在又有一个全场 9 折的活动,恰巧有一个商品参与了以上两个活动,而且还可以叠加使用 (假设活动参与的顺序是先折扣后满减)。这时候 D 妹子的代码就变成了这样
public int GetPrice()
{Product p = new Product();
// 9 折活动
int ret = p.Price * 90 / 100;
// 满减活动
if (ret >= 100 * 100)
{ret = ret - 20 * 100;}
return ret;
}
假如现在又来一个类似活动,那这块代码还需要修改,严重违反了开放关闭原则,而且频繁修改已经上线的代码,bug 的几率会大大增高。这也是 D 妹子领导骂她并且让她 codereview 的原因。
优化版本
那具体要怎么优化呢?修改代码之前,我还是想提醒一下,有几个要点需要注意一点:
- 商品菜菜认为有一个共同的基类比较好,这样就有了一个所有商品的控制点,为以后统一添加属性留一个入口。好比一个网关系统,为什么会诞生网关这个组件呢,因为有了它我们能方便的统一添加认证,授权,统计等一些列行为。
- 任何促销的活动最好有一个基类,作用类似商品基类。
- 对于商品而言,任何促销活动是商品的行为变化点,影响到的是最终的商品价格,所以获取价格这个行为要做特殊的处理。
- 不同种类的促销活动应该能自行扩展,不会影响别的类型促销活动。
- 不同种类的促销活动能叠加使用 (其实这里涉及到每个活动计算的标准是商品原价还是促销之后价格的问题)。
基于以上几点,首先把商品的对象做一下抽象
// 商品抽象基类
abstract class BaseProduct
{
// 商品价格,单位:分
public int Price {get; set;}
// 获取商品价格抽象方法
public abstract int GetPrice();}
// 抽象商品(比如话费商品),继承商品基类
class VirtualProduct : BaseProduct
{public override int GetPrice()
{return this.Price;}
}
接下来活动的基类也需要抽象出来
// 各种活动的抽象基类,继承要包装的类型基类
abstract class BaseActivity : BaseProduct
{}
有的同学会问,这里为什么要继承商品的基类呢?主要是为了活动的基类能嵌套使用,这样我就可以实现多个活动同时使用,如果不明白没关系,带着这个问题接着往下看
实现一个打折的活动
// 打折活动基类, 支持多个商品同时结算
class DiscountActivity : BaseActivity
{
BaseProduct product = null;
public DiscountActivity(int discount, BaseProduct _product)
{
Discount = discount;
product = _product;
}
// 折扣,比如 90 折 即为 90
public int Discount {get; set;}
// 获取折扣之后的价格
public override int GetPrice()
{return product.GetPrice() * Discount / 100;
}
}
实现一个满减的活动,而且支持自定义满减条件
class ReductionActivity : BaseActivity
{
BaseProduct product = null;
// 满减的对应表
Dictionary<int, int> reductMap = null;
public ReductionActivity(Dictionary<int, int> _redutMap, BaseProduct _product)
{
reductMap = _redutMap;
product = _product;
}
// 获取折扣之后的价格
public override int GetPrice()
{var productAmount = product.GetPrice();
// 根据商品的总价获取到要减的价格
var reductValue = reductMap.OrderByDescending(s => s.Key).FirstOrDefault(s => productAmount >= s.Key).Value;
return productAmount - reductValue;
}
}
现在我们来给商品做个促销活动吧
VirtualProduct p = new VirtualProduct() { Price=1000};
// 打折活动
DiscountActivity da = new DiscountActivity(90, p);
var retPrice= da.GetPrice();
Console.WriteLine($"打折后的价格 {retPrice}");
// 还能叠加参加满减活动
Dictionary<int, int> m = new Dictionary<int, int>() ;
m.Add(200, 5); // 满 200 减 5
m.Add(300, 10);
m.Add(500, 20);
m.Add(1000, 50);
// 这里活动能叠加使用了
ReductionActivity ra = new ReductionActivity(m, da);
retPrice = ra.GetPrice();
Console.WriteLine($"打折满减后的价格 {retPrice}");
ReductionActivity ra2 = new ReductionActivity(m, ra);
retPrice = ra2.GetPrice();
Console.WriteLine($"再打折后的价格 {retPrice}");
输出结果:
打折后的价格 900
打折满减后的价格 880
再打折后的价格 860
现在我们终于能优雅一点的同时进行商品的满减和打折活动了
进化到多个商品同时促销
以上代码已经可以比较优雅的能进行单品的促销活动了,但是现实往往很骨感,真实的电商场景中多以多个商品结算为主,那用同样的思路怎么实现呢?
- 由于这次需要实现的是多商品促销结算,所以需要一个自定义的商品列表来作为要进行结算的对象。此对象行为级别上与单品类似,有一个需求变化点的抽象:获取价格
// 商品列表的基类, 用于活动结算使用
class ActivityListProduct : List<BaseProduct>
{
// 商品列表活动结算的方法,基类必须重写
public virtual int GetPrice()
{
int ret = 0;
base.ForEach(s =>
{ret += s.GetPrice();
});
return ret;
}
}
- 把多商品促销活动的基类抽象出来,供不同的促销活动继承使用, 这里需要继承 ActivityListProduct,为什么呢?和单品的类似,为了多个子类能够嵌套调用
// 商品列表 活动的基类,继承自商品列表基类
internal abstract class BaseActivityList : ActivityListProduct
{}
- 创建一个打折和满减活动
// 打折活动基类, 支持多个商品同时结算
class DiscountActivityList : BaseActivityList
{
ActivityListProduct product = null;
public DiscountActivityList(int discount, ActivityListProduct _product)
{
Discount = discount;
product = _product;
}
// 折扣,比如 90 折 即为 90
public int Discount {get; set;}
public override int GetPrice()
{var productPrice = product.GetPrice();
return productPrice * Discount / 100;
}
}
// 满减的活动
class ReductionActivityList : BaseActivityList
{
ActivityListProduct product = null;
// 满减的对应表
Dictionary<int, int> reductMap = null;
public ReductionActivityList(Dictionary<int, int> _redutMap, ActivityListProduct _product)
{
reductMap = _redutMap;
product = _product;
}
// 获取折扣之后的价格
public override int GetPrice()
{var productAmount = product.GetPrice();
// 根据商品的总价获取到要减的价格
var reductValue = reductMap.OrderByDescending(s => s.Key).FirstOrDefault(s => productAmount >= s.Key).Value;
return productAmount - reductValue;
}
}
先来一波多商品促销活动
VirtualProduct p = new VirtualProduct() { Price = 1000};
VirtualProduct p2 = new VirtualProduct() { Price = 1000};
ActivityListProduct lst = new ActivityListProduct();
lst.Add(p);
lst.Add(p2);
DiscountActivityList dalist = new DiscountActivityList(80, lst);
Console.WriteLine($"打折后的价格 {dalist.GetPrice()}");
DiscountActivityList dalist2 = new DiscountActivityList(90, dalist);
Console.WriteLine($"打折后的价格 {dalist2.GetPrice()}");
DiscountActivityList dalist3 = new DiscountActivityList(90, dalist2);
Console.WriteLine($"打折后的价格 {dalist3.GetPrice()}");
// 还能叠加参加满减活动
Dictionary<int, int> m = new Dictionary<int, int>();
m.Add(200, 5); // 满 200 减 5
m.Add(300, 10);
m.Add(500, 20);
m.Add(1000, 50);
ReductionActivityList ral = new ReductionActivityList(m, dalist3);
Console.WriteLine($"再满减打折后的价格 {ral.GetPrice()}");
结算结果:
打折后的价格 1600
打折后的价格 1440
打折后的价格 1296
再满减打折后的价格 1246
现在基本上可以让 D 妹子不被挨骂了
神化版本见留言区
知道 D 妹子为什么取名 D 妹子吗?
添加关注,查看更精美版本,收获更多精彩