先看这样一个案例——有一群鸭子,有的鸭子会游泳,有的鸭子会呱呱叫,每一种鸭子的外貌都不一样。html
RD 设计了一个鸭子类,做为全部鸭子的超类。鸭子会呱呱叫(Quack)、也会游泳(Swim),那么由超类负责处理这部分的实现, 还有一个负责展现鸭子的外貌的 display 方法,它是抽象的,由各个具体的鸭子描述本身的外貌。代码以下:java
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("外观是红头"); } }
产品:通过市场调研,发现市面上不少相似的系统,都实现了鸭子飞翔的功能,为了保持竞争力,但愿咱们的鸭子也能飞web
RD :只要在 Duck 抽象类中加上 fly() 方法,而后让全部鸭子都继承fly(),就ok了算法
问题:并非全部的鸭子都必须会飞,这也符合产品(动物)的多样性原理。故实际的实现中,全部的鸭子都继承了这个超类,从而都有了(有的是被动的)飞翔的能力,这显然是很差的设计,甚至后患无穷。编程
实际开发中常常见有人这样作——子类若是不须要父类的某个方法,就强行覆盖。设计模式
问题:利用继承来提供 Duck 的行为,会致使下列的一些问题:服务器
一、代码在多个子类中无心义的重复——好比玩具鸭子不须要飞翔,可是还得必须显示的覆盖这部分的代码。app
二、运行时的行为不容易改变——代码都继承到了子类,等于代码是被写死了。ide
三、没法灵活的扩展——好比,新加入了木头的玩具鸭子,木头的鸭子不会呱呱叫,也不会飞翔,这就仍然须要很笨重的给木头鸭子覆盖呱呱叫+飞翔的方法,让其什么都不作。工具
四、很难知道全部鸭子所有真正的行为,没法容易的获得,某个鸭子类,到底须要实现的行为是什么。
五、牵一发动全身,形成其余鸭子不想要的改变——好比又要给鸭子增长跳舞的行为,那么全部的不须要跳舞的鸭子,也都要去修改……
初版方案不是很完美,因此须要一个更清晰的策略,只让部分鸭子类型可飞或可叫。能够把 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”相关的,每个类能实现各自的多样化的动做。好比:
一、叫的动做,能够是 “呱呱叫”,“吱吱叫”,或者 “安静(不叫)”等
二、飞的动做,能够是“快速的飞”,“不能飞”等,能知足,相似橡皮鸭不会飞这种新需求
把动做相关的代码,提取为类,看着好像不是特别好,毕竟你们都知道:接口才表明的是行为(或者说动做),类表明的是某种事物的具体类型或者抽象的某种类型。参考:什么时候使用接口(抽象类)?没错,确实须要接口来表示动做,所以引出第二个设计原则。
固然不能直接用类来实现真正的动做,因此利用接口表明每一个行为,设计两个表明鸭子动做的接口: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("不能飞"); } }
一、鸭子类 Duck 再也不负责实现 Flying 与 Quacking 接口,反而是额外制造一组其余类专门实现 FlyBehavior 与 QuackBehavior,这就是所谓的分离变和不变的部分,把变的部分抽取(抽象)——由专门的行为类而不是 Duck 类来实现行为接口。从次之后,Duck类就不须要维护常常须要变更的行为了。
public abstract class Duck { public void swim() { System.out.println("全部的鸭子都会游泳,不会改变"); } abstract void display(); // 全部鸭子都有外貌 }
二、使用接口表明抽象的行为,具体的鸭子类只须要按照自身的需求,去实现对应的行为接口( 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 关系,前者是组合,后者是继承。
结合前面的例子,知道继承关系的耦合度很高,一处改可能会致使到处须要修改。若是一个业务功能,继承能够实现,组合也能实现,优先使用组合。由于:
一、组合的关系具备很大的弹性
二、组合更好的分离了责任
三、组合的反作用很小
……
不少的设计模式,都巧妙的体现了该原则。
《Thinking in java》:“继承要慎用,其使用场合仅限于你确信使用该技术有效的状况。一个判断方法是,问一问本身是否须要重新类向基类进行向上转型。若是是必须的,则继承是必要的。反之则应该好好考虑是否须要继承……当用继承的时候,确定是须要利用多态的特性。若是用不到多态的特性,继承的关系是无用的”
《effective Java》:“只有当子类真正是超类的子类型时,才适合用继承。换句话说,对于两个类A和B,只有当二者之间确实存在is-a关系的时候,类B才应该继续类A。”
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:策略的外部封装类,或者说策略的容器类。它根据不一样策略执行不一样的行为。策略由外部环境决定,天然这个工厂就须要聚合策略接口的引用,并配有对应的执行策略的方法。
好比有这样一个程序,给文件加密的程序,当前有三种加密方法能够选用,分别是MD五、RSA,和AES加密算法,下面运用策略模式,看代码实现;
public interface EncryptStrategy { /** * 加密算法的抽象接口 */ void doEncrypt(); } public class AESEncryptBuilder implements EncryptStrategy { @Override public void doEncrypt() { System.out.println("进行AES加密!"); } } public class MD5EncryptBuilder implements EncryptStrategy { @Override public void doEncrypt() { System.out.println("进行MD5加密!"); } } public class RSAEncryptBuilder implements EncryptStrategy { @Override public void doEncrypt() { System.out.println("进行RSA加密!"); } } public class Factory { /** * 聚合的算法接口 */ private EncryptStrategy encryptStrategy; public Factory(EncryptStrategy encryptStrategy) { this.encryptStrategy = encryptStrategy; } /** * 执行加密操做 * 自己不实现加密算法,而是转而去调用聚合的算法接口持有的方法 */ public void execute() { this.encryptStrategy.doEncrypt(); } } public class TestStrategy { public static void main(String[] args) { Factory factory = new Factory(new MD5EncryptBuilder()); factory.execute(); } }
是否是很是简单,和工厂模式有有些相似。想到这里,联系以前的工厂模式(静态工厂,简单工厂,抽象工厂):
不由发问了:抽象工厂模式就是策略模式吧?只不过这个策略是个简单工厂而已?
两个模式比较重要的一个区别在于:工厂模式是建立型的设计模式,偏重于建立不一样的对象,而策略模式是行为型的设计模式,偏重于经过不一样的方式实现一样的行为。
反过来,建立对象本用的就是一个方法,经过不一样的工厂动态指定实际的建立方法,就是封装了这个方法,工厂模式的目的最终是为了建立对象,而策略模式的目的并不只限于建立对象,能够说工厂模式应用了策略模式,更好地说法是,抽象工厂模式和策略模式都应用了面向接口编程的思想。
经过上面的示例能够看出,策略模式仅仅封装算法,提供新的算法插入到已有系统中,以及能够把不须要的算法从系统中除去,策略模式自己不决定在什么时候使用何种算法,在什么状况下使用什么算法是由客户端决定的。
策略模式的一个很重要的特色:运行时策略的惟一性——也就是策略模式运行期间,客户端在每个时刻只能使用一个具体的策略实现,虽然能够动态地在不一样的策略实现中切换,但同时只能使用一个。
再看一个例子:购物结帐问题,收银系统根据不一样的折扣活动,获得商品的不一样价格
public interface Strategy { /** * 花费的价钱*/ double cost(double num); } public class StrategyA implements Strategy { /** * 打8折*/ @Override public double cost(double num) { return num * 0.8; } } public class StrategyB implements Strategy { /** * 9折*/ @Override public double cost(double num) { return num * 0.9; } } public class Context { private Strategy strategy; public Context(Strategy strategy) { this.strategy = strategy; } public double costs(double num) { return this.strategy.cost(num); } } public class MainDemo { public static void main(String[] args) { double num = 200; Context context = new Context(new StrategyB()); double lastNum = context.costs(num); System.out.println(lastNum);// 180.0 } }
前面的例子,环境类都是在构造器里注入的策略实现,固然也能够不使用构造器注入,而使用setter方法注入具体的算法实现
这就要说到常常见到的一种场景:有时候具体策略类有一些公有的行为、属性,这时候就应把这些公有的行为、属性统一提取放到共同的抽象策略角色里面。固然这时候抽象策略角色必需要用抽象类来实现。这其实也是典型的将代码向继承等级结构的上方集中的标准作法。
有一个例子,如图,人须要出去旅游,出门赶路的方式有,骑自行车去,开汽车去,直接作火车,或者飞机去……咱们应该能想到这是策略模式的一个应用场景。
两种实现代码,第一种实现使用了策略模式:
Person person = new Person(new Bike()); person.travel(); person = new Person(new Car()); person.travel();
第二种实现方式,不用策略模式,以下:
Bike bike = new Bike(); bike.travel(); Car car = new Car(); car.travel();
先理解接口的意义,参考:什么时候使用接口(抽象类)?
论规模,这个场景其实更相似是多态,回答问题以前,首先要明白一个道理——究竟是 “人” 旅游,仍是火车、汽车、自行车、飞机这些交通工具旅游?
若是采用第二种实现方式,那旅游这件事就和人没有关系了,只和交通工具备关。正常来讲,应该是人选择xxx交通工具去xxx地旅游,而不是直接让交通工具去本身选本身……人选交通工具,又有这么多工具能够选择,毫无疑问交通工具是易于变化的,因此把对应的交通工具嵌入到人这个类是合理的。
再来看第二种作法的坏处,咱们来把场景详细说下:如今有两我的(Person),分别是台湾的小明,和上海的老王,他们是老同窗,相约去北京旅游,他们能够经过火车、飞机和汽车这三种不一样的交通工具到达北京见面,到北京以后,两我的同时经过自行车到达天安门和另外一个老同窗小丽见面……如今把程序扩展——旅行结束后,每一个人旅行的总用时都要记录下来,用程序实现之。
若是是第二种作法,仅仅是经过调用 car.travel(),plane.travel(),train.travel() 和 bike.Travel() 方法的话,如何记录不一样人旅行的用时?也许强行的经过不一样的交通工具对象来记录,可是假如两人到达北京之后,小明经过自行车到天安门,而老王经过汽车car临时又去了故宫……此时,记录每一个人旅行总用时更加麻烦了,由于总用时是跟每一个人,而不是某种交通工具备关的,因此这就是为何要使用 person.travel() 的缘由,就是说咱们要明白某种行为(操做)的主体是什么。
面向对象的思想实际上就是将把现实世界中的万事万物当作是对象,并进行抽象,而后以计算机世界的方式对现实世界进行模拟。若是对现实世界的抽象错误,那么在以后就会碰到一系列很别扭,甚至没法解决的问题。
策略模式的优势就在于替换或是增长用于实现相同的功能的算法很是方便(基于多态这个特性)。为了达到这样的优势,“面向接口编程” 是必须的。另外,策略模式通常强调行为,不关注不一样的策略类自己的属性,也就是说对于图片中的例子,咱们是不区分不一样的 plane,bus 等交通工具对象的,咱们须要的只是各个交通工具类的 travel() 方法。
总结——使用策略模式的场景:
一、当系统中相互独立的算法较多
二、这些算法的功能都有共同接口
三、这些算法之后可能发生改变(增删改等)
四、须要隐藏算法的实现
五、若是不用策略模式,会出现大量if-else的时候……
若是知足如上条件,就能够考虑使用策略模式,如今麻烦点,之后就方便点,减小工做量,提升系统弹性
前面明确说到:必须是相互独立的算法…… 且策略模式的特色就是运行时算法的惟一性。故,简单说,若是一个操做的完成不是依靠一个算法,而是复合了多个算法,或者嵌套多个算法才能完成,此时不能使用策略模式。若是偏要使用,能够把算法粒度变粗,整合为一个算法,仍然能使用策略模式。可是最好的解决方案,是使用装饰模式,参考:对复合(协做)算法/策略的封装方法——装饰模式总结
策略模式关注的不是如何实现算法,而是如何调用这些算法,它的优势有:
一、策略模式提供了管理相关的平等的算法族的办法
策略类的等级结构定义了一个算法族。恰当使用继承能够把公共的代码移到父类里面,从而避免重复的代码。策略模式一个很大的特色就是各个策略算法的平等性。对于一系列具体的策略算法,你们的地位是彻底同样的,正由于这个平等性,才能实现算法之间相互替换。全部的策略算法在实现上也是相互独立的,因此能够这样描述这一系列策略算法:策略算法是相同行为的不一样实现。
二、策略模式提供了能够替换继承关系的办法(聚合抽象的引用)
继承能够处理多种算法或行为。若是不用策略模式,那么使用算法或行为的环境类就可能会有一些子类,每个子类提供一个不一样的算法或行为。可是,这样一来算法或行为的使用者就和算法或行为自己混在一块儿。决定使用哪种算法或采起哪种行为的逻辑就和算法或行为的逻辑混合在一块儿,从而不可能再独立演化。继承的缺陷就是使得动态改变算法或行为变得不可能。
三、使用策略模式能够避免使用多重 if 语句
多重 if 语句不易维护,它把采起哪种算法或采起哪种行为的逻辑与算法或行为的逻辑混合在一块儿,通通列在一个多重转移语句里面,比使用继承的办法还要原始和落后
一、前面咱们发现要想使用策略模式,客户端(调用者)必须清楚全部的策略都是什么,才能决定使用哪个策略,也就是说,客户端必须理解全部算法的不一样,才能适时的选择合适的算法,若是客户端不清楚以上,那么策略模式不适用。
二、策略模式策略类膨胀的问题,这一度被认为是策略模式的阴暗面。由于不管是为了解决多重 if-else 语句,仍是解决 switch case 分支过多,而且扩展性差的问题,一旦可替换的策略(算法)愈来愈多,虽然扩展性获得解决,可是策略(算法)类太多了,每个分支对应一个……又很是繁琐,这就是它的一个缺点。换句话说,策略模式形成不少的策略类。
有时候能够经过把依赖于环境类的状态保存到客户端里面,而将策略类设计成可共享的,这样策略类实例能够被不一样客户端使用。换言之,可使用享元模式来减小对象的数量。下面就趁热打铁,说一说享元模式——减少内存的占用问题——享元模式和单例模式的对比分析
javax.servlet.http.HttpServlet 类:
HTTP 请求实际上只是一个 HTTP 请求报文,Web 容器会自动将这个 HTTP 请求报文包装成一个 HttpServletRequest 对象,而且自动调用 HttpServlet的 service() 方法来解析这个HTTP请求,service()方法会解析HTTP请求行,而HTTP请求行由method,URI,HTTPVersion三个组成,method就是get或者post,service() 方法根据 method 来决定是执行 doGet 仍是 doPost,这一切都是服务器(容器)自动完成的,HTTP的格式也自动被解析。
只要自定义的类继承了HttpServlet,而且在web.xml里面配置了相应的servlet和mapping,服务器就会自动执行以上过程。而咱们自定义的每个Servlet都必需要实现Servlet接口,而图里的GenericServlet是个通用的、不特定于任何协议的Servlet,它实现了 Servlet 接口
而 HttpServlet 继承于 GenericServlet,所以 HttpServlet 也实现了 Servlet 接口,因此咱们定义的Servlet只须要继承HttpServlet父类便可。Servlet接口中定义了一个service方法:
HttpServlet对该方法进行了实现,实现方式就是将ServletRequest与ServletResponse转换为HttpServletRequest与HttpServletResponse。转换完毕后,会调用HttpServlet类中本身定义的service方法
发现下面这个 service 是 protected方法
在该转换以后的 service 方法中,首先得到到请求的方法名,而后根据方法名调用对应的doXXX方法,好比说请求方法为GET,那么就去调用doGet方法;请求方法为POST,那么就去调用doPost方法。好比:
在HttpServlet类中所提供的doGet、doPost……方法都是直接返回错误信息,因此咱们须要在本身定义的Servlet类中重写这些方法,通过上面的过程,咱们发现 HttpServlet 类就是一个策略抽象类,咱们本身定义的servlet类去覆盖HttpServlet里的这些方法,天然不一样的人,不一样的项目里,须要重写的内容和方法都不尽相同,这就是一个策略模式的思想。