文章目录
01 饮品店订单管理业务介绍
笔者初学设计模式的时候,觉得他太过抽象,学了之后就容易忘 😵 ,所以本文就想深入浅出结合实例来学习设计模式。文中一共应用工厂模式、装饰模式、策略模式和观察者模式四种设计模式来实现一个功能完备的饮品店订单管理业务。
饮品店饮品店,首先需要有丰富多样的饮品种类 🍺🍸🍹🍷,本饮品店出售三种品类的饮料,分别是果汁(Fruit Juice)、咖啡(Coffee)、奶茶(Milk Tea):
1. 果汁品类有三种饮品包括:草莓汁(Strawberry juice)、芒果汁(Mango juice)和西瓜汁(Watermelon juice);
2. 咖啡品类有:卡布奇诺(Cappuccino)、魔卡(Mocha)和香草拿铁(Vanilla Latte)三种饮品;
3. 奶茶品类包括:香港奶茶(Hong Kong Milk Tea)、抹茶奶茶(Matcha Milk Tea)和麦香奶茶(Wheat Fragrant Milk Tea)。
当然饮品店不可能一年四季都卖相同的东西 🍻 ,所以订单管理业务要考虑饮品种类和新品增加的情况,1️⃣ 我们采用工厂模式把具体类的实例化交给子类动态处理。
除了考虑饮品品类增加之外,还应该考虑饮品的不同规格和不同品类饮品的不同小料和调味品等更多个性化的服务,当然小料和调味品的种类也应该是可动态改变的 🍬。我们考虑果汁和奶茶有中杯(Middle)、大杯(Large)、超大杯(Oversize)三种规格;奶茶还可以添加红豆(RedBeans)、布丁(Pudding)和珍珠(Pearl)三种小料;咖啡则可以添加Milk、Soy和Whip三种调味品。2️⃣ 并采用装饰模式实现向现有的饮品对象添加新的附加品,创建了一个装饰类,用来包装原有的类,并在保持类方法签名完整性的前提下,提供了额外的功能。
品类不同、加料不同、规格不同,这时饮品价格也不一 💴 。所以,订单管理业务也需要考虑饮品的计费问题,不同的饮品价格不一,与此同时饮品不同的规格应该有不一样的价格,当然添加小料和调味品也需要增加额外的费用。
计费问题基础上我们还需要考虑我们饮品店的会员体系,分为普通顾客(General Customer)、白银会员(Silver Member)、黄金会员(Gold Member)和铂金会员(Platinum Member)📀。不同的会员享有不同的购物折扣,例如白银会员享有九折优惠、黄金会员享有八折优惠、铂金会员则享有六折优惠,除此之外,铂金会员每次购物都将获得额外赠品。3️⃣ 我们采用策略模式实现不同会员等级顾客的不同优惠策略。
最后,订单管理业务需要告知后厨顾客具体饮品需求 🔈,也要告知前台顾客消费的具体费用 🔉。顾客下单和订单处理存在一对多关系,4️⃣ 我们使用观察者模式,当一个顾客订单到达时,则会自动通知依赖它的后厨观察对象和前台观察对象。
02 丰富品类 工厂模式
2.1 抽象工厂模式
抽象工厂模式是围绕一个超级工厂创建其他工厂,是一种创建型的设计模式,它提供了一种📌 创建对象的最佳方式 📌。在抽象工厂模式中,接口是负责创建一个相关对象的工厂,不需要显式指定它们的类,每个生成的工厂都能按照工厂模式提供对象。
抽象工厂模式的结构 📑
抽象工厂模式结构中包括抽象工厂、具体工厂、抽象产品、具体产品:
- 抽象工厂提供了创建产品的接口,它包含多个创建产品的方法,可以创建多个不同等级的产品;
- 具体工厂实现抽象工厂中的多个抽象方法,完成具体产品的创建;
- 抽象产品定义了产品规范,描述了产品的主要特性和功能,抽象工厂模式有多个抽象产品;
- 具体产品实现了抽象产品角色多定义的接口,有具体工厂来创建,它同具体工厂之间是多对一的关系。
抽象工厂模式的优点 🎈
抽象工厂模式隔离了具体类的生成, 使得客户并不需要知道什么被创建。 由于这种隔离,更换一个具体工厂就变得相对容易, 所有的具体工厂都实现了抽象工厂中定义的那些公共接口,因此只需改变具体工厂的实例,就可以在某种程度上改变整个软件系统的行为。
当一个族中的多个对象被设计成一起工作时,它能够保证客户端始终只使用同一个族中的对象。另外,增加新的族很方便,无须修改已有系统,符合“开闭原则”。
抽象工厂模式的缺点 💣
增加新的等级结构麻烦,需要对原有系统进行较大的修改,甚至需要修改抽象层代码,这显然会带来较大的不便, 违背了“开闭原则”。
2.2 抽象工厂实现饮品多品类
订单管理业务中采用抽象工厂模式针对饮品店出售的三种品类饮料果汁、咖啡和奶茶具体类的实例化交给子类动态处理,实现新品类增加时代码修改的OCP原则,类图如下所示:
BeverageStore作为抽象工厂提供了创建产品的接口,它包含创建产品的方法,可以创建多个不同类型的产品,其编码实现如下所示:
package main.java.com.beverageStore.factory;
public abstract class BeverageStore {
abstract Beverage createBeverage(String item);
public Beverage orderBeverage(String type) {
Beverage beverage = createBeverage(type);
System.out.println("--- Making a " + beverage.getName() +" ---");
return beverage;
}
}
FruitJuice、Coffee和MilkTea作为具体工厂实现抽象工厂中的多个抽象方法,完成具体产品的创建,其中FruitJuice类的编码实现如下所示,其他具体工厂的编码实现与其类似。
package main.java.com.beverageStore.factory;
public class FruitJuice extends BeverageStore {
@Override
Beverage createBeverage(String item) {
// TODO Auto-generated method stub
if(item.equals("StrawberryJuice")) {
return new StrawberryJuice();
}
else if(item.equals("MangoJuice")) {
return new MangoJuice();
}
else if(item.equals("WatermelonJuice")) {
return new WatermelonJuice();
}
else {
return null;
}
}
}
Beverage抽象产品定义了饮品产品规范,描述了产品的主要特性和功能,一般抽象工厂模式可以有多个抽象产品,订单管理业务中只使用一个,Beverage抽象类的编码实现如下所示:
package main.java.com.beverageStore.factory;
public abstract class Beverage {
protected String name;
protected Double price;
protected String description;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public double getPrice() {
return price;
}
public void setPrice(Double price) {
this.price = price;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public abstract Double cost();
}
如类图所示本饮品店订单管理业务有九个具体产品,具体产品实现了抽象产品角色多定义的接口,又由具体工厂来创建,它同具体工厂之间是多对一的关系,以具体产品StrawberryJuice类为例介绍具体产品编码实现如下所示:
package main.java.com.beverageStore.factory;
public class StrawberryJuice extends Beverage {
public StrawberryJuice() {
// TODO Auto-generated constructor stub
this.name = "Strawberry Juice";
this.description = "Strawberry Juice";
this.price = 10.0;
}
@Override
public Double cost() {
// TODO Auto-generated method stub
return this.getPrice();
}
}
03 随便加料 装饰器模式
3.1 装饰器模式
装饰器模式指的是在不必改变原类文件和使用继承的情况下,动态地扩展一个对象的功能。它是通过创建一个包装对象,也就是装饰来包裹真实的对象。
装饰器模式的结构 📑
装饰器模式主要包括抽象构件角色、具体构件角色、装饰抽象角色和具体装饰角色:
- 抽象构件角色定义了一个对象接口,可以给这些对象动态地添加职责;
- 具体构件角色定义一个具体对象,也可以给这个对象添加一些职责;
- 装饰抽象角色是装饰抽象类,实现接口或抽象方法;
- 具体装饰角色起到给构件对象添加职责的功能。
装饰器模式的优点 🎈
装饰器对象可以在转发这些请求以前或以后增加一些附加功能,这样就确保了在运行时,不用修改给定对象的结构就可以在外部增加附加的功能,在面向对象的设计中,通常是通过继承来实现对给定类的功能扩展。
装饰器模式与继承关系的目的都是要扩展对象的功能,但是装饰器可以提供比继承更多的灵活性。通过使用不同的具体装饰类以及这些装饰类的排列组合,设计师可以创造出很多不同行为的组合。
装饰器模式的缺点 💣
这种比继承更加灵活机动的特性,也同时意味着更加多的复杂性。装饰器模式会导致设计中出现许多小类,如果过度使用,会使程序变得很复杂。
装饰器模式是针对抽象组件类型编程。但是,如果你要针对具体组件编程时,就应该重新思考你的应用架构,以及装饰者是否合适。当然也可以改变抽象构件接口,增加新的公开的行为,实现“半透明”的装饰器模式。在实际项目中要做出最佳选择。
3.2 装饰器实现加小料和饮品规格
订单管理业务中采用装饰器模式考虑饮品的不同规格和不同品类饮品的不同小料和调味品,小料和调味品的种类也是可以动态修改的,类图如下所示:
Beverage作为抽象构件角色定义了具体饮品对象的接口,可以给这些对象动态地添加附加功能,与工厂模式中的Beverage抽象产品角色一致,编码实现和上面相同,主要体现在description属性和cost方法的装饰内容。
具体构件角色定义一个具体对象,也可以给这个对象添加一些职责,以 HongKongMilkTea 为例编码实现如下:
package main.java.com.beverageStore.factory;
public class HongKongMilkTea extends Beverage {
public HongKongMilkTea() {
// TODO Auto-generated constructor stub
this.name = "HongKongMilkTea";
this.description = "HongKongMilkTea";
this.price = 12.0;
}
@Override
public Double cost() {
// TODO Auto-generated method stub
return this.getPrice();
}
}
订单管理业务中饮品店的订单管理业务有两个装饰抽象角色分别是饮品的规格和饮品可添加的小料与调味品,以Weight抽象角色为例编码实现接口如下:
package main.java.com.beverageStore.decorater;
import main.java.com.beverageStore.factory.Beverage;
public abstract class Weight extends Beverage {
Beverage beverage;
Weight(Beverage decoratedBeverage){
this.beverage = decoratedBeverage;
}
public String getDescription() {
return beverage.getDescription() + " " + super.getDescription();
}
public Double cost() {
// TODO Auto-generated method stub
return super.getPrice()+beverage.cost();
}
}
具体装饰角色起到给构件对象添加职责的功能,Weight有Middle、Large和Oversize三各具体装饰对象,以Middle为例编码实现如下:
package main.java.com.beverageStore.decorater;
import java.math.BigDecimal;
import main.java.com.beverageStore.factory.Beverage;
public class Middle extends Weight {
public Middle(Beverage beverage) {
// TODO Auto-generated constructor stub
super(beverage);
this.setPrice(0.0);
this.setDescription("Middle");
}
}
04 会员折扣 策略模式
4.1 策略模式
策略模式定义了一系列算法,并将每个算法封装起来,让它们之间可以互相替换,让算法的变化独立于算法的使用者。
策略模式的原则就是:分离变化部分,封装接口,基于接口编程各种功能。
此模式让行为的变化独立于算法的使用者。在策略模式中,我们创建表示各种策略的对象和一个行为随着策略对象改变而改变的 context 对象,策略对象改变 context 对象的执行算法。
策略模式的结构 📑
策略模式主要由抽象策略角色、具体策略角色和环境角色组成:
- 抽象策略角色是一个抽象角色,通常由一个接口或抽象类实现,此角色给出所有的具体策略类所需的接口;
- 具体策略角色封装了具体的算法和行为,实现抽象策略中的操作;
- 环境角色,也叫context角色,它屏蔽了高层模块对策略、算法的直接访问,封装可能存在的变化,内部会持有具体策略给客户调用。
策略模式的优点 🎈
策略模式提供了管理相关的算法族的办法。恰当使用继承可以把公共的代码转移到父类里面,从而避免重复的代码。
策略模式提供了可以替换继承关系的办法。继承可以处理多种算法或行为,如果不是用策略模式,那么使用算法或行为的环境类就可能会有一些子类,每一个子类提供一个不同的算法或行为。但是,这样一来算法或行为的使用者就和算法或行为本身混在一起。决定使用哪一种算法或采取哪一种行为的逻辑就和算法或行为的逻辑混合在一起,从而不可能再独立演化。继承使得动态改变算法或行为变得不可能。
使用策略模式可以避免使用多重条件转移语句。多重转移语句不易维护,它把采取哪一种算法或采取哪一种行为的逻辑与算法或行为的逻辑混合在一起,统统列在一个多重转移语句里面,比使用继承的办法还要原始和落后。
策略模式的缺点 💣
客户端必须知道所有的策略类,并自行决定使用哪一个策略类。这就意味着客户端必须理解这些算法的区别,以便适时选择恰当的算法类。换言之,策略模式只适用于客户端知道所有的算法或行为的情况。
策略模式造成很多的策略类,每个具体策略类都会产生一个新类。有时候可以通过把依赖于环境的状态保存到客户端里面,而将策略类设计成可共享的,这样策略类实例可以被不同客户端使用。换言之,可以使用享元模式来减少对象的数量。
4.2 策略模式实现不同类型会员折扣
订单管理业务中采用策略模式实现饮品店设立的会员体系中普通顾客、白银会员、黄金会员和铂金会员享有不同的优惠策略,类图如下所示:
Gift和Discount作为抽象策略角色用接口实现,这两个角色分别给出了顾客是否享有赠品和对应折扣优惠的具体策略类所需的接口,以Distcount为例其接口编码实现如下所示:
package main.java.com.beverageStore.strategy;
import java.math.BigDecimal;
public interface Discount {
public BigDecimal priceAfterDiscount(BigDecimal orderPrice);
}
普通顾客、白银会员、黄金会员和铂金会员对应的赠品和折扣具体策略角色封装了具体的算法和行为,实现了Gift和Discount抽象策略中的操作,以铂金会员折扣具体策略角色PlatinumMemberDiscount类编码实现为例,如下所示:
package main.java.com.beverageStore.strategy;
import java.math.BigDecimal;
public class PlatinumMemberDiscount implements Discount {
@Override
public BigDecimal priceAfterDiscount(BigDecimal orderPrice) {
// TODO Auto-generated method stub
int payPrice = orderPrice.intValue();
return new BigDecimal(payPrice * 0.6);
}
}
Member抽象类作为环境角色,它屏蔽了高层模块对策略、算法的直接访问,封装可能存在的变化,内部会持有具体的赠品Gift和折扣Discount策略给客户调用,其编码实现如下所示:
package main.java.com.beverageStore.strategy;
import java.math.BigDecimal;
public abstract class Member {
Gift gift;
Discount discount;
private String rank;
public Member(){};
public void setGift(Gift gf) {
gift = gf;
}
public void setDiscount(Discount dis) {
discount = dis;
}
public void setRank(String rk) {
rank = rk;
}
public void ifHasGift() {
gift.giveaway();
}
public double whichDiscount(BigDecimal orderPrice) {
return discount.priceAfterDiscount(orderPrice).doubleValue();
}
public String getRank() {
return rank;
}
}
05 订单通知 观察者模式
5.1 观察者模式
观察者模式又称为发布-订阅模式,该模式定义了一种,一对多的依赖关系,让多个观察者同时监听一个主题对像,这个主题对像在状态发生改变时,会通知所有的观察者对像更新。
观察者模式的结构 📑
观察者模式中包括抽象主题角色、具体主题角色、抽象观察者角色和具体观察者角色:
- 抽象主题角色它所有观察者对象的引用保存在一个集合中,每个主题都可以有任意数量的观察者;
- 具体主题角色中具体主题是抽象主题接口实现类,用来管理观察者对像的增加,删除,通知;
- 抽象观察者角色为所有的具体观察者定义一个接口,在得到主题通知时更新自己;
- 具体观察者角色实现抽象观察者角色所需要的更新接口,执行主题通知的更新操作。
观察者模式的优点 🎈
观察者模式中具体主题和具体观察者是松耦合关系。由于主题接口仅仅依赖于观察者接口,因此具体主题只是知道它的观察者是实现观察者接口的某个类的实例,但不需要知道具体是哪个类。
同样,由于观察者仅仅依赖于主题接口,因此具体观察者只是知道它依赖的主题是实现主题接口的某个类的实例,但不需要知道具体是哪个类。
观察者模式的缺点 💣
如果对观察者的通知是通过另外的线程进行异步投递的话,系统必须保证投递是以自恰的方式进行的。
如果一个被观察者对象有很多的直接和间接的观察者的话,将所有的观察者都通知到会花费很多时间。
如果在观察者和观察目标之间有循环依赖的话,观察目标会触发它们之间进行循环调用,可能导致系统崩溃。
观察者模式没有相应的机制让观察者知道所观察的目标对象是怎么发生变化的,而仅仅只是知道观察目标发生了变化。
5.2 观察者模式实现即时订单通知
订单管理业务中采用观察者模式实现饮品店后厨和前台两个不同的观察者对象同时监听顾客订单,类图如下所示:
Customer作为抽象主题角色把所有顾客订单的引用保存在一个集合中,该主题都可以有任意数量的观察者,Customer抽象主题接口的编码实现如下:
package main.java.com.beverageStore.observer;
public interface Customer {
/**
* 注册一个观察着
* @param user
*/
public void registerObserver(Observer observer);
/**
* 移除一个观察者
* @param observer
*/
public void removeObserver(Observer observer);
// 通知所有的观察着
public void notifyObservers();
}
具体主题角色ImplCustomer中具体主题是抽象主题Customer接口的实现类,用来管理观察者对像的增加,删除,通知,ImplCustomer的编码实现如下:
package main.java.com.beverageStore.observer;
import java.util.ArrayList;
import java.util.List;
public class ImplCustomer implements Customer {
private List<Observer> observers = new ArrayList<Observer>();
private String msg;
@Override
public void registerObserver(Observer observer) {
// TODO Auto-generated method stub
observers.add(observer);
}
@Override
public void removeObserver(Observer observer) {
// TODO Auto-generated method stub
int index = observers.indexOf(observer);
if (index >= 0)
{
observers.remove(index);
}
}
@Override
public void notifyObservers() {
// TODO Auto-generated method stub
for (Observer observer : observers)
{
observer.update(msg);
}}
public void setMsg(String msg){
this.msg = msg;
notifyObservers();
}
}
Observer作为抽象观察者角色为所有的具体观察者定义一个接口,让具体观察者在得到主题通知时更新自己,Observer的编码实现如下所示:
package main.java.com.beverageStore.observer;
public interface Observer {
public void update(String msg);
}
订单管理业务中有两个具体观察者角色分别是后厨观察者BackKitchen和前台观察者CounterObserver,它们实现抽象观察者角色Observer所需要的更新接口,执行主题通知的更新操作,以BackKitchenObserver为例编码实现如下:
package main.java.com.beverageStore.observer;
public class BackKitchenObserver implements Observer {
private Customer customer;
public BackKitchenObserver(Customer customer) {
this.customer = customer;
customer.registerObserver(this);
}
public void update(String msg) {
System.out.println("The customer has ordered the " + msg);
}
}
06 总结
在学习和使用设计模式的过程中,学习初期 🙋 大部分同学可能会觉得设计模式很复杂,认为模式种类太多,使用情景不一,很容易混淆,不知道在什么样的情景下使用合适的设计模式。
随着学习更加深入 🙇,大家需要意识到的是:设计模式并不是一成不变不可修改的,也不是必须按照某种标准严格遵守的。恰恰相反,设计模式中最核心的要素并非设计的结构,而是设计的思想。只有掌握住设计模式的核心思想,才能正确、灵活的应用设计模式,否则再怎么使用设计模式,也不过是生搬硬套。
当然,掌握设计模式的思想,关键是要仔细研究模式的意图和结构 🎅。一个模式的意图,就是使用这个设计模式的目的,体现了为什么要使用这个模式,也就是需求问题。这个模式的结构,就是如何去解决这个问题,是一种手段、一种经典的解决方法,这种解决方法只是一种建议。两个方面结合起来,明白为什么需要设计模式,同时明白了如何实现这个模式,就容易抓住模式的本质思想。
在抓住意图和结构的基础上,实践也是掌握设计模式的必要方法 👷。当然,设计模式必须在某个场景下得到应用才有意义,通过大量的实例理解模式的应用场景,灵活掌握设计模式的思想。
👉 项目源码点击这里 👈https://github.com/wang-haihua/TechStack/tree/master/DesignPattern/BeverageStore