策略模式-定义一个算法族

公号:码农充电站pro
主页:https://codeshellme.github.io

本篇来介绍策略模式(Strategy Design Pattern)。

假设我们要为动物进行建模,比如狗,猪,兔子等,每种动物的能力是不同的。

1,使用继承

首先你可能想到用继承的方式来实现,所以我们编写了下面这个 Animal 类:

abstract class Animal {
    public void run() {
        System.out.println("I can run.");
    }

    public void drinkWater() {
        System.out.println("I can drink water.");
    }

    protected abstract String type();
}

Animal 是一个抽象类,其中包括了动物的能力,每种能力用一个方法表示:

  • run:奔跑能力。
  • drinkWater:喝水能力。
  • type:返回动物的种类,比如“狗”,“兔子”。这是一个抽象方法,子类要去实现。

然后我们编写 DogPigRabbit

class Dog extends Animal {
    public String type() {
        return "Dog";
    }
}

class Pig extends Animal {
    public String type() {
        return "Pig";
    }
}

class Rabbit extends Animal {
    public String type() {
        return "Rabbit";
    }
}

上面的三种动物都继承了 Animal 中的 rundrinkWater,并且都实现了自己的 type 方法。

现在我们想给 PigRabbit 加入吃草的能力,最直接的办法是分别在这两个类中加入 eatGrass 方法,如下:

class Pig extends Animal {
    public void eatGrass() {
        System.out.println("I can eat grass.");
    }

    public String type() {
        return "Pig";
    }
}

class Rabbit extends Animal {
    public void eatGrass() {
        System.out.println("I can eat grass.");
    }

    public String type() {
        return "Rabbit";
    }
}

上面代码能够达到目的,但是不够好,因为PigRabbit 中的 eatGrass 一模一样,是重复代码,代码没能复用。

为了解决代码复用,我们可以将 eatGrass 方法放到 Animal 中,利用继承的特性,PigRabbit 中就不需要编写 eatGrass 方法,而直接从 Animal 中继承就行。

但是,这样还是有问题,因为如果将 eatGrass 放在 Animal 中,Dog 中也会有 eatGrass ,而我们并不想让 Dog 拥有吃草的能力。

也许你会说,我们可以在 Dog 中将 eatGrass 覆盖重写,让 eatGrass 不具有实际的能力,就像这样:

class Dog extends Animal {
    public void eatGrass() {
        // 什么都不写,就没有了吃草的能力
    }

    public String type() {
        return "Rabbit";
    }
}

这样做虽然能到达目的,但是并不优雅。如果 Animal 的子类特别多的话,就会有很多子类都得这样覆盖 eatGrass 方法。

所以,将 eatGrass 放在 Animal 中也不是一个好的方案。

2,使用接口

那是否可以将 eatGrass 方法提取出来,作为一个接口?

就像这样:

interface EatGrassable {
    void eatGrass();
}

然后,让需要有吃草能力的动物都去实现该接口,就像这样:

class Rabbit extends Animal implements EatGrassable {
    public void eatGrass() {
        System.out.println("I can eat grass.");
    }

    public String type() {
        return "Rabbit";
    }
}

这样做可以达到目的,但是,缺点是每个需要吃草能力的动物之间就会有重复的代码,依然没有达到代码复用的目的。

所以,这种方式还是不能很好的解决问题。

3,使用行为类

我们可以将吃草的能力看作一种“行为”,然后使用“行为类”来实现。那么需要有吃草能力的动物,就将吃草类的对象,作为自己的属性。

这些行为类就像一个个的组件,哪些类需要某种功能的组件,就直接拿来用。

下面我们编写“吃草类”:

interface EatGrassable {
    void eatGrass();
}

class EatGreenGrass implements EatGrassable {
    // 吃绿草
    public void eatGrass() {
        System.out.println("I can eat green grass.");
    }
}

class EatDogtailGrass implements EatGrassable {
    // 吃狗尾草
    public void eatGrass() {
        System.out.println("I can eat dogtail grass.");
    }
}

class EatNoGrass implements EatGrassable {
    // 不是真的吃草
    public void eatGrass() {
        System.out.println("I can not eat grass.");
    }
}

首先创建了一个 EatGrassable 接口,但是不用动物类来实现该接口,而是我们创建了一些行为类 EatGreenGrassEatDogtailGrassEatNoGrass,这些行为类实现了 EatGrassable接口。

这样,需要吃草的动物,不但能够吃草,而且可以吃不同种类的草。

那么,该如何使用 EatGrassable 接口呢?需要将 EatGrassable 作为 Animal 的属性,如下:

abstract class Animal {

    // EatGrassable 对象作为 Animal 的属性
    protected EatGrassable eg;

    public Animal() {
        eg = null;
    }

    public void run() {
        System.out.println("I can run.");
    }

    public void drinkWater() {
        System.out.println("I can drink water.");
    }

    public void eatGrass() {
        if (eg != null) {
            eg.eatGrass();
        }
    }

    protected abstract String type();
}

可以看到,Animal 中增加了 eg 属性和 eatGrass 方法。

其它动物类在构造函数中,要初始化 eg 属性:

class Dog extends Animal {
    public Dog() {
        // Dog 不能吃草
        eg = new EatNoGrass();    
    }
    
    public String type() {
        return "Dog";
    }
}

class Pig extends Animal {
    public Pig() {
        eg = new EatGreenGrass();
    }
    
    public String type() {
        return "Pig";
    }
}

class Rabbit extends Animal {
    public Rabbit() {
        eg = new EatDogtailGrass();
    }
    
    public String type() {
        return "Rabbit";
    }
}

对代码测试:

public class Strategy {
    public static void main(String[] args) {
        Animal dog = new Dog();
        Animal pig = new Pig();
        Animal rabbit = new Rabbit();

        dog.eatGrass();    // I can not eat grass.
        pig.eatGrass();    // I can eat green grass.
        rabbit.eatGrass(); // I can eat dogtail grass.
    }
}

4,策略模式

实际上,上面的实现方式使用的就是策略模式。重点在于 EatGrassable 接口与三个行为类 EatGreenGrassEatDogtailGrassEatNoGrass。在策略模式中,这些行为类被称为算法族,所谓的“策略”,可以理解为“算法”,这些算法可以互相替换。

策略模式定义了一系列算法族,并封装在类中,它们之间可以互相替换,此模式让算法的变化独立于使用算法的客户

我将完整的代码放在了这里,供大家参考,类图如下:

在这里插入图片描述

5,继承与组合

在一开始的设计中,我们使用的是继承(Is-a) 的方式,但是效果并不是很好。

最终的方案使用了策略模式,它是一种组合(Has-a) 关系,即 AnimalEatGrassable 之间的关系。

这也是一种设计原则:多用组合,少用继承,组合关系比继承关系有更好的弹性。

6,动态设定行为

策略模式不仅重在创建一组算法(行为类),能够动态的让这些算法互相替换,也是策略模式典型应用。

所谓的“动态”是指,在程序的运行期间,根据配置,用户输入等方式,动态的设置算法。

只需要在 Animal 中加入 setter 方法即可,如下:

abstract class Animal {
    // 省略了其它代码
    
    public void setEatGrassable(EatGrassable eg) {
        this.eg = eg;
    }
}

使用 setter 方法:

Animal pig = new Pig();
pig.eatGrass();	// I can eat green grass.

pig.setEatGrassable(new EatDogtailGrass()); // 设置新的算法
pig.eatGrass();	// I can eat dogtail grass.

本来 pig 吃的是绿草,我们通过 setter 方法将 绿草 换成了 狗尾草,可以看到,算法的切换非常方便。

7,总结

策略模式定义了一系列算法族,这些算法族也可以叫作行为类。策略模式使用了组合而非继承来构建类之间的关系,组合关系比继承关系更加有弹性,使用组合也比较容易动态的改变类的行为。

(本节完。)

推荐阅读:

设计模式之高质量代码

单例模式-让一个类只有一个实例

工厂模式-将对象的创建封装起来

欢迎关注作者公众号,获取更多技术干货。

码农充电站pro

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


什么是设计模式一套被反复使用、多数人知晓的、经过分类编目的、代码 设计经验 的总结;使用设计模式是为了 可重用 代码、让代码 更容易 被他人理解、保证代码 可靠性;设计模式使代码编制  真正工程化;设计模式使软件工程的 基石脉络, 如同大厦的结构一样;并不直接用来完成代码的编写,而是 描述 在各种不同情况下,要怎么解决问题的一种方案;能使不稳定依赖于相对稳定、具体依赖于相对抽象,避免引
单一职责原则定义(Single Responsibility Principle,SRP)一个对象应该只包含 单一的职责,并且该职责被完整地封装在一个类中。Every  Object should have  a single responsibility, and that responsibility should be entirely encapsulated by t
动态代理和CGLib代理分不清吗,看看这篇文章,写的非常好,强烈推荐。原文截图*************************************************************************************************************************原文文本************
适配器模式将一个类的接口转换成客户期望的另一个接口,使得原本接口不兼容的类可以相互合作。
策略模式定义了一系列算法族,并封装在类中,它们之间可以互相替换,此模式让算法的变化独立于使用算法的客户。
设计模式讲的是如何编写可扩展、可维护、可读的高质量代码,它是针对软件开发中经常遇到的一些设计问题,总结出来的一套通用的解决方案。
模板方法模式在一个方法中定义一个算法的骨架,而将一些步骤延迟到子类中,使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤。
迭代器模式提供了一种方法,用于遍历集合对象中的元素,而又不暴露其内部的细节。
外观模式又叫门面模式,它提供了一个统一的(高层)接口,用来访问子系统中的一群接口,使得子系统更容易使用。
单例模式(Singleton Design Pattern)保证一个类只能有一个实例,并提供一个全局访问点。
组合模式可以将对象组合成树形结构来表示“整体-部分”的层次结构,使得客户可以用一致的方式处理个别对象和对象组合。
装饰者模式能够更灵活的,动态的给对象添加其它功能,而不需要修改任何现有的底层代码。
观察者模式(Observer Design Pattern)定义了对象之间的一对多依赖,当对象状态改变的时候,所有依赖者都会自动收到通知。
代理模式为对象提供一个代理,来控制对该对象的访问。代理模式在不改变原始类代码的情况下,通过引入代理类来给原始类附加功能。
工厂模式(Factory Design Pattern)可细分为三种,分别是简单工厂,工厂方法和抽象工厂,它们都是为了更好的创建对象。
状态模式允许对象在内部状态改变时,改变它的行为,对象看起来好像改变了它的类。
命令模式将请求封装为对象,能够支持请求的排队执行、记录日志、撤销等功能。
备忘录模式(Memento Pattern)保存一个对象的某个状态,以便在适当的时候恢复对象。备忘录模式属于行为型模式。 基本介绍 **意图:**在不破坏封装性的前提下,捕获一个对象的内部状态,并在该
顾名思义,责任链模式(Chain of Responsibility Pattern)为请求创建了一个接收者对象的链。这种模式给予请求的类型,对请求的发送者和接收者进行解耦。这种类型的设计模式属于行为
享元模式(Flyweight Pattern)(轻量级)(共享元素)主要用于减少创建对象的数量,以减少内存占用和提高性能。这种类型的设计模式属于结构型模式,它提供了减少对象数量从而改善应用所需的对象结