`

JDP02-[策略模式]-鸭子模型

 
阅读更多

当我们掌握了Java的语法,当我们了解了面向对象的封装、继承、多态等特性,当我们可以用Swing、Servlet、JSP技术构建桌面以及Web应用,不意味着我们可以写出面向对象的程序,不意味着我们可以很好的实现代码复用,弹性维护,不意味着我们可以实现在维护、扩展基础上的代码复用。一把刀,可以使你制敌于无形而于江湖扬名,也可以只是一把利刃而使你切菜平静。Java,就是这把刀,它的威力取决于你使用的方式。当我们陷入无尽无止重复代码的泥沼,当我们面临牵一发而动全身的维护恶梦, 你应该想起“设计模式”这个行动秘笈。面向对象的精义,看似平淡,其实要经过艰苦实践才能成功。而构造OO系统的隐含经验于是被前人搜集而成并冠以“设计模式”之名。我们应该在编码行动初始就携带以它。接下来,让我们步“四人组”先行者之后,用中国文字、用实际案例领略模式于我们代码焕然一新的改变:

 

设计模式解读之一: 策略模式

 

    1. 模式定义
    
        把会变化的内容取出并封装起来,以便以后可以轻易地改动或扩充部分,而不影响不需要变化的其他部分;

    2. 问题缘起

    当涉及至代码维护时,为了复用目的而使用继承,结局并不完美。对父类的修改,会影响到子类型。在超类中增加的方法,会导致子类型有该方法,甚至连那些不该具备该方法的子类型也无法免除。示例,一个鸭子类型:

    public abstract class Duck {
        //所有的鸭子均会叫以及游泳,所以父类中处理这部分代码
        public void quack() {
            System.out.println("Quack");
        }
        
        public void swim() {
            System.out.println("All ducks float, even decoys.");        
        }
        
        //因为每种鸭子的外观是不同的,所以父类中该方法是抽象的,由子类型自己完成。
        public abstract void display();
    }

    public class MallardDuck extends Duck {
        //野鸭外观显示为绿头
        public void display() {
            System.out.println("Green head.");
        }
    }

    public class RedHeadDuck extends Duck {
        //红头鸭显示为红头
        public void display() {
            System.out.println("Red head.");
        }
    }

    public class RubberDuck extends Duck {
        //橡皮鸭叫声为吱吱叫,所以重写父类以改写行为
        public void quack() {
            System.out.println("Squeak");
        }

        //橡皮鸭显示为黄头
        public void display() {
            System.out.println("Yellow head.");
        }
    }

    上述代码,初始实现得非常好。现在我们如果给Duck.java中加入fly()方法的话,那么在子类型中均有了该方法,于是我们看到了 会飞的橡皮鸭子,你看过吗?当然,我们可以在子类中通过空实现重写该方法以解决该方法对于子类型的影响。但是父类中再增加其它的方法呢?

    通过继承在父类中提供行为,会导致以下缺点:

    a. 代码在多个子类中重复;
    b. 运行时的行为不容易改变;
    c. 改变会牵一发动全身,造成部分子类型不想要的改变;

    好啦,还是刚才鸭子的例子,你也许想到使用接口,将飞的行为、叫的行为定义为接口,然后让Duck的各种子类型实现这些接口。这时侯代码类似于:

    public abstract class Duck {
        //将变化的行为 fly() 以及quake()从Duck类中分离出去定义形成接口,有需求的子类中自行去实现

        public void swim() {
            System.out.println("All ducks float, even decoys.");        
        }
        
        public abstract void display();
    }

    //变化的 fly() 行为定义形成的接口
    public interface FlyBehavior {
        void fly();
    }

    //变化的 quack() 行为定义形成的接口
    public interface QuackBehavior {
        void quack();
    }

    //野鸭子会飞以及叫,所以实现接口  FlyBehavior, QuackBehavior
    public class MallardDuck extends Duck implements FlyBehavior, QuackBehavior{
        public void display() {
            System.out.println("Green head.");
        }

        public void fly() {
            System.out.println("Fly.");                
        }

        public void quack() {
            System.out.println("Quack.");                
        }
    }

    //红头鸭子会飞以及叫,所以也实现接口  FlyBehavior, QuackBehavior
    public class RedHeadDuck extends Duck implements FlyBehavior, QuackBehavior{
        public void display() {
            System.out.println("Red head.");
        }    

        public void fly() {
            System.out.println("Fly.");                
        }

        public void quack() {
            System.out.println("Quack.");                
        }    
    }

    //橡皮鸭不会飞,但会吱吱叫,所以只实现接口QuackBehavior
    public class RubberDuck extends Duck implements QuackBehavior{
        //橡皮鸭叫声为吱吱叫
        public void quack() {
            System.out.println("Squeak");
        }

        //橡皮鸭显示为黄头
        public void display() {
            System.out.println("Yellow head.");
        }
    }

    上述代码虽然解决了一部分问题,让子类型可以有选择地提供一些行为(例如 fly() 方法将不会出现在橡皮鸭中).但我们也看到,野鸭子MallardDuck.java和红头鸭子RedHeadDuck.java的一些相同行为代码不能得到重复使用。很大程度上这是从一个火坑跳到另一个火坑。

    在一段程序之后,让我们从细节中跳出来,关注一些共性问题。不管使用什么语言,构建什么应用,在软件开发上,一直伴随着的不变的真理是:需要一直在变化。不管当初软件设计得多好,一段时间之后,总是需要成长与改变,否则软件就会死亡。

    我们知道,继承在某种程度上可以实现代码重用,但是父类(例如鸭子类Duck)的行为在子类型中是不断变化的,让所有子类型都有这些行为是不恰当的。我们可以将这些行为定义为接口,让Duck的各种子类型去实现,但接口不具有实现代码,所以实现接口无法达到代码复用。这意味着,当我们需要修改某个行为,必须往下追踪并在每一个定义此行为的类中修改它,一不小心,会造成新的错误。

    设计原则:把应用中变化的地方独立出来,不要和那些不需要变化的代码混在一起。这样代码变化引起的不经意后果变少,系统变得更有弹性。

    按照上述设计原则,我们重新审视之前的Duck代码。

    1) 分开变化的内容和不变的内容

       Duck类中的行为 fly(), quack(), 每个子类型可能有自己特有的表现,这就是所谓的变化的内容。
           Duck类中的行为 swim() 每个子类型的表现均相同,这就是所谓不变的内容。

       我们将变化的内容从Duck()类中剥离出来单独定义形成接口以及一系列的实现类型。将变化的内容定义形成接口可实现变化内容和不变内容的剥离。其实现类型可实现变化内容的重用。这些实现类并非Duck.java的子类型,而是专门的一组实现类,称之为"行为类"。由行为类而不是Duck.java的子类型来实现接口。这样,才能保证变化的行为独立于不变的内容。于是我们有:

       变化的内容:

       //变化的 fly() 行为定义形成的接口
       public interface FlyBehavior {
        void fly();
       }
         
       //变化的 fly() 行为的实现类之一
       public class FlyWithWings implements FlyBehavior {
        public void fly() {
            System.out.println("I'm flying.");
        }
       }

       //变化的 fly() 行为的实现类之二
       public class FlyNoWay implements FlyBehavior {
        public void fly() {
            System.out.println("I can't fly.");
        }
       }

           -----------------------------------------------------------------

       //变化的 quack() 行为定义形成的接口
       public interface QuackBehavior {
        void quack();
       }

       //变化的 quack() 行为实现类之一
       public class Quack implements QuackBehavior {
        public void quack() {
            System.out.println("Quack");
        }
       }

       //变化的 quack() 行为实现类之二
       public class Squeak implements QuackBehavior {
        public void quack() {
            System.out.println("Squeak.");
        }
       }

       //变化的 quack() 行为实现类之三
       public class MuteQuack implements QuackBehavior {
        public void quack() {
            System.out.println("<< Slience >>");
        }
       }

       通过以上设计,fly()行为以及quack()行为已经和Duck.java没有什么关系,可以充分得到复用。而且我们很容易增加新的行为, 既不影响现有的行为,也不影响Duck.java。但是,大家可能有个疑问,就是在面向对象中行为不是体现为方法吗?为什么现在被定义形成类(例如Squeak.java)?在OO中,类代表的"东西"一般是既有状态(实例变量)又有方法。只是在本例中碰巧"东西"是个行为。既使是行为,也有属性及方法,例如飞行行为,也需要一些属性记录飞行的状态,如飞行高度、速度等。

    2) 整合变化的内容和不变的内容

       Duck.java将 fly()以及quack()的行为委拖给行为类处理。

       不变的内容:

       public abstract class Duck {
            //将行为类声明为接口类型,降低对行为实现类型的依赖
        FlyBehavior flyBehavior;
        QuackBehavior quackBehavior;

        public void performFly() {
            //不自行处理fly()行为,而是委拖给引用flyBehavior所指向的行为对象
            flyBehavior.fly();
        }

        public void performQuack() {
            quackBehavior.quack();
        }

        public void swim() {
            System.out.println("All ducks float, even decoys.");        
        }
        
        public abstract void display();
       }

       Duck.java不关心如何进行 fly()以及quack(), 这些细节交由具体的行为类完成。
       
       public class MallardDuck extends Duck{
        public MallardDuck() {
            flyBehavior=new FlyWithWings();
            quackBehavior=new Quack();        
        }
        
        public void display() {
            System.out.println("Green head.");
        }
       }

           测试类:

       public class DuckTest {
        public static void main(String[] args) {
            Duck duck=new MallardDuck();
            duck.performFly();
            duck.performQuack();        
        }
       }

       在Duck.java子类型MallardDuck.java的构造方法中,直接实例化行为类型,在编译的时侯便指定具体行为类型。当然,我们可以:
       
       1) 我们可以通过工厂模式或其它模式进一步解藕(可参考后续模式讲解);
       2) 或做到在运行时动态地改变行为。

    3) 动态设定行为

       在父类Duck.java中增加设定行为类型的setter方法,接受行为类型对象的参数传入。为了降藕,行为参数被声明为接口类型。这样,既便在运行时,也可以通过调用这二个方法以改变行为。

       public abstract class Duck {
        //在刚才Duck.java中加入以下二个方法。
        public void setFlyBehavior(FlyBehavior flyBehavior) {
            this.flyBehavior=flyBehavior;
        }
        
        public void setQuackBehavior(QuackBehavior quackBehavior) {
            this.quackBehavior=quackBehavior;
        }

        //其它方法同,省略...
       }

           测试类:

       public class DuckTest {
        public static void main(String[] args) {
            Duck duck=new MallardDuck();
            duck.performFly();
            duck.performQuack();
            duck.setFlyBehavior(new FlyNoWay());
            duck.performFly();
        }
       }

       如果,我们要加上火箭助力的飞行行为,只需再新建FlyBehavior.java接口的实现类型。而子类型可通过调用setQuackBehavior(...)方法动态改变。至此,在Duck.java增加新的行为给我们代码所带来的困绕已不复存在。

    该是总结的时侯了,让我们从代码的水中浮出来,做一只在水面上自由游动的鸭子吧:

    3.  解决方案

        MallardDuck 继承  Duck抽象类;          -> 不变的内容
        FlyWithWings 实现 FlyBehavior接口;     -> 变化的内容,行为或算法
    在Duck.java提供setter方法以装配关系;    -> 动态设定行为

    以上就是策略模式的实现三步曲。接下来,让我们透过步骤看本质:
    
    1) 初始,我们通过继承实现行为的重用,导致了代码的维护问题。          -> 继承, is a
    2) 接着,我们将行为剥离成单独的类型并声明为不变内容的实例变量并通过  -> 组合, has a
       setter方法以装配关系;

        继承,可以实现静态代码的复用;组合,可以实现代码的弹性维护;使用组合代替继承,可以使代码更好地适应软件开发完后的需求变化。

    策略模式的本质:少用继承,多用组合

分享到:
评论

相关推荐

    超微 X11DPG-OT-CPU主板用户手册

    超微 X11DPG-OT-CPU主板用户手册

    超微 X11DPG-QT主板用户手册

    ### 超微 X11DPG-QT 主板用户手册关键知识点解析 #### 一、产品概述 **超微 X11DPG-QT** 是一款由 **Super Micro Computer, Inc.**(以下简称“Supermicro”)设计并制造的专业级服务器主板。此主板面向对性能、...

    明华汉澳DP-R123-U-SB2(X3-HZ) RD及DP串口系列读写器演示程序及开发包.zip

    适用于RD-EB、RD-ET、RD-EZ、RD-EB-MX、RD-ET-MX、RD-EZ-MX、KRD-EB、KRD-ET、KRD-EZ、KRD-EB-MX、KRD-ET-MX、KRD-EZ-MX 、SRD-R100、Q3-R100、Q3-R101、Q3-R102、DP-R103、DP-R 113、DP-R123、DP-R133、DP-R143、...

    dp-8016p-pk 32位驱动

    《松下DP-8016P/PK 32位驱动详解及安装指南》 在信息技术领域,打印机是日常办公不可或缺的设备之一。松下DP-8016P和DP-8020P作为高效能的商务打印机,为用户提供了高质量的文档输出服务。然而,为了让这些设备正常...

    固件-CPU 1516F-3 PN DP-6ES7516-3FN02-0AB0-V2.9.7.zip

    压缩包"固件-CPU 1516F-3 PN DP-6ES7516-3FN02-0AB0-V2.9.7.zip"包含了两个文件:"S7_JOB.S7S"和"FWUPDATE.S7S"。这些文件是西门子S7系列PLC固件更新所必需的。S7S文件格式是西门子专有的,用于存储程序、数据和配置...

    重庆德图电气DP3-PAA DP3-PAV DP3-PDA DP3-PDV系列电压电流报警表.pdf

    DP3-PAA DP3-PDA DP3-PDV DP3-PAV ◎ 上下限报警继电器输出 ◎ 红色高亮数码管14.2mmH ◎ 具有报警回差设定功能 ◎ 显示范围±3999(采用3-3/4位A/D芯片) ◎ 工作电源AC 110/220V 50/60Hz

    固件-CPU 1516-3 PN DP-6ES7516-3AN02-0AB0-V2.9.7.zip

    固件更新包“固件-CPU 1516-3 PN DP-6ES7516-3AN02-0AB0-V2.9.7.zip”内含的升级文件,如“FWUPDATE.S7S”和“S7_JOB.S7S”,为自动化系统提供了更新固件的可能性。这份固件版本V2.9.7可能包含了对硬件性能的优化、...

    重庆德图电气欧姆表DP4-PR10KDP4-PR1K/DP4-PR600.gif

    DP4-PR1K/DP4-PR600/DP4-PR10K 上下限报警继电输出, 具有报警设定数据功能, 可带RS-485/RS232计算通讯, 光电隔离变送器输出4-20MA

    STM32cubeMX--STM32F427--dp83848---freeRTOS--LWIP点灯实验

    在配置dp83848时,需要设置正确的接口模式、速度和双工状态,并确保两者之间的MDI/MDIX自动交叉功能正常工作。 freeRTOS是一个实时操作系统,用于资源有限的嵌入式系统。在这个实验中,它将被用作任务调度器,允许...

    天际航图像快速建模系统DP-Modeler2.3

    《天际航图像快速建模系统DP-Modeler2.3》是一款专为3D建模和视觉效果设计的专业软件,结合了先进的图像处理技术和高效的工作流程,旨在为用户提供便捷、精准的三维模型构建能力。DP-Modeler是天际航公司的明星产品...

    DP-302打印服务器1.02版

    DP-302的USB接口打印服务器为用户提供在局域网内分享他们的打印机的服务,并对于那些DP-302兼容的打印机产品提供在线打印服务。DP-302打印服务器允许多个用户直接从电脑上发送文件传输到打印机上进行打印工作。在...

    欧辰 RT133-1PL02-DP产品规格书.zip

    《欧辰 RT133-1PL02-DP产品规格书》是针对欧辰公司的一款工业级设备的详细技术文档,主要包含了该产品的设计特点、功能参数、性能指标、安装指南以及使用注意事项等关键信息。这篇规格书的目的是帮助用户、工程师和...

    DP-DP-Koppler_Rel3_www.44dpdp.com_www.44dpdp.con_EC1-DEB-DPM_GSD

    标题"DP-DP-Koppler_Rel3_www.44dpdp.com_www.44dpdp.con_EC1-DEB-DPM_GSD"中提到的"DP-DP-Koppler"可能指的是一个特定的软件或系统开发项目,其中"DP"可能是项目代号或者代表“Data Processing”的缩写,而"Koppler...

    S7400H与S7300通过DP Y-LINK通讯例子.zip西门子PLC编程实例程序源码下载

    S7400H与S7300通过DP Y-LINK通讯例子.zip西门子PLC编程实例程序源码下载S7400H与S7300通过DP Y-LINK通讯例子.zip西门子PLC编程实例程序源码下载S7400H与S7300通过DP Y-LINK通讯例子.zip西门子PLC编程实例程序源码...

    DP-301U网络打印服务器驱动

    《DP-301U网络打印服务器驱动:深入解析与应用指南》 在现代办公环境中,网络打印服务器扮演着至关重要的角色,它使得多台计算机能够通过网络共享一台打印机,大大提高了工作效率。D-LINK公司的DP-301U网络打印...

    Android dimens sw 屏幕适配文件

    values-sw300dp values-sw310dp values-sw320dp values-sw330dp values-sw340dp values-sw350dp values-sw360dp values-sw370dp values-sw380dp values-sw390dp values-sw400dp values-sw410dp values-sw420dp ...

    D-Link DP-302 打印服务器驱动

    【D-Link DP-302 打印服务器驱动】是一款专为D-Link公司的DP-302打印服务器设计的驱动程序。该驱动程序在IT领域中扮演着至关重要的角色,因为它允许计算机与DP-302打印服务器之间进行有效的通信,确保打印机能够正确...

    D-Link DP-302 V1.03 打印服务器固件

    D-Link DP-302 打印服务器固件,最新V1.03版本。也是最终版本,支持更多打印机。

    D-P模型参数与M-C模型参数的转换关系.pdf

    在岩土工程领域,D-P模型(德鲁克-普拉格模型)与M-C模型(莫尔-库仑模型)都是用于描述土体或岩石材料力学行为的重要模型。D-P模型是一个基于应力不变量的塑性模型,它包含了流动法则和硬化法则,适用于复杂的...

    PyPI 官网下载 | dpac-datetimepicker-3.0.tar.gz

    **PyPI 官网下载 | dpac-datetimepicker-3.0.tar.gz** 在Python的世界里,`PyPI`(Python Package Index)是官方的第三方软件包仓库,它为开发者和使用者提供了大量的Python库和模块。`dpac-datetimepicker-3.0.tar...

Global site tag (gtag.js) - Google Analytics