一、 什么是设计模式
在我看来设计模式就是利用对应语言的特性,合理降低复杂度,将各个对象解耦,提高项目的扩展性,为后续维护提供有力支撑。
一开始并没有设计模式只说,老一辈开发者们也是慢慢摸索,在这个探索过程中,有先贤大能之士总结出一套可行性高的方案,我们将此方案分门别类,并称之为设计模式。
二、为什么要使用设计模式
正如上文中提到,主要目的是为了日后的维护扩展,一个毫无设计的屎山项目维护起来那是相当要人命的!
三、怎样使用设计模式
下文中的所有内容,都将围绕此主题展开
1.策略模式
提到策略模式,我就不得不想起《headfirst设计模式》中各种奇葩鸭子。
那么咱们就仿照原书中的例子来(或者魔改成小只因🐔?)。
老王的公司给孩子们做了一款模拟小鸡游戏 ImitateChickGame:
如图:
年前因为竞争压力加剧,公司主管认为创新的时候到了,需要在股东会议上展示点不一样的。
主管一琢磨,喊道:“老王给大家整个活!”
老王暗自不忿“啥活都让老子干,老子是全栈不是全干!”,随即决定应付一下:
“要不就加入一个小鸡飞行功能,跟公司保证一周搞定。”
“只需要在Chick类中加上fly()方法,然后所有鸡都会继承,这还不简单?”
但是一个麻烦的问题来了:
老王忽略了一件事,愿系统中并非所有的鸡都会飞,但都继承自Chick类,一只退化了翅膀的铁公鸡,在屏幕上“飞来飞去”,看呆了众人。
“老铁什么情况,这咋搞的?”同事们纷纷摸不着头脑。
老王尴了个大尬“好吧,是有点莫名其妙,但是当成游戏的一种特色的话,也是可以的......”
老王这次体会到一件事:当涉及"维护"时,基于复用的目的去使用继承,会出现尴尬的问题。
“由于铁公鸡不会飞也不会叫,更不吃虫子,似乎我可以在铁公鸡的类里把fly、crow、eat方法覆盖掉...”
“可是如果以后又加入什么奇葩鸡种,像玩具尖叫鸡不会飞但它会叫....”
此时继承导致了以下主要缺点:
A:运行时行为不容易改变。B:改变会牵一发而动全身。
老王苦思冥想,知道继承是无法满足需求了,以后每几个月都要迭代,每当新的小鸡引入,都有可能需要覆盖fly、crow等方法......没完没了。
那么使用接口呢?
可以把fly方法从超类中取出,放进“flyable接口”中,这样一来,只有会飞的鸡才去实现该接口,同样的eat、crow等方法也可以设计为接口。
如图:
仔细看一下,好像解决了牵一发而动全身的问题,但是又进入了另一个噩梦,很多类的代码无法复用,
而且各种能飞能叫的鸡,飞行、叫声细节还有所不同,也就是说每个fly、crow方法都要单独写。
此时,老王最盼望的就是设计模式,期望它像炎炎夏日的一瓶快乐水,让自己活过来。(快乐水无敌!)
如果能有一种建立软件的方法,可以让我们以最小的改动代价,花尽量少的时间重写代码,腾出时间看小姐姐算法就好了。
如果你是老王你会怎么做呢?
软件开发中有一个一直伴随项目的真理,就是CHANGE(改变!)。
不管软件最初设计的有多好,上线一段时间,总是跟随着需求慢慢改变与成长,否则项目就死掉了。
驱动改变的原因太多了,如讨厌的甲方想要新功能、从其他公司接手数据,数据库不兼容,造成格式不兼容等等。
把问题归零
我们知道但靠上边的方法,无法解决问题,幸运的是有一个设计原则恰好适合现在的情况。
设计原则1:找出应用中变化之处,把它独立出来不要和那些不需要变化等代码混在一起。
简单来说就是把变化的部分“封装”起来,好让其它部分不会受到影响,让系统更有弹性。
比如每过一段时间某个模块都要发生变动,那么这些经常变动的部分就要考虑抽取,把它跟其它稳定的代码隔离。
这几乎是每个设计模式背后的精神。所有的模式都提供了一套方法让“系统中某部分的变化不影响其它模块”。
咱们再详细设计一下:
现在我们要分开变化的部分,目前来看fly、crow、eat等经常变化,暂且忽略eat,准备两组类完全远离Chick类,
一个是fly相关的,一个是crow相关的,每一组类实现各自的动作。
比如我们可以有一个类实现公鸡叫(高昂的啼叫),另一个实现母鸡叫(喔喔喔),再来一个实现安静或玩具尖叫鸡的那种要死要死的尖叫。
如何设计这一组组的行为呢?
我们希望这个设计具有弹性,也就是说,我们可以指定某些鸡具有飞行等行为,比如新加入一种野鸡,
我们可以动态的赋予其飞行的能力,在运行时改变野鸡的行为,这样我们就得将行为抽象为接口。
设计原则2 针对接口编程而不是针对实现
我们利用接口来定义各种行为,例如飞行行为FlyBehavior,其它飞行行为皆实现此接口。
鸡的超类对此完全不管,由行为类FlyBehavior来支配行为.
针对接口编程的关键在于“多态”。
利用多态,程序可以针对超类型编程(接口),执行时会根据实际情况执行到真正的行为。
例如有一个Animal抽象类(接口),Dog、Cat继承自Animal,那么:
“针对实现编程”:
abstract class Animal{
abstract void makeSound();
}
//Dog extends Animal
class Dog extends Animal{
void makeSound(){
bark();//汪汪叫
}
void bark(){}
}
class Cat extends Animal{
void makeSound(){
meow();//喵喵叫
}
void meow(){}
}
Dog d = new Dog();
/*
*声明"d"为Dog类型,为具体实现,会造成针对实现编码。
*/
d.bark();
//但是针对"接口/超类型"编程
Animal animal = new Dog();
animal.makeSound();//利用animal多态调用,实现真正调用真正执行的方法
//更棒的是,我们的子类实例化时不进行硬编码例如new Dog();改成:
a = getAnimal();
a.makeSound();
//我们不知道实际的子类型,我们只关心正确的makeSound()就够了.
我们接着实现小鸡的行为
我们建立两个接口:
这样的设计可以让飞行和叫声的动作被其它对象复用,因为这些跟鸡的超类无关了。
而我们新增的一些行为,不会影响到现有行为类。
问:一个类代表一个行为,感觉似乎有点奇怪,类不应该是带表某种“东西”的集合吗,类不应该是同时具备状态与行为吗?
答:在OO系统中,类确实代表某些相同特征的集合,是既有状态也有方法。只是,在当前这个例子中,碰巧“东西”是个行为。但即便是行为,也可以有状态和方法,例如,飞行行为可以有具体的实例变量,记录飞行行为的属性(每秒翅膀拍动几下、最大高度、速度等)
关键在于,小鸡将飞行与叫声相关的动作,委托给别的类处理。
而不是定义在小鸡的超类中。
我们定义两个相似的方法performFly()和performCrow()取代了超类中fly与crow。
现在我们来实现一下performCrow():
public class Chick
{
//每只鸡的实例都会引用CrowBehavior接口对象
//高昂叫接口
CrowBehavior crowBehavior;
//fly接口
FlyBehavior flyBehavior;
public void performCrow()
{
//🐔对象不亲自处理叫声行为 而是委托给接口引用的对象
crowBehavior.crow();
}
//我们可以动态设定 接口的实例
public void setFlyBehavior(FlyBehavior fb)
{
this.flyBehavior = fb;
}
public void setCrowBehavior(CrowBehavior cb)
{
this.crowBehavior = cb;
}
//省略一些其它方法...
}
这样Chick的所有实例都与行为分离,我们只关注对象如何进行叫声行为就行了。
并且,我们随时可以改变小鸡的行为。
我们引入玩具尖叫鸡ScreamingChicken:
/*
*玩具尖叫鸡
*/
public class ScreamingChicken extends Chick
{
public ScreamingChicken()
{
//一开始我们的玩具尖叫鸡是不会飞的
//别忘了flyBehavior与crowBehavior 继承自Chick
flyBehavior = new FlyNoWay();
//Crow高昂啼叫 继承自 CrowBehavior接口
crowBehavior = new Crow();
}
public void display()
{
System.out.println("老子是尖叫鸡🐔!");
}
}
建立一个新的飞行行为实例,玩具尖叫鸡不会飞,那是因为没有生理学上的翅膀,我们整一个火箭🚀的。
public class FlyRocketPowered implements FlyBehavior
{
@Override
public void fly()
{
System.out.println("老子采用火箭动力飞行,咻咻咻~小飞棍来no");
}
}
测试下咱们使玩具尖叫鸡具有飞行功能:
public class ChickTest
{
public static void main(String[]args)
{
//玩具尖叫鸡
Chick scremingChicken = new ScremingChicken();
//此时采用默认构造方法创建了FlyNoWay的实例 不能飞行
screamingChicken.performfly();
scremingchicken.performCorw();
//开始蜕变
Chick rocketScreamingChicken = new ScreamingChicken();
//调用setFlyBehavior方法传入火箭动力飞行的实例
rocketScreamingChicken.setFlyBehavior(new FlyRocketPowered());
rocketScreamingChicken.performFly();//动弹滴赋予了飞行行为
}
}
封装行为的大局观
我们脚步稍停,呼吸一口新鲜口气,全面审视一下代码整体布局。
我们重新调整了小鸡模拟器的设计,让小鸡实例继承Chick,飞行fly等行为实现对应的接口。
我们描述事情的方法也稍有改变,我们不再把🐤到行为说成是一组行为,我们开始把行为想成是“一族算法”。
请注意下图中类的关系,想一想它们是IS- A(是一个)、HAS-A(有一个)或implements(实现)。
我们可以发现,“有一个”可能比“是一个”更好。当两个类结合起来使用,就是组合。这种做法和继承的不同之处在于,小鸡的行为不是继承来的,而是与适当的行为对象组合起来的。
这是一个设计原则:设计原则3:多用组合,少用继承。
咱们总结下目前学到的设计模式原则:
将易于变化 不确定的部分 单独抽出封装,不要跟不需要变化的部分放在一起,让其与其他类松耦合。
- 找出应用中变化之处,把它独立出来不要和那些不需要变化等代码混在一起。
- 针对接口编程而不是针对实现
- 多用组合,少用继承。
利用组合创建的系统,更具有弹性、扩展性,可以在运行时改变行为,也就是说运行时判断使用那种算法或策略。
给策略模式一个严格些的定义:
策略模式定义了算法族,分别封装起来,让它们之间可以互相替换,此模式让算法的变化独立于使用算法的客户。
此时我们已经学习完这个策略模式,当然并不代表看完就会了,最重要的是多次实践练习,能够在关键时刻,想到该怎么灵活使用。
------------ 参考自《Head First 设计模式》
下一篇将带来 观察者模式
-------------- 未完待续 ----------------