设计模式之八 封装算法

  • 封装算法

定义

模板方法模式在一个方法中定义一个算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤。

 

优缺点

 

应用场景

 Java数组类的设计者提供给我们一个方便的模板方法用来排序。

public static void sort(Object[] a) {
    Object aux[] = (Object[])a.clone();
    mergeSort(aux, a, 0, a.length, 0);
}

//将这个方法想象成一个模板方法
private static void mergeSort(Object src[], Object desc[], int low, int high, int off) {
    for(int i = low; i < high; i++) {
        for(int j = i; j > low && ((Comparable)desc[j-1]).compareTo((Comparable)desc[j]) > 0; j--) {
            swap(desc, j, j-1);
        }
    }
    return;
}

sort()的设计者希望它应用于所有的数组,所以他们把sort()变成静态方法,这样一来,任何数组都可以使用这个方法。他使用起来和在超类中是一样的。因为sort()并不是真正定义在超类中,所以sort()方法需要知道你已经实现了compareTo()方法,否则就无法进行排序;所以我们需要实现Comparable接口,实现这个接口所声明的方法compareTo()。

public class Duck implements Comparable {
	String name;
	int weight;
	public Duck(String name, int weight) {
		this.name = name;
		this.weight = weight;
	}
	
	@Override
	public String toString() {
		return "Duck [name=" + name + ", weight=" + weight + "]";
	}

	@Override
	public int compareTo(Object o) {
		Duck otherDuck = null;
		if(o instanceof Duck) {
			otherDuck = (Duck) o;
			if(this.weight < otherDuck.weight) {
				return -1;
			}
			if(this.weight == otherDuck.weight) {
				return 0;
			}
			return 1;
		}
		return 0;//这一步应该抛相应异常
	}
}

钩子的使用场景:

public class MyApplet extends Applet {
    String message;

    //init钩子用来进行applet的初始化动作,它会在applet一开始的时候被调用一次
    public void init() {
        message = "Hello World";
        repaint();
    }
    
    //这个start钩子可以在applet正要被显示在网页上时,让applet做一些动作
    public void start() {
        message = "Now I'm Starting up!";
        repaint();
    }
    
    //如果用户跳转到别的网页,这个stop钩子会被调用,然后applet就可以在这里做一些事情停止他的行动
    public void Stop() {
        message = "Oh, now I'm Stopped";
        repaint();
    }

    //applet正在被销毁(如:关闭浏览器),这个钩子会被调用
    public void destory() {
        //applet正在被销毁
    }

    public void paint(Graphics g) {
        g.drawString(message, 5, 15);
    }
}

例子

 咖啡和茶的冲泡案例

步骤茶的冲泡法咖啡冲泡法
0把水煮沸把水煮沸
1用沸水浸泡茶叶用沸水冲泡咖啡
2把茶倒进杯子把咖啡倒进杯子
3加柠檬加糖和牛奶

茶的初始代码

public class Tea {

	public void prepareRecipe() {
		boilWater();
		steepTeaBag();
		pourInCup();
		addLemon();
	}
	
	public void boilWater() {
		System.out.println("Boiling water");
	}
	public void steepTeaBag() {
		System.out.println("Steeping the tea");
	}
	public void pourInCup() {
		System.out.println("Pouring into cup");
	}
	public void addLemon() {
		System.out.println("adding Lemon");
	}
}

冲咖啡的初始代码

public class Coffee {
	
	public void prepareRecipe() {
		boilWater();
		brewCoffeeGrinds();
		pourInCup();
		addSugarAndMilk();
	}
	
	public void boilWater() {
		System.out.println("Boiling water");
	}
	public void brewCoffeeGrinds() {
		System.out.println("Dripping Coffee through filter");
	}
	public void pourInCup() {
		System.out.println("Pouring into cup");
	}
	public void addSugarAndMilk() {
		System.out.println("Adding Sugar and Milk");
	}
}

初始代码出现了重复代码,因此我们需要清理一下设计方案了。似乎把相同的部分提取出来放在一个基类里是个不错的主意。

提取出相同点:

步骤操作
0把水煮沸
1用热水冲泡
2把饮料倒进杯子
3在饮料内加入适当的调料

我们可以抽象出一个咖啡因饮料基类

public abstract class CaffeineBeverage {
	final void prepareRecipe() {
		boilWater();
		brew();
		pourInCup();
		addCondiments();
	}
	
	abstract void brew();
	
	abstract void addCondiments();
	
	public void boilWater() {
		System.out.println("Boiling water");
	}
	
	public void pourInCup() {
		System.out.println("Pouring into cup");
	}
}

新的茶类与咖啡类

public class Tea extends CaffeineBeverage{

	@Override
	void brew() {
		System.out.println("Steeping the tea");
		
	}

	@Override
	void addCondiments() {
		System.out.println("adding Lemon");
		
	}
}
public class Coffee extends CaffeineBeverage {
	
	@Override
	void brew() {
		System.out.println("Dripping Coffee through filter");
	}

	@Override
	void addCondiments() {
		System.out.println("Adding Sugar and Milk");
	}
}

在咖啡因饮料基类中,prepareRecipe()是我们的模板方法。模板方法定义了一个算法的步骤,并允许子类为一个或多个步骤提供实现。

模板方法由超类主导一切,并受超类保护;复用性最大化;维护修改容易;超类专注于算法本身,而由子类提供完整的实现。

让我们看看类图:

 类名方法描述
抽象类AbstractClass

templateMethod()

primitiveOperation1()

primitiveOperation2()

模板方法templateMethod()

执行操作方法

primitiveOperation1()

primitiveOperation2()

具体类ConcreteClass

primitiveOperation1()

primitiveOperation2()

这个具体类实现抽象的操作

抽象类

abstract class AbstractClass {
	final void templateMethod() {
		primitiveOperation1();
		primitiveOperation2();
		concreteOperation();
		hook();
	}
	
	abstract void primitiveOperation1();
	
	abstract void primitiveOperation2();
	
	final void concreteOperation() {//声明为final,这样子类就无法覆盖它
		//具体实现
	}
	void hook() {	}//可以有“默认不做事”的钩子(hook)方法,子类视情况决定要不要覆盖它们
}

钩子是一种被声明在抽象类中的方法,但是只有空的或默认的实现。钩子的存在,可以让子类有能力对算法的不同点进行挂钩。要不要挂钩,由子类自行决定。

先看一个具体例子:

public abstract class CaffeineBeverage {
	final void prepareRecipe() {
		boilWater();
		brew();
		pourInCup();
		if(customerWantsCondiments()) {
			addCondiments();
		}
	}
	
	abstract void brew();
	
	abstract void addCondiments();
	
	final void boilWater() {
		System.out.println("Boiling water");
	}
	
	final void pourInCup() {
		System.out.println("Pouring into cup");
	}
	
	boolean customerWantsCondiments() {//这是一个钩子,通常是空或默认实现
		return true;
	}
}

当你的子类“必须”提供算法中的某个方法或步骤的实现时,就是用抽象算法。如果算法的这个部分是可选的,就用钩子。如果是钩子的话,子类可以选择实现,但不强制。

 

对比

序号模式叙述
0模板方法子类决定如何实现算法中的步骤
1策略封装可互换的行为,然后使用委托来决定要采用哪一个行为
2工厂方法由子类决定实例化哪个具体类

设计原则

封装变化

多用组合,少用继承

针对接口编程,不针对实现编程

为交互对象之间的松耦合设计而努力

类应该对扩展开放,对修改关闭

依赖抽象,不依赖具体类

最少知识原则:只和朋友交谈 

好莱坞原则:别找我,我会找你(由超类主控一切,当它们需要的时候,自然会去调用子类)

总结

 "模板方法"定义了算法的步骤,把这些步骤的实现延迟到子类。

模板方法模式为我们提供了一种代码复用的重要技巧。

模板方法的抽象类可以定义具体方法、抽象方法和钩子。

为了防止子类改变模板方法中的算法,可以将模板方法声明为final。

好莱坞原则告诉我们,将决策权放到高层模块中,以便决定如何以及何时调用低层模块。

策略模式和模板方法模式都封装算法,一个用组合,一个用继承。

工厂方法是模板方法的一种特殊版本。

小知识

 好莱坞原则:

别调用我们,我们会调用你(由超类主控一切);

当高层组件依赖低层组件,而低层组件又依赖高层组件,而高层组件又依赖边侧组件,而边侧组件又依赖低层组件时,依赖腐败就发生了。

在好莱坞原则下,我们允许低层组件将自己挂钩到系统上,但是高层组件会决定什么时候和怎样使用这些低层组件。

依赖倒置原则与好莱坞原则的关系:

依赖倒置原则教我们尽量避免使用具体类,而多使用抽象(接口)。而好莱坞原则是用在创建框架或组件上的一种技巧,好让低层组件能够被挂钩计算中,而且又不会被高层组件依赖低层组件。两者的目标都是解耦

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值