之前在infoq上看到一篇文章:
swift版本原文参考:
http://www.infoq.com/cn/articles/design-patterns-in-swift
于是想着把这篇文章修改为Java8的版本,本文是对原文的Java8版本的修改,所以大部分文章和示例都是采用原文的叙述;再次表达对原文作者的感谢;
设计模式##
设计模式(Design Pattern)是对软件设计中普遍存在的各种问题,所提出的解决方案。这个术语是由埃里希·伽玛等人(Erich Gamma,Richard Helm,Ralph Johnson和John Vlissides这四人提出的。也被称为:Gang of Four,GOF,四人帮)在1990年代从建筑设计领域引入到计算机科学的。 设计模式并不能直接用于完成代码的编写,而是描述在各种不同情况下,要怎么解决问题的一种方案。
以上描述摘自维基百科。
随着我们所使用的编程语言的演化,我们遇到的问题也确实一直在改变。GOF提出的23种设计模式也许部分过时了。但我们遇到的问题并不会消失,设计模式的概念将一直存在:提炼普遍存在的问题,提出解决方案。这其实是一个抽象过程。现在已有的编程语言都存在表达的局限,即对某类问题抽象层次过低。所以,我们在使用任何编程语言时,都还是会遇到一些普遍存在的却没有被语言本身很好解决的问题。这时,我们就会使用到“设计模式”,即人们总结出来的解决方案:遇到问题A,用方案A;遇到问题B,用方案B。只不过问题会一直变化。现在我们不可以再使用那23个模式来解决问题了,但是我们仍然需要总结出其它模式来解决新的问题。这种情况一直会持续到我们拥有“完美语言”的那一天。但现在看起来,这一天还没有到来的迹象。
Java8中的设计模式##
Java8中提出了函数式编程等新的概念,使得Java8在面对传统的GOF23模式的时候,解决问题的方式已经得到了部分的改善,之前的一些设计模式在Java8面前就已经不再是问题了。但是Java8提出了函数式编程的同时,因为涉及到向后兼容的问题,同样在Java8中可能面对新的代码设计问题,比如接口中的默认方法带来的诸多设计细节,同样也是会引人深思的;
下面主要讨论的是,在Java8中,传统的哪些开发模式被消除了或者以一种更简单的方式简化了。
一,命令模式##
命令模式使用对象封装一系列操作(命令),使得操作可以重复使用,也易于在对象间传递。首先来一个传统Java方式实现的命令模式代码。
public class Light {
public void on() {
System.out.println("light turn on");
}
public void off() {
System.out.println("light turn off");
}
}
interface Command {
void execute();
}
class FilpUpCommand implements Command {
private Light light;
public FilpUpCommand(Light light) {
this.light = light;
}
public void execute() {
light.on();
}
}
class FilpDownCommand implements Command {
private Light light;
public FilpDownCommand(Light light) {
this.light = light;
}
public void execute() {
light.off();
}
}
以上代码中,灯(Light)是命令(Command)的操作对象(Receiver)。我们定义了命令的协议,同时我们实现两个具体的命令操作:FlipUpCommand和FlipDownCommand。它们分别使灯亮,和使灯灭。
class LightSwitch{
private List<Command> queue=new ArrayList<>();
public void addCommand(Command cmd){
queue.add(cmd);
}
public void execute(){
for(Command cmd:queue){
cmd.execute();
}
}
}
class Client{
public static void pressSwitch(){
Light lamp=new Light();
Command flipUpCommand=new FilpUpCommand(lamp);
Command flipDowomnCmand=new FilpDownCommand(lamp);
LightSwitch lightSwitch = new LightSwitch();
lightSwitch.addCommand(flipUpCommand);
lightSwitch.addCommand(flipDowomnCmand);
lightSwitch.addCommand(flipUpCommand);
lightSwitch.addCommand(flipDowomnCmand);
lightSwitch.execute();
}
}
上面的代码首先创建了一个命令执行者LightSwitch,并创建一个客户对象来使用命令;
在函数式编程中,由于存在高阶函数。我们可以直接将一个函数作为参数传给另外一个函数。所以,使用类包裹函数在对象间传递这件事情就显得多余了。以下代码显示如何使用高阶函数达到命令模式相同的效果:
class LightSwitchFP {
private List<Consumer<Light>> queue = new ArrayList<>();
public void addCommand(Consumer<Light> cmd) {
queue.add(cmd);
}
public void execute(Light light) {
for (Consumer<Light> cunsumer : queue) {
cunsumer.accept(light);
}
}
}
class Client {
public static void pressSwitch() {
Light lamp = new Light();
Consumer<Light> flipUp = light -> {light.on();};
Consumer<Light> flipDown = light -> {light.off();};
LightSwitchFP lightSwitch = new LightSwitchFP();
lightSwitch.addCommand(flipUp);
lightSwitch.addCommand(flipDown);
lightSwitch.addCommand(flipUp);
lightSwitch.addCommand(flipDown);
lightSwitch.execute(lamp);
}
}
在Java8中,首先我们直接使用Java8提供的Consumer函数接口作为我们的命令接口,因为有了lambda表达式,我们根本无需在单独为具体命令对象创建类型,而通过传入labmda表达式来完成具体命令对象的创建;
二,策略模式##
策略模式定义了一系列算法,将每个算法封装起来,并且使它们之间可以互相替换。此模式让算法的变化独立于使用算法的客户。
下面简单演示一个传统的策略模式实现方案:
interface Strategy {
public Integer compute(Integer op1, Integer op2);
}
class Add implements Strategy {
public Integer compute(Integer op1, Integer op2) {
return op1 + op2;
}
}
class Multiply implements Strategy {
public Integer compute(Integer op1, Integer op2) {
return op1 * op2;
}
}
class Context {
private Strategy strategy;
public Context(Strategy strategy) {
this.strategy = strategy;
}
public void use(Integer first, Integer second) {
System.out.println(this.strategy.compute(first, second));
}
}
类似于命令模式,策略模式中的策略对象主要用于封装操作(函数),不同的是策略模式中的策略对象封装的是不同的算法。这些算法实现了相同的接口,在这个例子中,接口是用Strategy协议表示的。我们使用两个实现了Strategy协议的具体类:Add和Multiply分别封装两个简单的算法。Context对象,用于对算法进行配置选择,它有一个Strategy类型的实例变量:strategy。通过配置Context的strategy具体类型,可以使用不同的算法。
然后我们再看看怎么简化策略模式:
public static final BinaryOperator<Integer> add = (op1, op2) -> op1 + op2;
public static final BinaryOperator<Integer> multiply = (op1, op2) -> op1 * op2;
class ContextFP {
private BinaryOperator<Integer> strategy;
public ContextFP(BinaryOperator<Integer> strategy) {
this.strategy = strategy;
}
public void use(Integer first, Integer second) {
System.out.println(strategy.apply(first, second));
}
}
public static void main(String[] args) {
StraFP fp = new StraFP();
ContextFP ctx = fp.new ContextFP(StraFP.add);
ctx.use(1, 2);
}
在Java8中,我们很自然的使用内建的function interface作为封装算法的载体,这样更为直接自然。例子中,ContextFP的构造器的传参就是函数类型。给予构造器代表不同算法的函数,就配置了不同的算法。
函数也可以作为类的实例变量。这样在类中,直接维护代表算法的函数也成为可能。从类型声明可以看出,ContextFP中的实例变量strategy就是一个函数。
一等函数的概念使得函数获得了更高的地位,使得函数的灵活性大大增加。在很多场景下直接使用函数会是更直接自然的选择。面向对象编程范式,赋予了对象更高的地位。但是,如果给予函数“正常”一些的地位,可以简化不少问题。设计模式中的不少模式存在都是由于函数的使用限制,需要使用在使用类包裹函数。类似的例子还有模版方法模式(Template method)。
上面代码示例不能对策略做很好的封装,下面提供了一个枚举的版本:
public enum StrategyEnum {
ADD(() -> (x, y) -> x + y),
MULTIPLY(() -> (x, y) -> x * y);
private Supplier<BinaryOperator<Integer>> operation;
private StrategyEnum(Supplier<BinaryOperator<Integer>> operation) {
this.operation = operation;
}
public BinaryOperator<Integer> get() {
return operation.get();
}
}
class ContextFP {
private StrategyEnum strategy;
public ContextFP(StrategyEnum strategy) {
this.strategy = strategy;
}
public void use(Integer first, Integer second) {
System.out.println(strategy.get().apply(first, second));
}
}