浅谈设计模式 - 装璜器模式(五)

前言:

装璜器模式是是对类进行加强的一种典型设计模式,它容许对于一个现有类进行加强的操作,对于喜爱应用继承的搭档,这个模式十分贴切的展现的了对于继承的灵便用法。然而装璜器模式同样不是一个推崇应用的模式,因为他对于继承存在依赖性,从本文后续就能够理解到装璜类收缩的问题,所以在设计代码构造的时候,装璜器模式并不是第一思考

什么是装璜器模式?

装璜器模式:对现有类不改变构造的状况下为类增加新职责和性能的模式。

动静的扩大类的职责,装璜器模式是一种是比继承更加灵便的代码扩大模式。同时装璜类之间能够进行相互的嵌套

装璜器模式的结构图:

  • Component 装璜接口:装璜接口定义了装璜的顶层形象行为,个别定义被装璜者和装璜者的专用行为

    • ConrecteComponent 被装璜类:次要为被装璜类实现,和装璜类互相独立,领有独自的性能办法
    • Decorder 装璜器:定义了装璜的通用接口,蕴含装璜器的通用办法

      • ConrecteDecorderA 装璜器A:定义了装璜器的具体设计,能够蕴含本人的装璜办法
      • ConrecteDecorderB 装璜器B:定义了装璜器的具体设计,能够蕴含本人的装璜办法

装璜器模式的特点

  1. 装璜者和被装璜者都须要实现雷同的接口(必要条件)
  2. 装璜者个别须要继承一个抽象类,或者须要定义形象的办法和实现
  3. 装璜者能够在所委托被装璜者的行为之前或之后,加上本人的行为,以达到特定的目标。
  4. 任何父类呈现的中央都能够用子类进行替换,在活用继承的同时能够灵便的扩大。

什么时候应用装璜器模式

  • 须要大量的子类为某一个对象进行职责加强的时候,能够应用装璜器模式
  • 心愿应用继承对于类进行动静扩大的时候,能够思考应用装璜器模式

理论案例:

模仿场景:

咱们用一个奶茶的构造来模仿一个装璜器的设计场景,咱们通常在奶茶店点奶茶的时候,对于一杯奶茶,能够增加各种配料,这时候配料就是奶茶的装璜者,而奶茶就是典型的被装璜者,咱们应用配料去“装璜”奶茶,就能够失去各种口味的奶茶。同时能够计算出奶茶的价格

上面咱们来看一下针对模仿场景的案例和应用:

不应用设计模式:

不应用设计模式,咱们的第一思考就是简略的应用继承去设计装璜类,咱们通过各种子类组合来实现一杯杯不同口味的奶茶,从上面的结构图能够看到,将被装璜类定义为独立的类,同时不进行任何的继承而是作为独立的类应用。而调料也就是奶茶饮料的配料须要继承同一个抽象类,同时在外部实现本人的办法。

紧接着,咱们在装璜者的办法中引入被装璜者,能够通过外部组合被装璜者进行 模拟行为的同时进行加强,就像IO当中的Buffer

咱们依据下面的阐明画出这一种设计的大抵结构图:

看了下面的设计图稿之后,咱们来阐明一下具体的代码实现:

首先是奶茶实体类:在奶茶的实体类外面定义两个属性, 应用一个display()打印信息,奶茶的实体类示意被装璜类

/** * 奶茶实体类 * * @author zxd * @version 1.0 * @date 2021/2/7 22:21 */public class MilkTea {    private String name;    private double price;    public MilkTea(String name, double price) {        this.name = name;        this.price = price;    }    public String getName() {        return name;    }    public void setName(String name) {        this.name = name;    }    public double getPrice() {        return price;    }    public void setPrice(double price) {        this.price = price;    }    public void display() {        System.out.println("name = "+ name + " price = " +price);    }}

上面是柠檬汁的被装璜类,这个被装璜类也是独立的:

/** * 柠檬汁 * * @author zxd * @version 1.0 * @date 2021/2/7 22:53 */public class LeamonJuice {    private String name;    private double price;    public LeamonJuice(String name, double price) {        this.name = name;        this.price = price;    }    public String getName() {        return name;    }    public void setName(String name) {        this.name = name;    }    public double getPrice() {        return price;    }    public void setPrice(double price) {        this.price = price;    }    public void display() {        System.out.println("name = "+ name + " price = " +price);    }}

调料的父类:留神这是一个抽象类,定义了调料的根本办法。

/** * 调料父类 * * @author zxd * @version 1.0 * @date 2021/2/7 22:23 */public abstract class Codiment {    /**     * 为装璜类增加附加值     * @return     */    abstract void plusAdditionVal(MilkTea milkTea);    /**     * 详细信息     */    protected String description(){        return "无任何配料";    }}

调料的子类珍珠类,这里为父类进行装璜,增加父类的信息

/** * 配料:珍珠 * * @author zxd * @version 1.0 * @date 2021/2/7 22:27 */public class Pearl extends Codiment{    @Override    void plusAdditionVal(MilkTea milkTea) {        if(milkTea == null){            throw new RuntimeException("对不起,请先增加奶茶");        }        milkTea.setPrice(milkTea.getPrice() + 2);        milkTea.setName(milkTea.getName() + "," +description());    }    /**     * 详细信息     */    protected String description(){        return "珍珠";    }}

调料的子类椰果类,这里同样是为了父类进行装璜的办法:

/** * 配料:椰果 * * @author zxd * @version 1.0 * @date 2021/2/7 22:30 */public class Coconut extends Codiment{    @Override    void plusAdditionVal(MilkTea milkTea) {        if(milkTea == null){            throw new RuntimeException("对不起,请先增加奶茶");        }        milkTea.setPrice(milkTea.getPrice() + 1);        milkTea.setName(milkTea.getName() + "," +description());    }    @Override    protected String description() {        return "椰果";    }}

最初咱们应用一个单元测试:

/** * 单元测试 * * @author zxd * @version 1.0 * @date 2021/2/7 22:34 */public class Main {    public static void main(String[] args) {        MilkTea milkTea = new MilkTea("原味奶茶", 5);        Pearl pearl = new Pearl();        Coconut coconut = new Coconut();        pearl.plusAdditionVal(milkTea);        coconut.plusAdditionVal(milkTea);        milkTea.display();    }}/*打印后果:name = 原味奶茶,珍珠,椰果 price = 8.0*/

不应用设计模式的优缺点:

长处:

  • 增加一个装璜者非常简略,只须要继承形象父类接口,同时子类只须要通过办法传入被装璜者进行装璜。

毛病:

  • 咱们的调料父类如果减少形象办法所有的子类都须要改变,这是整个子类群体来说是毁灭性的,对于编写代码的程序员来说也是毁灭性的。
  • 能够看到装璜者曾经是一种面向实现编程的状态,如果咱们换一种被装璜者,须要增加更多的装璜类进行装璜。并且这些装璜者是互相独立并且不能复用的
从结构图的设计就能够看出这种设计不合乎面向接口编程的设计准则

总结不应用模式:

不应用设计模式看起来没有什么大问题,然而能够从构造能够看到形象父类以及子类的耦合过于重大,父类齐全不敢动abstract void plusAdditionVal(MilkTea milkTea)这个形象签名办法,并且如果需要减少一个其余的被装璜者,这些装璜奶茶的装璜者就齐全“傻眼”了,因为他们齐全不意识新的被装璜者,这导致程序要更多的子类来接收新的的被装璜者,这种设计构造将导致类子类有限收缩,没有止境。

应用设计模式:

从不应用设计模式能够看出,不应用设计模式最大的问题是在于调料的父类形象办法耦合过于重大,以及被装璜类和装璜者之间存在依赖磁铁。从结构图能够看进去被装璜类和装璜类并没有显著的关联,咱们之前曾经阐明了装璜模式更多的是对于一个被装璜类的加强,既然是加强,那么被装璜类和装璜类通常须要具备雷同的形象行为,这样才比拟合乎装璜模式的设计构造。

上面就下面的结构图进行改良,在 被装璜类装璜类之上,再减少一层接口,调料的父类不在治理专用接口,而是能够减少本人的办法。咱们改良一下结构图,只有略微改良一下,整个构造就能够变得非常好用:

为了不便展现代码和了解,这里只列出了奶茶类调料父类配料:珍珠,以及咱们最重要的专用接口进行介绍:

咱们从最顶层开始,最顶层在结构上定义了一个形象专用接口,提供装璜者以及被装璜者进行实现或者定义形象和扩大:

/** * 饮料的抽象类,定义饮料的通用接口 * * @author zxd * @version 1.0 * @date 2021/2/7 23:46 */public interface DrinkAbstract {    /**     * 装璜接口     */    void plusAdditionVal();    /**     * 计算售价     * @return     */    double coat();}

而后是奶茶类,咱们的奶茶类在上一个版本根底上,实现了一个新的接口,所以须要定义实现接口后的办法:

奶茶类:

/** * 奶茶实体类 * * @author zxd * @version 1.0 * @date 2021/2/7 22:21 */public class MilkTea implements DrinkAbstract{    private String name;    private double price;    public MilkTea(String name, double price) {        this.name = name;        this.price = price;    }    public String getName() {        return name;    }    public void setName(String name) {        this.name = name;    }    public double getPrice() {        return price;    }    public void setPrice(double price) {        this.price = price;    }    public void display() {        System.out.println("name = "+ name + " price = " +price);    }    // 减少    @Override    public void plusAdditionVal() {        System.out.println("name = "+ name  + " price = " + price);    }    // 减少    @Override    public double coat() {        return price;    }}

上面是调料的父类,调料的父类须要改变的内容不是很多,实质上就是把本人的形象办法提取到父接口。这个类能够是抽象类,也能够是配料接口的通用形象:

/** * 调料父类 * 这里须要实现饮料接口 * @author zxd * @version 1.0 * @date 2021/2/7 22:23 */public class Codiment implements DrinkAbstract{    /**     * 为装璜类增加附加值     * @return     */    public void plusAdditionVal(){        description();    }    @Override    public double coat() {        return 5.0f;    }    /**     * 详细信息     */    private String description(){        return "无任何配料";    }}

最初是配料的具体实现类配料-珍珠进行改变:

/** * 配料:珍珠 * * @author zxd * @version 1.0 * @date 2021/2/7 22:27 */public class Pearl extends Codiment implements DrinkAbstract{    private DrinkAbstract drinkAbstract;    public Pearl(DrinkAbstract drinkAbstract) {        this.drinkAbstract = drinkAbstract;    }    @Override    public void plusAdditionVal() {        // 如果是奶茶        if(drinkAbstract instanceof MilkTea){            MilkTea drinkAbstract = (MilkTea) this.drinkAbstract;            drinkAbstract.setName(drinkAbstract.getName() + " -- " + "珍珠");            drinkAbstract.setPrice(drinkAbstract.getPrice() + 55);            description();        }    }    @Override    public double coat() {        return 5;    }    /**     * 详细信息     */    private void description(){        drinkAbstract.plusAdditionVal();    }}

最初,咱们来看下单元测试的变动:

public class Main {    private static void run2(){        DrinkAbstract drinkAbstract = new MilkTea("原味奶茶", 5);        Pearl codiment = new Pearl(drinkAbstract);        codiment.plusAdditionVal();    }    public static void main(String[] args) {       run2();    }}/*控制台后果:name = 原味奶茶 -- 珍珠 price = 60.0*/

能够看到咱们应用装璜类对于被装璜类的属性进行了扭转的同时并没有扭转被装璜者的自身的行为,而是对于行为做了扩大。

应用装璜器设计模式的优缺点:

长处:

  1. 装璜类的专用类不再须要设置形象的办法,使得装璜实现子类也不在依赖形象父类的形象办法
  2. 既然装璜者和被装璜对象有雷同的超类型,所以在任何须要原始对象(被包装的)的场合,就能够用装璜过的对象代替它。
  3. 装璜类和被装璜类的扩大和实现都是解耦的,不须要相互关注实现细节,装璜子类能够单独实现办法
  4. 咱们解决了减少新的被装璜类之后导致装璜类大量收缩的问题,当初能够进行简略的利用。

毛病:

  1. 实质上还是继承构造,而且装璜类和被装璜类必须有雷同的顶级父类接口
  2. 装璜类在零碎越来越简单之后会呈现显著的收缩。

JAVA IO - 典型的装璜模式:

首先阐明JAVA IO类其实实质上并不是一个非常优良的设计(因为简单的装璜子类和API构造),这个问题能够查看《JAVA编程思维》作者对于JAVA IO简单难用的API以及继承构造进行过的一系列吐槽,而且JAVA IO通过前面版本的迭代改良。使得本来的办法更加复杂多变,然而不论JAVA IO设计的API如何不“便民”,这一块的设计仍然是十分值得学习和思考的,也是装璜模式最典型的应用。

上面为一张《Head First设计模式的一张图》阐明一下JAVA IO装璜设计的装璜器收缩问题:

  • 能够看到InputStream是一个抽象类。
  • JDK1.5当中,他扩大自接口java.io.Closeable,规定须要接入装璜的类须要实现本人的流敞开办法。
  • JDK1.7 中,在Closeable根底上减少了java.io.AutoClosable来实现流的主动敞开性能。

从下面的图标也能够看到装璜器的一些毛病:

  1. 装璜类之间的具备简单的继承构造
  2. 装璜者之间尽管能够相互嵌套,然而不肯定相互兼容
JAVA IO对于JAVA初学者来说非常不敌对,从其余语言能够看到汲取了这一点的教训,通常都把IO流这一块设计的越简略好用越好(尽量的让调用者不须要去思考IO流的细节问题)。而JAVA IO 显然设计的不是很亲民。

总结装璜器模式:

长处:

+ 装璜者和被装璜对象有雷同的接口。+ 能够用一个或多个装璜者包装一个被装璜对象或者被装璜对象。+ 既然装璜者和被装璜对象有雷同的超类型,所以在任何须要原始对象(被包装的)的场合,能够用装璜过的对象代替它。+ 装璜者能够在所委托被装璜者的行为之前或之后,加上本人的行为,以达到特定的目标。+ 装璜者能够有限的嵌套,因为他们实质上归属于同一个接口

毛病:

+ 装璜者很容易呈现大量的小类,这让理解代码的人不容易分明不同装璜的设计+ 一个依赖其余具体类型的接口导入装璜者可能会带来劫难。所以导入装璜者要十分小心谨慎,并且认真思考是否真的须要装璜者模式+ 装璜者相互嵌套可能会减少代码的复杂度,也减少扩大装璜者子类的复杂度,最终这个难题会变成调用者的难题

总结:

许多的设计模式书籍都正告过装璜器模式是一个须要审慎思考的设计模式,因为装璜模式很容易会造成装璜类的收缩,同时对于特定类型接入装璜类可能会有意想不到的劫难,同时在接入装璜类的时候,须要认真的理解专用接口和抽象类的实现,须要理解这一类装璜针对的行为,否则只是简略的继承装璜父类或者继承接口可能会有一些莫名其妙的问题。