您的位置:首页 > 其它

继承、组合和接口用法——策略模式复习总结

2016-02-02 00:09 267 查看

前言——为什么继承不被优先推荐使用

先看这样一个案例——有一群鸭子,有的鸭子会游泳,有的鸭子会呱呱叫,每一种鸭子的外貌都不同。

第一版——使用继承

RD 设计了一个鸭子类,作为所有鸭子的超类。鸭子会呱呱叫(Quack)、也会游泳(Swim),那么由超类负责处理这部分的实现, 还有一个负责展示鸭子的外貌的 display 方法,它是抽象的,由各个具体的鸭子描述自己的外貌。代码如下:

public abstract class Duck {
public void quack() {
System.out.println("呱呱叫");
}

public void swim() {
System.out.println("游泳");
}

public abstract void display();
}
 
/////////////////////////////////
public class MallardDuck extends Duck {
@Override
public void display() {
System.out.println("外观是绿头");
}
}
 
/////////////////////////////////
public class RedheadDuck extends Duck {
@Override
public void display() {
System.out.println("外观是红头");
}
}

增加需求

产品:经过市场调研,发现市面上很多类似的系统,都实现了鸭子飞翔的功能,为了保持竞争力,希望我们的鸭子也能飞

RD :只要在 Duck 抽象类中加上 fly() 方法,然后让所有鸭子都继承fly(),就ok了

问题:并不是所有的鸭子都必须会飞,这也符合产品(动物)的多样性原理。故实际的实现中,所有的鸭子都继承了这个超类,从而都有了(有的是被动的)飞翔的能力,这显然是不好的设计,甚至后患无穷。

当涉及“维护”时,为了“复用”目的而使用继承,并不完美

实际开发中经常见有人这样做——子类如果不需要父类的某个方法,就强行覆盖。

问题:利用继承来提供 Duck 的行为,会导致下列的一些问题:

1、代码在多个子类中无意义的重复——比如玩具鸭子不需要飞翔,但是还得必须显示的覆盖这部分的代码。

2、运行时的行为不容易改变——代码都继承到了子类,等于代码是被写死了。

3、无法灵活的扩展——比如,新加入了木头的玩具鸭子,木头的鸭子不会呱呱叫,也不会飞翔,这就仍然需要很笨重的给木头鸭子覆盖呱呱叫+飞翔的方法,让其什么都不做。

4、很难知道所有鸭子全部真正的行为,无法容易的得到,某个鸭子类,到底需要实现的行为是什么。

5、牵一发动全身,造成其他鸭子不想要的改变——比如又要给鸭子增加跳舞的行为,那么所有的不需要跳舞的鸭子,也都要去修改…… 

第二版——使用接口+组合

第一版方案不是很完美,所以需要一个更清晰的策略,只让部分鸭子类型可飞或可叫。可以把 fly() 从 Duck 超类中抽象出来,用一个 FlyAble 接口来实现,让只有会飞的鸭子实现此接口,同样的方式,也可以设计一个 QuackAble 接口。

public interface Quackable {
void quack();
}
 
////////////////////
public interface Flyable {
void fly();
}
 
/////////////////////
public abstract class Duck {
public void swim() {
System.out.println("游泳");
}

public abstract void display();
}
 
////////////////////
public class MallardDuck extends Duck implements Flyable, Quackable {
@Override
public void display() {
System.out.println("外观是绿头");
}

@Override
public void fly() {
System.out.println("飞翔");
}

@Override
public void quack() {
System.out.println("呱呱叫");
}
}

为了使用接口而使用接口——效果会适得其反

在本案例中,虽然使用 Flyable 与 Quackable 接口可以解决第一版继承带来的问题,但是却产生了代码无法复用的新问题。因为 Java 的接口在 1.8 之前,不能具有实现代码,所以除非你能肯定所有使用这个系统的人都是使用的 JDK 8及其以后的版本,否则继承接口无法 100% 确保达到代码的复用目的。

这就意味着:无论何时你需要修改某个行为,你必须得往下追踪,并在每一个定义此行为的类中修改它,一不小心,可能会造成新的错误。幸运的是,有一些设计原则,恰好适用于此状况。

第一设计原则:找出应用中变化之处,把它们独立,不要和那些不需要变化的代码混在一起

把会变化的代码提取,并封装为类,以便未来可以轻易地改动或扩充此部分,而不会影响其他不需要变化的部分。可以说这个原则是每个设计模式的精髓。

分析:最开始的 Duck 抽象类里的 fly 方法和咕咕叫(quack)方法都会随着鸭子的种类不同,而被改变。那么可以把这两个方法提取,建立一组新的类来实现。建立两个类,一个是“fly”相关的,一个是“quack”相关的,每一个类能实现各自的多样化的动作。比如:

1、叫的动作,可以是 “呱呱叫”,“吱吱叫”,或者 “安静(不叫)”等

2、飞的动作,可以是“快速的飞”,“不能飞”等,能满足,类似橡皮鸭不会飞这种新需求

把动作相关的代码,提取为类,看着好像不是特别好,毕竟大家都知道:接口才代表的是行为(或者说动作),类代表的是某种事物的具体类型或者抽象的某种类型。参考:何时使用接口(抽象类)?没错,确实需要接口来表示动作,因此引出第二个设计原则。

第二设计原则:面向接口编程而不是面向具体实现

当然不能直接用类来实现真正的动作,所以利用接口代表每个行为,设计两个代表鸭子动作的接口:FlyBehavior 与 QuackBehavior,而第一原则里说的抽取出的两种新类(一个是“fly”相关的,一个是“quack”相关的),作为具体的行为的实现类,即让 “fly” 相关的,和 “quack” 相关的代表鸭子的所有具体动作的类都实现其中的一个接口。

public interface FlyBehavior {
void fly();
}
 
////////////////////
public class FlyWithWings implements FlyBehavior {
@Override
public void fly() {
System.out.println("飞翔");
}
}
 
//////////////////
public class FlyWithoutWings implements FlyBehavior {
@Override
public void fly() {
System.out.println("不能飞");
}
}

1、鸭子类 Duck 不再负责实现 Flying 与 Quacking 接口,反而是额外制造一组其他类专门实现 FlyBehavior 与 QuackBehavior,这就是所谓的分离变和不变的部分,把变的部分抽取(抽象)——由专门的行为类而不是 Duck 类来实现行为接口。从次以后,Duck类就不需要维护经常需要变动的行为了。

public abstract class Duck {
public void swim() {
System.out.println("所有的鸭子都会游泳,不会改变");
}
 
abstract void display(); // 所有鸭子都有外貌
}

2、使用接口代表抽象的行为,具体的鸭子类只需要按照自身的需求,去实现对应的行为接口( FlyBehavior 、 QuackBehavior等,以后可以扩展),等 Duck 需要使用某个行为的时候,具体的实现不会绑死在具体的鸭子类上。

public abstract class Duck {
FlyBehavior flyBehavior; // 面向接口编程,这也是组合的体现
QuackBehavior quackBehavior; // 面向接口编程// 面向接口编程
public void setFlyBehavior (FlyBehavior fb) {
flyBehavior = fb;
}

// 面向接口编程
public void setQuackBehavior(QuackBehavior qb) {
quackBehavior = qb;
}

abstract void display(); // 所有鸭子都有外貌

public void performFly() {
flyBehavior.fly();
}

public void performQuack() {
quackBehavior.quack();
}

public void swim() {
System.out.println("所有的鸭子都会游泳,不会改变");
}
}

以后,在新设计中,鸭子的具体的类将使用接口(FlyBehavior与QuackBehavior)表示行为,所以实际的“实现”不会被绑死在鸭子的子类中。

而且,这里之所以使用抽象类代表Duck,还是考虑到,既然Duck没有需要变化的部分了,那么完全可以让其代表一个类型——鸭子,故没有必要使用接口。

问题:为什么非要把行为设计成接口,而不用抽象类

这个问题说明没有真正理解接口,要理解 “面向接口编程” 的真正意思——针对超类型编程。这里所谓的“接口”有多个含义,接口是一个广义的 “概念”,只不过在 Java 里特指的 interface。

面向接口编程,关键就在实现多态,和能利用多态,程序可以针对超类型编程,执行时会根据实际状况执行到真正的行为,不会被绑死在超类型的行为上,只不过这里优先使用的接口。

public interface FlyBehavior {
void fly();
}
 
/////////////////////////
public interface QuackBehavior {
void quack();
}
 
///////////////////////
public class FlyWithWings implements FlyBehavior {
public void fly() {
System.out.println("I'm flying!!");
}
}
 
public class FlyRocketPowered implements FlyBehavior {
public void fly() {
System.out.println("I'm flying with a rocket");
}
}
 
public class FlyNoWay implements FlyBehavior {
public void fly() {
System.out.println("I can't fly");
}
}
 
//////////////////////
// 呱呱的叫
public class Quack implements QuackBehavior {
public void quack() {
System.out.println("Quack");
}
}
 
// 嘶哑的叫
public class MuteQuack implements QuackBehavior {
public void quack() {
System.out.println("MuteQuack");
}
}

///////////////////
public abstract class Duck {
FlyBehavior flyBehavior;
QuackBehavior quackBehavior;public void setFlyBehavior (FlyBehavior fb) {
flyBehavior = fb;
}

public void setQuackBehavior(QuackBehavior qb) {
quackBehavior = qb;
}

abstract void display();

public void performFly() {
flyBehavior.fly();
}

public void performQuack() {
quackBehavior.quack();
}

public void swim() {
System.out.println("All ducks float, even decoys!");
}
}
 
// 能呱呱的叫,能飞
public class MallardDuck extends Duck {
public MallardDuck() {
setQuackBehavior(new Quack());
setFlyBehavior(new FlyWithWings());
}

public void display() {
System.out.println("I'm a real Mallard duck");
}
}
 
// 不能飞,能嘶哑的叫
public class DecoyDuck extends Duck {
public DecoyDuck() {
setFlyBehavior(new FlyNoWay());
setQuackBehavior(new MuteQuack());
}
public void display() {
System.out.println("I'm a duck Decoy");
}
}
 
////////////////// 客户端
public class MiniDuckSimulator1 {
public static void main(String[] args) {
Duck mallard = new MallardDuck();
mallard.performQuack(); // 咕咕叫
mallard.performFly(); // 能飞

Duck decoyDuck = new DecoyDuck();
decoyDuck.performFly(); // 不能飞
// 客户端可以动态的改变鸭子的行为,让它能飞了
decoyDuck.setFlyBehavior(new FlyRocketPowered());
decoyDuck.performFly(); // 嘶哑的叫

}
}

如上,也是策略模式的体现——既可以实现代码复用,又能实现责任分离,即使新增了行为,也不会影响现有的鸭子类。

总结:什么时候开始优化

虽然,过早优化是万恶之源,但是类似接口,抽象类,继承等这样基本的思想还是要从最开始就伴随整个系统的,比如策略模式这种简单的设计模式,完全可以在开始设计的时候,就考虑进去,直接实现,而不是后期重构。

类能否代表行为?

前面的例子里,优化的方案,使用了类代表行为,虽然具体的行为实现,还是应用的接口,那么这里合理么?

合理,众所周知,在OOP中,类代表某个事物,某个具有状态的事物的类型,而行为有时候也是会有状态的,比如飞翔,可以有飞翔速度等属性,因此,在本例,飞行这个行为也是一种类型,只不过是凑巧的。为了方便,使用的类代表具体行为,而行为的动作实现,仍然是接口代表

算法族的概念

到这里,不再把鸭子的各个行为说成“一组行为”、或者“相关的行为”了,而是开始把行为看成是“一族算法”。算法代表鸭子能做的事情(不同的叫法和飞法)

总结:遵循组合(聚合)优于继承的设计原则

记住,考虑到在同样可行的情况下,优先使用组合而不是继承,也有书中,管他叫:has-a 关系,好于 is-a 关系,前者是组合,后者是继承。

结合前面的例子,知道继承关系的耦合度很高,一处改可能会导致处处需要修改。如果一个业务功能,继承可以实现,组合也能实现,优先使用组合。因为:

1、组合的关系具有很大的弹性

2、组合更好的分离了责任

3、组合的副作用很小

……

很多的设计模式,都巧妙的体现了该原则。

《Thinking in java》:“继承要慎用,其使用场合仅限于你确信使用该技术有效的情况。一个判断方法是,问一问自己是否需要从新类向基类进行向上转型。如果是必须的,则继承是必要的。反之则应该好好考虑是否需要继承……当用继承的时候,肯定是需要利用多态的特性。如果用不到多态的特性,继承的关系是无用的”

《effective Java》:“只有当子类真正是超类的子类型时,才适合用继承。换句话说,对于两个类A和B,只有当两者之间确实存在is-a关系的时候,类B才应该继续类A。”

JDK 8 中的接口新特性,可以改善一些旧问题

https://www.zhihu.com/question/41166418

Java 8的接口,即便有了default method,还暂时无法完全替代抽象类。它不能拥有状态,只能提供公有虚方法的默认实现。Java 9的接口已经可以有非公有的静态方法了。未来的Java版本的接口可能会有更强的功能,或许能更大程度地替代原本需要使用抽象类的场景。

作者:RednaxelaFX
链接:https://www.zhihu.com/question/41166418/answer/139494009
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

策略模式到底是怎么运行的

StrategyPattern:策略模式也算比较简单的,同工厂模式一样都属于面向接口编程……策略模式是对象的行为模式之一,而工厂模式是对象的创建模式

策略模式对一系列的算法加以封装,为所有算法定义一个抽象的接口,并通过继承(实现)该接口,对所有算法加以封装和实现,具体的算法选择由客户端决定(策略)。

策略模式使得算法可以在不影响到客户端的情况下发生变化。Strategy 模式主要用来平滑地处理算法的切换 。

简单说:策略模式可以让我们在程序中随意的、快速的替换接口的实现“算法”,而且还不用修改接口,也不需要修改客户端。可以说策略模式,是接口的典型应用。

前面说了策略模式封装算法,自然有一个算法接口 IStrategy,扩展的不同的策略(算法封装)StrategyABuilder,StrategyBBuilder……再来一个策略的容器——其实就是一个工厂,下面总结下策略模式的角色:

Strategy接口 : 策略(算法)的接口

ConcreteStrategy :各种策略(算法)的具体实现

Context:策略的外部封装类,或者说策略的容器类。它根据不同策略执行不同的行为。策略由外部环境决定,自然这个工厂就需要聚合策略接口的引用,并配有对应的执行策略的方法。

View Code HttpServlet对该方法进行了实现,实现方式就是将ServletRequest与ServletResponse转换为HttpServletRequest与HttpServletResponse。转换完毕后,会调用HttpServlet类中自己定义的service方法

View Code

发现下面这个 service 是 protected方法

View Code

在该转换之后的 service 方法中,首先获得到请求的方法名,然后根据方法名调用对应的doXXX方法,比如说请求方法为GET,那么就去调用doGet方法;请求方法为POST,那么就去调用doPost方法。比如:

doPut方法 doGet方法

在HttpServlet类中所提供的doGet、doPost……方法都是直接返回错误信息,所以我们需要在自己定义的Servlet类中重写这些方法,经过上面的过程,我们发现 HttpServlet 类就是一个策略抽象类,我们自己定义的servlet类去覆盖HttpServlet里的这些方法,自然不同的人,不同的项目里,需要重写的内容和方法都不尽相同,这就是一个策略模式的思想。

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: