zoukankan      html  css  js  c++  java
  • [design-patterns]设计模式之一策略模式

    设计模式

    从今天开始开启设计模式专栏,我会系统的分析和总结每一个设计模式以及应用场景。那么首先,什么是设计模式呢,作为一个软件开发人员,程序人人都会写,但是写出一款逻辑清晰,扩展性强,可维护的程序就不是那么容易做到了。现实世界的问题复杂多样,如何将显示问题映射到我们编写的程序中本就是困难重重。另一方面,软件开发中一个不变的真理就是“一切都在变化之中”,这种变化可能来自于程序本身的复杂度,也可能来自于客户不断变化的需求,这就要求我们在编写程序中一定要考虑变化的因素,将变化的因素抽离出来,设计出一款低耦合的程序就成了程序设计中的难点。

    设计模式就是用来解决以上的这些问题的一种方法。应用好设计模式,可以让我们免受程序设计中的变化的苦恼。从设计初至今,设计模式不是一成不变的,也并不是库,它比库更加高级,而且并不是用来解决某一种特定的问题,而是广泛的面向我们在程序开发中遇到的困境。在我们学习完一门语言的相关语法后,我相信设计模式应该作为紧接着学习的重点,它会让你对相关语法的认识更加深刻。比如说java中的接口就是一例,接口是让java流行的重要原因,但是初学完接口的使用,我们并不能很好的掌握接口,只有应用好设计模式,接口才能够大放异彩,我们也才能够真正的体会到接口给面向对象设计带来的魔力。实际上,在很多流行的开源库或者框架中,我们可以看到大量的使用设计模式的例子。

    在网络中,有关于设计模式的讲解不在少数,在这里我并不会引出复杂的定义,而是以例子作为基础,道出我们在软件开发中面临的问题,然后引出设计模式,看看设计模式是如何帮助我们解决这些问题的。在之后的讲解中,我都会以java为主要语言,因为它的语法足够简单,使用其他语言作为主语言的开发者触类旁通即可。

    最后,关于学习设计模式,希望大家能看一些经典的书籍,不要以其他网路上的博客(包括我的)做抓手学习。毕竟别人嚼过的不香,只有自己品味才能真正理解。书籍最经典的莫过于四人组(经常简称为GoF,即Group of Four)的《设计模式:可复用面向对象软件的基础》,初学可以看《Head First设计模式》,另有一本《大话设计模式》是国人写的评价也很高,同样适合初学者。

    啰嗦了这么多,接下来开始步入正题。

    设计模式之一策略模式

    现在的你,在一家游戏开发公司供职。某一天,你的上司让你开发出一套简单的游戏,游戏中有多个角色,每个角色可以使用武器进行攻击。你打算先以角色入手,很快你设计出了以下的模型:

     

    但是很快,你就发现这样设计并没有一开始想象的那么美好,因为你的上司让你给角色新增一个聊天功能,这时候问题出现了。

    你很快想到因为所有角色都继承于Role,所以给Role添加一个聊天方法是理所应当的:

    但是这个时候,你的同事告诉你不要这样做,因为他之前设计了一些怪物模型,同样继承于Role类,很明显,怪物也可以使用武器战斗,但是他们不能参与聊天。

    你很郁闷,你的同事继承了你的类却没有告诉你,不过你告诉他不用大惊小怪,只要在怪物中重写聊天方法,让他们什么都不做就好了:

    但是你的同事告诉你不要逗他,因为他写的怪物少说已经有了二十几个,你不能残忍的让你的同事挨个去重写chat方法。

    当你们正在商讨解决方案时,你的上司告诉你,现在角色的武器太单一了,也就是说你不能把所有的战斗实现都放在父类中,我们需要更加多样化的武器。

    你很苦恼,但是作为有经验的开发人员,你很快想到可以采用接口去实现,将所有的动作都抽象成一个个接口,让子类实现需要的接口,这样可以解决上司的需求和同事的问题:

    但是,这一次是你自己将自己否定了,因为你的角色设计已经进行了很多,你不想将每个角色的方法都重新实现一遍,而且,这些武器很多都是重复的,作为受过良好的OO设计课程的开发人员,你不会强迫自己去写这些重复的代码。最重要的是,你的开发经验告诉你,这些武器很可能会发生改变,如果这样设计,当发生变化时,你就不得不去检查每一个角色类,并修改其中的代码。同样的,你的同事也同意你的看法。

    设计原则

    现在,你迫切的希望有一种设计模式能够解救你于水火之中。不过在揭晓这个设计模式之前,我们先回到原点,看一下我们遇到的问题到底是由什么原因造成的。

    我们可以看到,继承不能很好的解决我们的问题,因为角色的行为是不断发生变化的,他们使用不同的武器和技能,并且你的上司很可能会不断地要求你添加新的行为。接口看起来不错,但是接口不具有实现代码,无法做到代码的复用,这意味着如果你想将某一种行为做统一的变化,就需要一个一个类去检查方法的实现。

    要解决这个问题,首先我们需要明确一些设计原则,掌握了这些设计原则,我们才能更好的运用设计模式,解决问题。

    面对以上的情况,我们有一个原则正好适用,那就是“封装变化”:

    找出应用中可能需要变化的地方,把它们独立出来,不要和那些固定的代码混在一起。

    那么对于当前的应用来讲,战斗以及聊天功能都是变化的功能,我们就应该将他们独立出来,分别设计战斗功能和聊天功能。接下来就是如何实现这两个功能了。这又会用到一个设计原则,那就是“接口编程”:

    针对接口编程,而不是针对实现编程。

    相信这个原则大家都非常清楚,我就不多讲了,接下来直接上代码,这里我只分析战斗功能,其他功能也是类似的:

    设计实现

    首先是接口设计:

    1 public interface IFight {
    2     void fight();
    3 }

    这个接口很简单,只是提供了fight方法,接下来我们实现几个用各种武器战斗的类:

     1 public class FightUseAxe implements IFight {
     2     @Override
     3     public void fight() {
     4         System.out.println("使用斧子战斗");
     5     }
     6 }
     7 ===============================================
     8 public class FightUseBlade implements IFight {
     9     @Override
    10     public void fight() {
    11         System.out.println("使用剑战斗");
    12     }
    13 }
    14 ===============================================
    15 public class FightUseKnife implements IFight {
    16     @Override
    17     public void fight() {
    18         System.out.println("使用匕首战斗");
    19     }
    20 }

    注意上面的代码不能写在一个同一个文件中。

    接下来我们对角色类进行重构:

     1 public abstract class Role {
     2     
     3     private IFight weapon;
     4     
     5     public void fight() {
     6         weapon.fight();
     7     }
     8     
     9     public void setWeapon(IFight weapon) {
    10         this.weapon = weapon;
    11     }
    12     
    13     public abstract void display();
    14 }

    父类中我们实现了fight()方法,做到了统一管理,但是具体方法的实现实际上是交给子类去完成了,也就是IFight属性的赋值是在子类中完成的。接下来我们来看其中一个子类:

     1 public class King extends Role {
     2     @Override
     3     public void display() {
     4         System.out.println("显示国王的样子");
     5     }
     6     
     7     public static void main(String[] args) {
     8         Role role = new King();
     9         role.display();
    10         role.setWeapon(new FightUseAxe());
    11         role.fight();
    12         role.setWeapon(new FightUseBlade());
    13         role.fight();
    14     }
    15     /**
    16      * 运行结果:
    17      * 显示国王的样子
    18      * 使用斧子战斗
    19      * 使用剑战斗
    20      */
    21 }

    我们可以看到,通过将战斗独立出来,我们实现了使用者(King)和武器的松耦合,即我们可以动态的改变角色使用的武器。同样的对于其他的角色也是一样,实际上,这是一种方法的委托,角色类不再实现战斗的具体方式,而是交给了成员属性IFight去执行。从另一方面来说,这是一种组合的概念,它和继承不同的地方在于,角色的战斗方式并不是继承而来的,而是和适当的战斗类来组合而成。在这个实例中,我们可以明显的看到组合的优势明显大于继承。

    同样,这是一个很重要的设计原则,“多用组合,少用继承”。

    其实,从更加抽象的角度来讲,例子中的战斗方式实际上是一种算法,通过策略模式,我们分离了使用算法的角色和算法之间的联系。因此我们可以给出策略模式的定义:

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

    总结

    今天我们学习了第一个设计模式即策略模式,当我们面对一些程序中可能会频繁变化的部分时可以采用此模式,它让算法的实现独立于使用算法的客户。同时我们还学习了三个面向对象设计中的重要原则:

    • 封装变化
    • 多用组合,少用继承
    • 针对接口编程,而不针对实现编程

    最后,为了巩固我们的学习成果,下面我们思考这样一个问题。在一片草原中,一群游牧民族养了一群羊和一群马,也就是说在这个草原中有三种生物,分别是人、马、羊、他们的动作有吃(eat)和睡(sleep),不过吃和睡的方式不同,马是站着吃站着睡,羊是站着吃趴着睡,人是坐着吃躺着睡。那么我们该怎样设计该OO模型,在实现功能的基础上,达到低耦合的要求,满足不断变化的需求呢?

  • 相关阅读:
    27 Spring Cloud Feign整合Hystrix实现容错处理
    26 Spring Cloud使用Hystrix实现容错处理
    25 Spring Cloud Hystrix缓存与合并请求
    24 Spring Cloud Hystrix资源隔离策略(线程、信号量)
    23 Spring Cloud Hystrix(熔断器)介绍及使用
    22 Spring Cloud Feign的自定义配置及使用
    21 Spring Cloud使用Feign调用服务接口
    20 Spring Cloud Ribbon配置详解
    19 Spring Cloud Ribbon自定义负载均衡策略
    18 Spring Cloud Ribbon负载均衡策略介绍
  • 原文地址:https://www.cnblogs.com/dotgua/p/strategy-pattern.html
Copyright © 2011-2022 走看看