今天做kama网上模版方法模式的题目18-咖啡馆,共经历了三次修改。其中最后一次优化在枚举类中加入了工厂,用到函数式接口以及方法引入的相关知识。这里通过对这次实践做一下总结,深化一下对模版方法模式、函数式接口以及方法引入的理解。
改进过程
第一次实现
import java.util.Scanner;
abstract class AbstractCoffeeModel{
// 注意用private的话,子类看不到
// private String coffeeName;
protected String coffeeName;
public AbstractCoffeeModel(String coffeeName) {
this.coffeeName = coffeeName;
}
public final void makeCoffee(){
System.out.println("Making " + coffeeName + ":");
doGrind();
doBrew();
if (this.coffeeName.equals("Latte")) {
doAddMilk();
}
doAddAondiments();
System.out.println();
}
// 定义抽象方法
protected abstract void doGrind();
protected abstract void doBrew();
protected abstract void doAddAondiments();
// 美式不需要实现;
protected abstract void doAddMilk();
enum CoffeeType{
Americano(1, "American Coffee"),
Latte(2, "Latte")
;
private int code;
private String name;
// 构造函数
CoffeeType(int code, String name) {
this.code = code;
this.name = name;
}
// 获取code
public int getCode() {
return code;
}
// 获取name
public String getName() {
return name;
}
// 根据code获取对应的name
public static String getNameByCode(int code) {
for (CoffeeType type : CoffeeType.values()) {
if (type.getCode() == code) {
return type.getName();
}
}
return null; // 如果没有匹配到,则返回null或抛出异常
}
}
}
class ConcreteCoffeeModel extends AbstractCoffeeModel {
// public ConcreteCoffeeModel(String coffeeName) {
// this.coffeeName = coffeeName;
// }
public ConcreteCoffeeModel(String coffeeName) {
super(coffeeName);
}
@Override
protected void doGrind() {
System.out.println("Grinding coffee beans");
}
@Override
protected void doBrew() {
System.out.println("Brewing coffee");
}
@Override
protected void doAddAondiments() {
System.out.println("Adding condiments");
}
@Override
protected void doAddMilk() {
System.out.println("Adding milk");
}
}
public class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
while (scanner.hasNext()){
int num = scanner.nextInt();
String coffeeName = AbstractCoffeeModel.CoffeeType.getNameByCode(num);
ConcreteCoffeeModel coffeeModel = new ConcreteCoffeeModel(coffeeName);
coffeeModel.makeCoffee();
}
}
}
第一次改进 – 模版方法模式
上面的实现,虽然实现了需求,但是将具体的条件逻辑(如判断咖啡种类的 if 语句)放在抽象类中是不符合模板方法模式原则的。这会导致在添加新类型的咖啡时,需要修改抽象模板类,从而违背了模板方法模式应有的开放封闭原则(Open-Closed Principle)。正确的模版方法实现应该是下面这样的:
import java.util.Scanner;
enum CoffeeType{
Americano(1, "American Coffee"),
Latte(2, "Latte")
;
private int code;
private String name;
// 构造函数
CoffeeType(int code, String name) {
this.code = code;
this.name = name;
}
// 获取code
public int getCode() {
return code;
}
// 获取name
public String getName() {
return name;
}
}
abstract class AbstractCoffeeModel{
// 注意用private的话,子类看不到
// private String coffeeName;
protected String coffeeName;
public AbstractCoffeeModel(String coffeeName) {
this.coffeeName = coffeeName;
}
public final void makeCoffee(){
System.out.println("Making " + coffeeName + ":");
doGrind();
doBrew();
doAddAondiments();
System.out.println();
}
// 定义抽象方法
protected abstract void doGrind();
protected abstract void doBrew();
protected abstract void doAddAondiments();
}
class AmericanoCoffeeModel extends AbstractCoffeeModel {
// public ConcreteCoffeeModel(String coffeeName) {
// this.coffeeName = coffeeName;
// }
public AmericanoCoffeeModel() {
super(CoffeeType.Americano.getName());
}
@Override
protected void doGrind() {
System.out.println("Grinding coffee beans");
}
@Override
protected void doBrew() {
System.out.println("Brewing coffee");
}
@Override
protected void doAddAondiments() {
System.out.println("Adding condiments");
}
}
class LatteCoffeeModel extends AbstractCoffeeModel {
// public ConcreteCoffeeModel(String coffeeName) {
// this.coffeeName = coffeeName;
// }
public LatteCoffeeModel() {
super(CoffeeType.Latte.getName());
}
@Override
protected void doGrind() {
System.out.println("Grinding coffee beans");
}
@Override
protected void doBrew() {
System.out.println("Brewing coffee");
}
@Override
protected void doAddAondiments() {
System.out.println("Adding milk");
System.out.println("Adding condiments");
}
}
public class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
while (scanner.hasNext()){
int num = scanner.nextInt();
if(num == CoffeeType.Americano.getCode()) {
AmericanoCoffeeModel coffeeModel = new AmericanoCoffeeModel();
coffeeModel.makeCoffee();
} else if (num == CoffeeType.Latte.getCode()) {
LatteCoffeeModel coffeeModel = new LatteCoffeeModel();
coffeeModel.makeCoffee();
}
}
}
}
这样,后续如果要新增咖啡种类,就只需要新增枚举值,创建新的继承自模版类的具体咖啡类即可。
第二次改进 — 枚举工厂方法
可以看到,在客户端代码中,仍然存在if-else
语句,这是不太优雅的,可以在继续在枚举类中加入工厂方法,实现动态地创建具体实现类(美式还是拿铁)。即:
• 使用枚举工厂方法:将具体的咖啡创建逻辑封装在枚举中,使得 Main 类可以通过枚举创建具体的咖啡对象,增加新的咖啡类型时不需要修改 Main 类。
最终代码:
import java.util.Scanner;
enum CoffeeType{
//AmericanoCoffeeModel::new 是一个方法引用,指向 AmericanoCoffeeModel 的构造函数。这意味着当调用 factory.create() 时,它会使用 AmericanoCoffeeModel 的构造函数创建一个新的实例。
Americano(1, "American Coffee", AmericanoCoffeeModel::new),
Latte(2, "Latte", LatteCoffeeModel::new)
;
private int code;
private String name;
private CoffeeModelFactory factory;
// 构造函数
CoffeeType(int code, String name, CoffeeModelFactory factory) {
this.code = code;
this.name = name;
this.factory = factory;
}
public int getCode() {
return code;
}
public String getName() {
return name;
}
public static CoffeeType getByCode(int code) {
for (CoffeeType type : CoffeeType.values()) {
if (type.getCode() == code) {
return type;
}
}
return null;
}
/**
* 使用工厂方法创建具体的咖啡模型实例
*
* @return 具体的咖啡模型实例
*/
public AbstractCoffeeModel createCoffeeModel() {
return factory.create();
}
/**
* 创建具体咖啡模型实例的工厂接口
*/
@FunctionalInterface // 函数式方法,只能拥有一个抽象方法。
private interface CoffeeModelFactory {
AbstractCoffeeModel create();
}
}
public class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
while (scanner.hasNext()){
int num = scanner.nextInt();
CoffeeType.getByCode(num).createCoffeeModel().makeCoffee();
}
}
}
接下来,详细解释一下这个枚举类:
1. 首先,创建一个接口 CoffeeModelFactory,用于生成 AbstractCoffeeModel 的实例。因为 CoffeeModelFactory 只有一个抽象方法 create(),它符合函数式接口的要求,可以被用作方法引用的目标。
/**
* 创建具体咖啡模型实例的工厂接口
*/
@FunctionalInterface // 函数式方法,只能拥有一个抽象方法。
private interface CoffeeModelFactory {
AbstractCoffeeModel create();
}
- @FunctionalInterface 注解:标记这个接口是一个函数式接口,确保接口中只有一个抽象方法。这个注解并非必需,但它可以帮助编译器检查接口是否符合函数式接口的要求。
2. 提供工厂生产咖啡模型实例的方法,调用工厂的create()
方法。
/**
* 使用工厂方法创建具体的咖啡模型实例
*
* @return 具体的咖啡模型实例
*/
public AbstractCoffeeModel createCoffeeModel() {
return factory.create();
}
3. 使用方法引用创建工厂实例:在 CoffeeType
枚举中,通过方法引用(ClassName::new)将 AmericanoCoffeeModel
和 LatteCoffeeModel
的构造函数分别绑定到 CoffeeModelFactory
接口的 create()
方法的实现上。这样,当我们需要创建相应的咖啡模型实例时,就可以通过 createCoffeeModel()
方法获取。
ClassName::new
这种方法引用用于调用ClassName
的构造函数。它表示创建ClassName
类型的新实例。
Americano(1, "American Coffee", AmericanoCoffeeModel::new),
Latte(2, "Latte", LatteCoffeeModel::new)
;
总结
1 模版方法模式
要记住模版方法模式优化后,不应该在抽象模版类中进行判断。对于要求不同的对象(美式或拿铁),算法的框架(模版方法)是一致的,创建不同的具体继承类来实现不同对象的差异化要求。
开闭原则
系统对扩展开放,对修改关闭。新增功能时,通过添加新子类即可,不需要修改已有代码。
这样以后增加新的对象(比如卡布奇诺)时,就不需要改变模版方法,只需要增加新的具体继承类即可。这样才遵循设计模式的开闭原则!
2 函数式接口和方法引入
【bilibili】Java语法中的方法引用::是什么?
https://www.bilibili.com/video/BV1je4y1c79s?vd_source=dd1bad3ca79b4f5adadcfe93c604c7b5