老王之策略模式

3 篇文章 0 订阅

一、 什么是设计模式

在我看来设计模式就是利用对应语言的特性,合理降低复杂度,将各个对象解耦,提高项目的扩展性,为后续维护提供有力支撑。

一开始并没有设计模式只说,老一辈开发者们也是慢慢摸索,在这个探索过程中,有先贤大能之士总结出一套可行性高的方案,我们将此方案分门别类,并称之为设计模式。

二、为什么要使用设计模式

正如上文中提到,主要目的是为了日后的维护扩展,一个毫无设计的屎山项目维护起来那是相当要人命的!

三、怎样使用设计模式

下文中的所有内容,都将围绕此主题展开

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:多用组合,少用继承

咱们总结下目前学到的设计模式原则:

将易于变化 不确定的部分 单独抽出封装,不要跟不需要变化的部分放在一起,让其与其他类松耦合。

  1. 找出应用中变化之处,把它独立出来不要和那些不需要变化等代码混在一起。
  2. 针对接口编程而不是针对实现
  3. 多用组合,少用继承。

利用组合创建的系统,更具有弹性、扩展性,可以在运行时改变行为,也就是说运行时判断使用那种算法或策略。

给策略模式一个严格些的定义:

策略模式定义了算法族,分别封装起来,让它们之间可以互相替换,此模式让算法的变化独立于使用算法的客户。

此时我们已经学习完这个策略模式,当然并不代表看完就会了,最重要的是多次实践练习,能够在关键时刻,想到该怎么灵活使用。

------------ 参考自《Head First 设计模式》

                                                                                                                下一篇将带来 观察者模式

                                                -------------- 未完待续 ----------------

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值