汉堡中的设计模式——模板方法

x33g5p2x  于2021-11-14 转载在 其他  
字(5.8k)|赞(0)|评价(0)|浏览(212)

艺术来源于生活,有时候灵感真的就在那么一瞬间

看到上图这个板烧没有,这就是我今晚的晚餐了;走进麦当劳里面,有很多很多的汉堡

板烧鸡腿堡、麦辣鸡腿堡、麦香堡、深海鳕鱼堡....

这些个汉堡的制作方式,似乎有着一种很明显的相似感,汉堡的制作方式非常像之前学习过的一种设计模式,今天我们就来聊一聊

我们不妨更加仔细的思考一下上面的问题

  1. 我要做一个【板烧鸡腿堡】

  2. 取出两块面包,烤热

  3. 放上烤热的腌制好的鸡腿肉,撒上独特的酱汁

  4. 撒上蔬菜

  5. 组合成汉堡

  6. 装在纸袋里面

  7. 我要做一个【麦辣鸡腿堡】

  8. 取出两块面包,烤热

  9. 放上炸制好的鸡腿肉,撒上独特的酱汁

  10. 撒上蔬菜

  11. 组合成汉堡

  12. 装在纸袋里面

  13. 我要做一个【新奥尔良烤鸡腿堡】

  14. 取出两块面包,烤热

  15. 放上新奥尔良鸡腿肉,撒上独特的酱汁

  16. 撒上蔬菜

  17. 组合成汉堡

  18. 装在纸袋里面

  19. 制作其他汉堡......

有图有真相

可以发现,除了步骤2存在差异之外,其他步骤其实都是一样的,那么我们是否可以将步骤2给抽取出来,其他步骤都不变

就好像模板一般

那么我们是否可以这么做,改造一下模型图,将不变的写在超类里面,变化的由子类决定

这就是模板设计模式的思想,下面是《Head First 设计模式》对模板设计模式的解释
模板方法模式:在一个方法中定义一个算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤

我们现在就使用模板设计模式来改造一下我们的模型图

改造后的模型图

从上图就很清晰的看到,模板设计的理念就是定义一个骨架,把公用部分挪到超类中,同时又把会变化的步骤交给子类来决定

世间众多的设计模式,都要做到一件事,那就是 封装变化点

何谓封装变化点?

  • 模板设计模式:把【个性化操作(这就是变化点)】操作延迟到未来实现,最后插入到整个定义好步骤的算法中
  • 类似的还有策略模式,同样是封装算法家族
  • 还有桥接模式,实例被定义成抽象的,通过接口交互,允许实例自由的变化而不会影响到另外的实例
  • 还有很多很多......

既然模型图出来了,那么就按照模型图来把代码敲一敲吧,天上飞的理念还是得有落地的那一刻,这是很重要的,实践出真知

/**
 * 定义制作汉堡包的模板流程
 * @author Amg
 * @date 2021/9/24
 */
public abstract class AbstractMakeHamburgerHandler {
	
	//定义层final,确保子类不会把我“骨架”都修改了
	public final void execute() {
		baking();
		customize();
		vegetable();
		compose();
		finish();
		hook();
	}
	
	private void baking() {
		System.out.println("取出两块面包,烤热");
	}
	
	/**
	 * 自定义步骤,可以添加你想添加的食材
	 */
	protected abstract void customize();
	
	private void compose() {
		System.out.println("组合汉堡");
	}
	
	private void vegetable() {
		System.out.println("撒上蔬菜");
	}
	
	private void packing() {
		System.out.println("用纸袋包裹着");
	}
	
	private void finish() {
		System.out.println("完成制作!请慢用");
	}
    
	protected void hook() {
		//这个hook方法就是一个程序“钩子”,待会会解释
	}
}

/**
 * 板烧鸡腿堡
 * @author Amg
 * @date 2021/9/24
 */
public class banshao extends AbstractMakeHamburgerHandler {
	
	@Override
	protected void customize() {
		System.out.println("[制作板烧鸡腿堡] 专属的腌制好的鸡腿肉,撒上独特的酱汁");
	}
	
}

/**
 * 麦辣鸡腿堡
 *
 * @author Amg
 * @date 2021/9/24
 */
public class mailajituibao extends AbstractMakeHamburgerHandler {
	
	@Override
	protected void customize() {
		System.out.println("[制作麦辣鸡腿堡] 放上炸制好的鸡腿肉,撒上独特的酱汁");
	}
}

//新奥尔良就不实现了,有兴趣的朋友可以自行敲一敲(主要是肯德基你跑错地方了,这里是麦当劳专场)
public class Client {
	
	public static void main(String[] args) {
		AbstractMakeHamburgerHandler banshao = new banshao();
		banshao.execute();
		
		System.out.println("---------------------------------");
		
		mailajituibao mailajituibao = new mailajituibao();
		mailajituibao.execute();
	}
	
}

输出结果

现在我们模型图画了,代码也敲了,那么模板设计模式有什么优缺点吗?

优点

  • 抽取共用部分到超类中,让程序更加的简洁
  • 封装不变的对象,扩展变化的部分(符合开闭原则)

缺点

  • 采用继承结构,理论上来说,每扩展一个汉堡(推出新品)就需要创建一个新的类继承超类,如果有100个汉堡,就得有100个类,这就会显示很膨胀(好处是Java 8引入了消费者接口,我们可以使用这个接口,而不必创建一大堆的新类

简单讲解一下Consumer(消费者)接口

先看看这个接口里面的方法

/**
* Performs this operation on the given argument.
*
* @param t the input argument
*/
void accept(T t);

/**
* Returns a composed {@code Consumer} that performs, in sequence, this
* operation followed by the {@code after} operation. If performing either
* operation throws an exception, it is relayed to the caller of the
* composed operation.  If performing this operation throws an exception,
* the {@code after} operation will not be performed.
*
* @param after the operation to perform after this operation
* @return a composed {@code Consumer} that performs in sequence this
* operation followed by the {@code after} operation
* @throws NullPointerException if {@code after} is null
*/
default Consumer<T> andThen(Consumer<? super T> after) {
    Objects.requireNonNull(after);
    return (T t) -> { accept(t); after.accept(t); };
}

我们得知道消费者这个角色的第一任务就是消费嘛,我不管什么乱七八糟的,我只知道调用accept方法,我就要去消费

至于andThen方法,返回一个组合的Consumer ,它依次执行此操作和after操作。 如果执行任一操作引发异常,则将其转发给组合操作的调用者。 如果执行此操作抛出异常,则不会执行after操作(这段是直接翻译文档过来的,解释得很透彻了)

Java8 Stream中大量使用到消费者和生产者,有兴趣的朋友也可以去了解一下(话说jdk都出到17了...)

所有,如果以Consumer方式使用模板模式,是怎么样的?直接上代码

/**
 * jdk1.8之后做汉堡
 * @author Amg
 * @date 2021/9/24
 */
public class MakeHamburgerHandler {
	
	/**
	 * 这个方法进行复用
	 * @param consumer	jdk1.8之后出现的【消费者】接口,接收参数,无返回值,调用accept方法就把信息给输出出来
	 */
	private void execute(Consumer<String> consumer) {
		baking();
		consumer.accept(null);
		vegetable();
		compose();
		packing();
		finish();
	}
	
	
	private void baking() {
		System.out.println("取出两块面包,烤热");
	}
	
	private void compose() {
		System.out.println("组合汉堡");
	}
	
	private void vegetable() {
		System.out.println("撒上蔬菜");
	}
	
	private void packing() {
		System.out.println("用纸袋包裹着");
	}
	
	private void finish() {
		System.out.println("制作完成,客官请慢用!");
	}
	
	
	/**
	 * 板烧鸡腿堡
	 */
	public void banshao() {
		execute(a -> System.out.println("[制作板烧鸡腿堡] 专属的腌制好的鸡腿肉,撒上独特的酱汁"));
	}
	/**
	* 麦辣鸡腿堡
	*/
	public void mailajituibao() {
		execute(a -> System.out.println("[制作麦辣鸡腿堡] 放上炸制好的鸡腿肉,撒上独特的酱汁"));
	}
}
/**
 * 使用JDK1.8之后提供的supplier、consumer接口,可以不再不要抽象类,以及减少类对象的创建
 * 而且把相同的业务给放在同一个类里面,方便后续的维护;
 * <p></p>
 * <p>但是思考一下,这样子就会违反OCP原则,上架和下架汉堡、修改汉堡佐料等等操作都需要去修改{@link MakeHamburgerHandler}</p>
 *
 * @author Amg
 * @date 2021/9/24
 */
public class Client {
	
	public static void main(String[] args) {
		
		MakeHamburgerHandler makeHamburgerHandler = new MakeHamburgerHandler();
		
		makeHamburgerHandler.banshao();
		
		System.out.println("------------------jdk1.8之后模板模式的使用------------------------");
		
		makeHamburgerHandler.mailajituibao();
		
		System.out.println("------------------jdk1.8之后模板模式的使用------------------------");
		
	}
}

代码上的注释也很清晰了,这里再总结一下,使用Consumer接口,我们可以不再需要抽象类、以及一大堆的派生子类,想要什么汉堡就包装一下execute方法即可,可以说是非常的好使

但是随之而来的问题也是,我们每次修改都会动到这个类,所以是违反了OCP原则的

最最最最后一个问题,就是上面遗留的hook方法有什么用?

上面遗留的hook方法其实是一个程序钩子他一般是空方法体或者默认实现,作用是让子类有能力对算法的不同点进行挂钩,意思就是执行或者不执行额外的逻辑

通俗点就是麦当劳在点完餐之后通常还会有一些推荐点餐,我爱吃薯条(就是你了),你可以选择要还是不要,如果我想在板烧套餐里面添加,麦辣鸡腿堡套餐里面不要,要怎么做呢?

修改我们的代码

public final void execute() {
    baking();
    customize();
    vegetable();
    compose();
    finish();
    //将hook方法更名为isReceiveChips
    if (isReceiveChips()) {
        System.out.println("薯条添加完毕!");
    }
}

/**
* 是否接收推荐过来的薯条,默认不要
* @return false
*/
protected boolean isReceiveChips() {
    return false;
}

//获取用户输入
protected String getUserInput() {

    String answer;

    System.out.println("是否还需要来一份薯条呢?[y/n]");

    BufferedReader in = new BufferedReader(new InputStreamReader(System.in));

    try {
        answer = in.readLine();
    } catch (IOException e) {
        answer = "n";
    }

    return answer;
}

//板烧子类覆盖这个方法
@Override
protected boolean isReceiveChips() {
    String answer = getUserInput();

    if (answer.equalsIgnoreCase("y"))
        return true;
    return false;
}

输出的结果

可以看到,我们在板烧套餐里面重写了isReceiveChips方法,并且返回true;而在麦辣鸡腿堡套餐里面就没有这个操作,所以麦辣鸡腿堡套餐采用默认行为

再回顾一下,钩子的存在,可以让子类有能力对算法的不同点进行挂钩

写完,收工,继续吃汉堡去!

相关文章

微信公众号

最新文章

更多