对复合(协作)算法/策略的封装方法——装饰模式总结

前言

装饰模式顾名思义就是在不改变原对象的前提下,将新功能优雅的附加(装饰)到该对象上,可以实现对复合算法(策略)的优雅封装、对需要协作的算法(策略)进行有机组合。

装饰模式和策略模式用法类似,但是也有明显区别——策略模式运行时只能运行一个算法,且其各个算法(算法族)之间必须相互独立,不能有联系,装饰模式没有这些约束。

装饰模式和策略模式一样,也是对继承的一种替代方案——使用对象组合的方式,做到运行时装饰对象,从而优雅的替代死板的继承。

另外,装饰模式是对象的结构型模式。

再议继承的缺陷

在策略模式:继承、组合和接口用法——策略模式复习总结,里有一个鸭子的案例,说到了继承的种种局限性,这可以用策略模式(组合+接口)改进。同样,我们也可以用装饰模式。

看一个新的例子——做手抓饼的例子,现在要实现一个做手抓饼的点餐程序,一般人都会加一个鸡蛋,或者烤肠,辣条等等,加不同的料,价格肯定也不一样,下面看代码:

public class Cake { // 代表饼
    protected String getInfo() {
        return "这是一个白饼";
    }

    protected double getCost() {
        return 2.0D; // 卖两元
    }
}

下面是加了鸡蛋的饼,和加鸡蛋和烤肠的饼

public class CakeWithEgg extends Cake {
    @Override
    public String getInfo() {
        return super.getInfo() + "加1鸡蛋";
    }

    @Override
    public double getCost() {
        return super.getCost() + 2;
    }
}
//////////////////////////////
public class CakeWithEggSausage extends CakeWithEgg {
    @Override
    public String getInfo() {
        return super.getInfo() + "加1个烤肠";
    }

    @Override
    public double getCost() {
        return super.getCost() + 2;
    }
}

客户端调用如下:

public class Main {
    public static void main(String[] args) {
        Cake cake = new Cake();
        System.out.println(cake.getInfo() + ",价格:" + cake.getCost());

        Cake cake1 = new CakeWithEgg();
        System.out.println(cake1.getInfo() + ",价格:" + cake1.getCost());

        Cake cake2 = new CakeWithEggSausage();
        System.out.println(cake2.getInfo() + ",价格:" + cake2.getCost());
    }
}

很简单,很快就实现好了,UML 类图如下:

看似很正常,很完美,但是此时,有新的需求了——加 N 个鸡蛋或者烤肠等。。。此时会发现,该程序已经无法正常的扩展了,如果还是依样画葫芦的添加新类,通过继承。。。那么必将导致系统的类“爆炸”——成为垃圾代码的典范案例。。。

小结

利用继承扩展类的行为,在代码编译期间就决定了行为是什么了,且所有它的子类都要无脑的接收父类的内容。。。这在某些场景下是不太合理的(联系策略模式里鸭子的例子),而使用组合就可以规避这样的问题。

组合可以动态的组合对象,在不改变现有类的基础上,给这个类添加新的行为,既不会对旧代码引入 bug,也能增加新功能,这就是 OCP(open close principle)——开闭原则的体现。

设计原则:开闭原则(OCP)

额外提一句:软件设计在考虑基本原则之前,也要综合考虑 deadline,项目规模,等因素,一句话——屈于现实,有时候允许代码写的不符合设计原则,但是心中要有一条‘准线‘,而不是一直听之任之,甚至随波逐流,无所谓的态度。。。

OCP定义:一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。

核心思想:面向抽象编程——用抽象构建框架,用实现扩展细节,其实我觉得核心就是一句话,所有的开源框架和设计模式的精髓——加间接层,只要有变化,或者感觉别扭,就可以抽象不变的代码,然后加间接层,扩展功能。

OCP 的目的在于面对需求的改变而保持系统的相对稳定,从而使得系统可以很容易的从一个版本升级到另一个版本。而实际生产环境里,绝对封闭的系统是不存在的。无论模块怎么封闭,到最后总还是有一些无法封闭的变化。

设计的基本思路

既然不能做到完全封闭,那就应该只封闭变化的部分:

1、做粗粒度的隔离:把变化的模块和稳定的模块先区分开

2、做细粒度的隔离:在变化的模块里,尝试对逻辑段落进行封装,把那些实在无法封闭的部分抽象出来,即进行细粒度的隔离——比如使用抽象类,接口等

3、要允许扩展,当系统的变化来临时,要及时的做出反应。而不是一味的就知道修改已有的业务代码

矛盾

实际上,变化来的越早,抽象就越容易,代码的维护也就越容易

但是,当项目接近于完成时,才来的一些需求,则会使抽象变得很困难。这个困难并不是抽象本身的困难,抽象本身不难,难在系统的架构已经完成,修改牵扯的方面太多而使得抽象工作变得很困难。

这是一个矛盾点

拆分原则

总的来说:

1、不能害怕改变——当新需求到来时,首先要做的不是上来就修改已有的业务代码,而是尽可能的将变化抽象出来进行隔离,然后再进行扩展。

2、面对需求的变化,对程序的修改应该是尽可能通过添加代码来实现,而不是通过修改已有代码来实现。当然,现实中,各种情况一综合,大家也都知道,呵呵了。。。

组合和委托的运用——装饰模式的实现

下面采取一些新的设计思路;以饼Cake为主体,我们认为它是被装饰的实体,现在要实现,在运行时动态的用鸡蛋,烤肠,辣条等装饰者,去装饰饼 cake。全程只 new 一个饼即可,如果客户想要一个加鸡蛋的饼,就用鸡蛋去装饰这个饼,如果想要 N 个鸡蛋,就用 N 个鸡蛋去装饰这个饼,烤肠等同理。最后再调用 cost,info 等方法,将饼的信息和价钱算出来。

其实,装饰理解为包装更合适。联系 Java 的 I/O API,其实就是这么用的,各种 buffer 的流去包装字节,字符流。

如此一来,我们可以把看似不同的东西,饼和鸡蛋不同,看做同一种东西来使用,如何实现呢?自然就用到了组合+委托的设计思想。

既然饼和鸡蛋两个不相干的东西能被当做一个东西来用,它们肯定都要继承同一个类型:手抓饼是具体的被装饰的类——Cake,我们可以给它设计一个抽象的父类,只代表饼——DefaultCake,而鸡蛋是一个具体的装饰类——EggDecorator,它要有一个抽象的(或者非抽象)父类——装饰者类 Decorator。同时我们让装饰者类 Decorator 也继承 DefaultCake 即可。

下面看代码:

public abstract class DefaultCake { // 代表饼——抽象的饼类,也就是抽象的被装饰类
    // 注意,该类也可以用接口实现,且这个角色不是必须的,也可以直接就是具体的被装饰类
    protected abstract String getInfo();
    protected abstract double getCost();
}
//////////////////////////////////////////
public class Cake2 extends DefaultCake { // 具体的被装饰类
    @Override
    protected String getInfo() {
        return "这是一个白饼";
    }

    @Override
    protected double getCost() {
        return 2.0D;
    }
}

如上能解决两个类的类型一致问题,下面让两个类关联起来——使用组合+委托,我们目的是让装饰者——EggDecorator去装饰被装饰的类——Cake,可以把抽象的被装饰类DefaultCake组合到装饰者Decorator类里,来关联装饰类和被装饰类,然后联系策略模式,通过构造器或者 setter 方法,把抽象的被装饰类DefaultCake注入进装饰类Decorator——这就叫委托,即装饰类Decorator将新的功能,委托给被装饰类DefaultCake去实现。

public class Decorator extends DefaultCake { // 装饰类的父类。这个类也可以设计为抽象的,前提装饰类要额外做一些事情的时候
    private DefaultCake defaultCake; // 通过组合抽象的被装饰父类,来关联装饰类和具体的被装饰类

    public Decorator(DefaultCake defaultCake) { // 通过构造器注入
        // 这样可以把被装饰类DefaultCake传入装饰类Decorator——这也叫委托,即装饰类Decorator将装饰的功能,委托给被装饰类DefaultCake去实现
        this.defaultCake = defaultCake;
    }

    @Override
    protected String getInfo() {
        return this.defaultCake.getInfo();
    }

    @Override
    protected double getCost() {
        return this.defaultCake.getCost();
    }
}
//////////////////////////////
public class EggDecorator extends Decorator { // 具体的装饰类——鸡蛋
    public EggDecorator(DefaultCake defaultCake) { // 这里必须要实现有参构造器,因为父类写了有参构造器
        super(defaultCake);
    }

    @Override
    protected String getInfo() {
        return super.getInfo() + " 加1个鸡蛋";
    }

    @Override
    protected double getCost() {
        return super.getCost() + 2;
    }
}
/////////////////////////////
public class SausageDcorator extends Decorator { // 具体的装饰类——烤肠
    public SausageDcorator(DefaultCake defaultCake) {
        super(defaultCake);
    }

    @Override
    protected String getInfo() {
        return super.getInfo() + " 加1个烤肠";
    }

    @Override
    protected double getCost() {
        return super.getCost() + 2;
    }
}

客户端调用:

public class Main2 {
    public static void main(String[] args) {
        DefaultCake cake = new Cake2();
        cake = new EggDecorator(cake); // 用鸡蛋这个具体的装饰者去包装被装饰者——饼cake,看起来很像 I/O API
        cake = new EggDecorator(cake); // 再加一个鸡蛋
        cake = new EggDecorator(cake); // 理论上可以加 N 个,但是我们只用一个类就能解决这个需求
        cake = new SausageDcorator(cake); // 再加一个烤肠
        System.out.println(cake.getInfo() + ",价格:" + cake.getCost()); // 这是一个白饼 加1个鸡蛋 加1个鸡蛋 加1个鸡蛋 加1个烤肠,价格:10.0
    }
}

其实也不难,下面是类图:

装饰模式的特点

1、装饰者和被装饰者要有相同的超类型

类型的一致性,通过继承共同父类解决

2、装饰者要把装饰的责任委托给被装饰者,这样处理,装饰者能在委托之前或者之后,加上自己的专属行为,以达到特殊目的

3、通过组合+委托,实现动态的,运行时,不限量的为对象增加新功能

4、保持了接口的透明性——OCP 原则

因为装饰者和被装饰者类型具有一致性,所以,它们的 API 也具有一致性,也就实现了接口的透明性,即不会有继承的弊端——覆盖掉父类方法,具体的被装饰类不论被装饰多少次,其父类(或者接口)的 API 都不会被修改,同时这也体现了递归的思想。

再次强调:装饰模式使用继承,是为了实现类型的一致性,而不是为了扩展类的功能。

装饰模式的适用场景

1、适合替代继承,给类添加新的职责

2、比策略模式更灵活——可以在运行时动态的给一个对象添加新功能,也可以撤销已经添加的新功能,可以同时封装多个算法(策略)去完成一件事。

装饰模式的优点

其实没必要说太多了,前面包括策略模式已经说到了很多继承的缺陷,也说到了策略模式的一个缺点——算法族的算法必须独立和平等,装饰模式可以弥补这些缺陷

1、装饰模式比继承灵活,可以在不改变对象的前提下,扩展对象。如果只使用继承扩展类,那么当新功能较多时,会导致类的膨胀和复杂,且子类不一定都要具备父类的这些特性,联系策略模式里鸭子的例子。而且,继承对类的扩展是静态的,在编译时就要确定,而装饰模式是动态的扩展

2、说到算法族的平等和独立,这是策略模式的局限性,但是装饰模式就可以通过排列组合,实现算法之间的协作和组合,也是题目的意思

不叫缺点的“缺点”

增加了系统的类数量,增加了代码量,代码量多了,自然程序就显得复杂一些,但是我认为,瑕不掩瑜,无所谓,可以说是硬给扣的一些帽子,大胆的用吧,如果有人看不懂,只能说对方编程能力比较水,OO 能力比较差。

JDK 使用装饰模式的例子

I/O 包:源码没什么分析的必要了,很直观

还有对集合的线程安全的包装API,比如java.util 包里的 Collections.synchronizedMap()等;

原文地址:https://www.cnblogs.com/kubixuesheng/p/10344505.html

时间: 2024-10-04 02:25:19

对复合(协作)算法/策略的封装方法——装饰模式总结的相关文章

使用策略模式封装拦截器

通过if-else 来实现拦截器的封装 axios.interceptors.response.use((res) => { let {message, statusCode} = res.data.data // 退出登录状态码 let logoutCodes = new Set([435001, 435011, 436050]) if (statusCode === 1000) { // 更新全局token let {pragma} = res.headers if (pragma) win

算法策略的总结

策略是面向问题的,算法是面向实现的. 一.不同算法策略特点小结 1.贪心策略 贪心策略一方面是求解过程比较简单的算法,另一方面它又是对能适用问题的条件要求最严格(即适用范围很小)的算法. 贪心策略解决问题是按一定顺序,在只考虑当前局部信息的情况下,就做出一定的决策,最终得出问题的解. 即:通过局部最优决策能得到全局最优决策 2.递推策略 递推也是由当前问题的逐步解决从而得到整个问题的解,依赖于信息间本身的递推关系,每一步不需要决策参与到算法中,更多用于计算 3.递归策略 递归常常用于分治算法.动

混合模式(策略模式+工厂方法模式+门面模式)

使用这三种模式设计一个简单的计算器程序:计算器是用于计算数值之间进行数学计算后所获得的值.它包含基本的"加减"功能.以上对以上需求进行分析可以得出计算有两种策略(+与-). 计算策略的实现: /*抽象策略*/public interface Strategy{ /*定义了计算策略所拥有的算法*/ public int calculate(int a,int b);} /*加法策略的实现*/public class AddStrategy implements Strategy{ @Ov

hibernate5(8)操纵对象入门[3]操控对象封装方法

为什么要说是"封装方法"呢?因为它帮我们封装好了底层的增删改查操作,直接调用相应方法即可灵活地操作我们数据库数据.它们由Session接口提供,下面我们通过实例一一分析这些方法. 1.save方法 Session 的 save() 方法使一个临时对象转变为持久化对象 Session 的 save() 方法完成以下操作: 1. 把 News 对象加入到 Session 缓存中,使它进入持久化状态 2. 选用映射文件指定的标识符生成器,为持久化对象分配唯一的 OID.在 使用代理主键的情况

内存布局------c++程序设计基础、编程抽象与算法策略

图中给出了在一个典型c++程序中如何组织内存的框架.程序中的指令(在底层都是按位存储的)和全局变量往往被存储在静态去(static area),该区域位于地址编址号较小的接近机器地址空间的开始处.该区域所分配的内存量在程序运行期间不会发生改变. 内存中的最高地址区表示栈区(stack area).当你的程序每调用一个函数或者方法,计算机就会在这个内存区创建一个新的栈帧.当函数返回时,所创建的栈帧会被撤销,以为后续的函数调用所需的栈帧释放内存. 处于栈区和静态区之间的内存区域被称为堆区(heap

11算法策略之动态规划

动态规划 在动态规划算法策略中,体现在它的决策不是线性的而是全面考虑不同的情况分别进行决策, 并通过多阶段决策来最终解决问题.在各个阶段采取决策后, 会不断决策出新的数据,直到找到最优解.每次决策依赖于当前状态, 又随即引起状态的转移.一个决策序列就是在变化的状态中产生出来的,故有"动态"的含义.所以,这种多阶段决策最优化的解决问题的过程称为动态规划. [例1]数塔问题 有形如图4-11所示的一个数塔,从顶部出发,在每一结点可以选择向左走或是向右走,一直走到底层,要求找出一条路径,使路

八、MD5加密并封装,并调用封装方法

一.MD5加密 封装Md5 public class Md5 { //十六进制下数字到字符的映射数组 private static final char hexDigits[] = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' }; /**把inputString加密*/ public static String md5Str(String inputStr){ return en

五大常见算法策略——递归与分治策略

摘要:递归与分治策略是五大常见算法策略之一,分治策略的思想就是分而治之,即先将一个规模较大的大问题分解成若干个规模较小的小问题,再对这些小问题进行解决,得到的解,在将其组合起来得到最终的解.而分治与递归很多情况下都是一起结合使用的,能发挥出奇效(1+1>2),这篇文章我们将先从递归说起,再逐渐向分治过渡,主要讲解方式是通过9个例题来说明问题的,问题都是根据难度由简到难,由浅入深,对递归与分治能有个大概的了解雏形,当然最重要还是要做大量练习才能掌握. 1.递归 我们第一次接触递归一般都是在初学C语

分享几个Javascript 封装方法

基本封装方法 请看下面的例子: var Person = function(name,age){ this.name = name; this.age = age || "未填写"; this.hobbys = []; } Person.prototype = { sayName:function(){ console.log(this.name); }, sayAge:function(){ console.log(this.age); }, addHobby:function(ho