前言
作为一名合格的前端开发工程师,全面的把握面向对象的设计思维十分重要,而“设计模式”是泛滥软件开发人员通过相当长的一段时间的试验和谬误总结进去的,代表了面向对象设计思维的最佳实际。正如《HeadFirst设计模式》中说的一句话,十分好:
晓得形象、继承、多态这些概念,并不会马上让你变成好的面向对象设计者。设计巨匠关怀的是建设弹性的设计,能够保护,能够应酬扭转。
是的,很多时候咱们会感觉本人曾经分明的把握了面向对象的基本概念,封装、继承、多态都能纯熟应用,然而零碎一旦简单了,就无奈设计一个可保护、弹性的零碎。本文将联合 《HeadFirst设计模式》书中的示例加上本人一丢丢的集体了解,带大家意识下设计模式中的第一个模式——策略模式。
策略模式
策略模式:Strategy,是指,定义一组算法,并把其封装到一个对象中。而后在运行时,能够灵便的应用其中的一个算法
模仿鸭子游戏
设计模仿鸭子游戏
一个游戏公司开发了一款模仿鸭子的游戏,所有的鸭子都会呱呱叫(quack)、游泳(swim) 和 显示(dislay) 办法。
基于面向对象的设计思维,想到的是设计一个 Duck
基类,而后让所有的鸭子都集成此基类。
class Duck { quack() { } swim() { } display() { }}
绿头鸭(MallardDuck)和红头鸭(RedheadDuck)别离继承 Duck
类:
class MallardDuck extends Duck { quack() { console.log('gua gua'); } display() { console.log('I am MallardDuck'); }}class RedheadDuck extends Duck { display() { console.log('I am ReadheadDuck'); } quack() { console.log('gua gua'); }}
让所有的鸭子会飞
当初对所有鸭子提出了新的需要,要求所有鸭子都会飞。
设计者立马想到的是给 Duck
类增加 fly
办法,这样所有的鸭子都具备了航行的能力。
class Duck { quack() { } fly() { } swim() { } display() { }}
然而这个时候代码通过测试发现了一个问题,零碎中新加的橡皮鸭(RubberDuck)也具备了航行的能力了。这显然是不迷信的,橡皮鸭不会飞,而且也不会叫,只会收回“吱吱”声。
于是,设计者立马想到了覆写 RubberDuck
类的 duck
和 fly
办法,其中 fly
办法外面什么也不做。
class RubberDuck extends Duck { quack() { console.log('zhi zhi'); } fly() { }}
继承可能并不是最优解
设计者认真思考了上述设计,提出了一个问题:如果后续新增了更多类型的鸭子,有的鸭子既不会飞又不会叫怎么办呢?难道还是持续覆写 fly
或者 quack
办法吗?
显然,集成不是最优解。
通过一番考虑,设计者想到了通过接口来优化设计。
设计两个接口,别离是 Flable
和 Quackable
接口。
interface Flyable { fly(): void;}interface Quackable { quack(): void;}
这样,只有实现了 Flyable
的鸭子能力航行,实现了 Quackable
的鸭子能力谈话。
class MallardDuck implements Flayable, Quackable { fly() { } quack() { }}
通过接口尽管能够限度鸭子的行为,然而每个鸭子都要检查一下是否须要实现对应的接口,鸭子类型多起来之后是非常容易出错的,同时,通过接口的形式尽管限度了鸭子的行为,然而代码量却没有缩小,每个鸭子外部都要反复实现fly和quack的代码逻辑。
离开变动和不变动的局部
上面开始介绍咱们的第一个设计准则:
找出利用中可能须要变动之处,把它们独立进去,不要和那些不须要变动的代码混在一起。
在 Duck
类中,quack
和 fly
是会随着鸭子的不同而扭转的,而 swim 和 display 是每个鸭子都不变的。因而,这里能够使用第一个设计准则,就是离开变动和不变动的局部。
面向接口编程,而不是针对实现编程
为了更好的设计咱们的代码,当初介绍第二个设计准则:
针对接口编程,而不是针对实现编程。
针对接口编程的真正含意是针对超类型编程(抽象类或者接口),它利用了多态的个性。
在针对接口的编程中,一个变量申明的类型应该是一个超类型,超类型强调的是它与它的所有派生类共有的“个性”。
针对实现编程。
比方:
interface Animal { void makeSound();}class Dog implements Animal { public void makeSound() { bark(); } public void bark() { // 汪汪叫 }}Dog d = new Dog();d.bark();
因为 d
的类型是 Dog
,是一个具体的类,而不是抽象类,并且 bark
办法是 Dog
上特有的,不是共性。
针对接口编程。
Animal a = new Dog();a.makeSound();
变量 a
的类型是 Animal
,是一个形象类型,而不是一个具体类型。此时 a
调用 makeSound
办法,代表的是所有的 Animal
都能进行的一种操作。
当初咱们接着之前的思路,将鸭子的 fly
和 quack
两个行为变为两个接口 FlyBehavior
和 QuackBehavior
。所有的鸭子不间接实现这两个接口,而是有专门的行为类实现这两个接口。
interface FlyBehavior { fly(): void;}interface QuackBehavior { quack(): void;}
行为类来实现接口:
// 实现了所有能够航行的鸭子的动作class FlyWithWings implements FlyBehavior { fly(): void { console.log('I can fly with my wings !'); }}// 实现了所有不会航行的鸭子的动作class FlyNoWay implements FlyBehavior { fly(): void { console.log('I can not fly !'); }}// 实现了所有坐火箭航行的鸭子的动作class FlyRocketPowered implements FlyBehavior { fly(): void { console.log('I can fly with a rocket !'); }}// 实现了橡皮鸭的吱吱叫声class Squeak implements QuackBehavior { quack(): void { console.log('zhi zhi !'); }}// 实现了哑巴鸭的叫声class MuteQuack implements QuackBehavior { quack(): void { console.log(); }}
这样做有两个益处:
- 鸭子的行为能够被复用,因为这些行为曾经与鸭子自身无关了。
- 咱们能够新增一些行为,不会放心影响到既有的行为类,也不会影响有应用到航行行为的鸭子类。
整合鸭子的行为
当初鸭子的所有的行为须要被整合在一起,须要委托给他人解决。
持续革新 Duck
类。
abstract class Duck { flyBehavior: FlyBehavior; quackBehavior: QuackBehavior; constructor(flyBehavior: FlyBehavior, quackBehavior: QuackBehavior) { this.flyBehavior = flyBehavior; this.quackBehavior = quackBehavior; } public performFly(): void { this.flyBehavior.fly(); } public performQuack():void { this.quackBehavior.quack(); } public setFlyBehavior(flyBehavior: FlyBehavior) { this.flyBehavior = flyBehavior; } public abstract display(): void; public swim() { console.log('all ducks can swim !'); }}
在鸭子类外部定义两个变量,类型别离为 FlyBehavior
和 QuackBehavior
的接口类型,申明为接口类型不便后续通过多态的形式设置鸭子的行为。移除鸭子类中的 fly
和 quack
办法,因为这两个办法曾经被拆散到 fly
行为类和 quack
行为类中了。
通过 performQuack
办法来调用鸭子的行为,setFlyBehavior
办法来动静批改鸭子的行为。
所有的鸭子集成 Duck
类:
// 绿头鸭class MallardDuck extends Duck { constructor() { super(new FlyWithWings(), new Quack()); } display() { console.log('I am mallard duck !'); }}// 模型鸭class ModelDuck extends Duck { constructor() { super(new FlyNoWay(), new MuteQuack()); } public display(): void { console.log('I am model duck !'); }}
在鸭子的构造函数中调用父类的构造函数,初始化鸭子的行为。
测试鸭子游戏
class Test { duck: Duck; constructor() { this.duck = new MallardDuck(); } setPerformFly() { this.duck.setFlyBehavior(new FlyRocketPowered()); } quack() { this.duck.performQuack(); } fly() { this.duck.performFly(); }}const test = new Test();test.fly();test.quack();test.setPerformFly();test.fly();
通过 setFlyBehavior
能够动静的扭转鸭子的行为,是鸭子具备坐火箭航行的能力。
多用组合,少用集成
从下面的例子就能够看出,每一个鸭子都有一个 FlyBehavior
和 QuackBehavior
,让鸭子(Duck
类)将航行和呱呱叫委托它代为解决。
当你将两个类联合起来应用,这就是组合(composition
)。这种做法和继承不同的中央在于,鸭子的行为不是继承而来,而是和应用的行为对象组合而来的。也就是咱们要介绍的第三个设计准则:
多用组合,少用继承
策略模式的设计步骤
- 定义一个接口,接口中申明各个算法所共有的操作
interface Strategy { execute(): void;}
- 定义一系列的策略,并且在遵循
Strategy
接口的根底上实现算法
class StrategyA implements Strategy { execute() { console.log('I am StrategyA'); // 算法 A }}class StrategyB implements Strategy { execute() { console.log('I am StrategyB'); // 算法 B }}
- 定义一个上下文
Context
类,在Context
类中保护指向某个策略对象的援用,在构造函数中来接管策略类,同时还能够通过setStrategy
在运行时动静的切换策略类,Context
类通过setStrategy
来将具体的工作委派给策略对象。这里的Context
类也能够作为一个基类被具体的实现类所继承,就相当于上文鸭子游戏中介绍的 Duck 类,能够被 MallardDuck、ReadheadDuck 等继承。
class Context { private strategy: Strategy; constructor(stategy: Stategy) { this.stategy = Stategy; } executeStrategy() { this.stragegy.execute(); } setStrategy(stategy: Stategy) { this.stategy = stategy; }}
- 创立客户端类,客户端代码会依据条件来抉择具体的策略。
class Application { stragegy: Stragegy; constructor(stragegy: Stragegy) { this.stragegy = stragegy; } setCondition(condition) { if (condition === 'conditionA') { stragegy.setStrategy(new StrategyA()); } if (condition === 'conditionB') { stragegy.setStrategy(new StrategyB()); } } execute() { this.stragegy.execute(); }}const app = new Application();app.setCondition('conditionB');app.execute();
策略模式结构图:
策略模式的应用场景
当咱们在设计代码的时候,想应用对象中各种不同的算法变体,并心愿能在运行时切换算法时, 能够思考应用策略模式。
参考
源代码地址
- 针对接口编程,而不是针对实现编程
- 策略模式
- 《HeadFirst设计模式》