【从0开始学设计模式-10| 装饰模式】

概念

装饰模式(Decorator Pattern)可以在不改变一个对象本身功能的基础上给对象增加额外的新行为,在现实生活中,这种情况也到处存在。

例如一张照片,我们可以不改变照片本身,给它增加一个相框,使得它具有防潮的功能,而且用户可以根据需要给它增加不同类型的相框,甚至可以在一个小相框的外面再套一个大相框。

装饰模式是一种用于替代继承的技术 ,它通过一种无须定义子类的方式来给对象动态增加职责,使用对象之间的关联关系取代类之间的继承关系。

在装饰模式中引入了装饰类 ,在装饰类中既可以调用待装饰的原有类的方法,还可以增加新的方法,以扩充原有类的功能

定义

在面向对象编程中,装饰模式是一种设计模式,它允许将行为静态或动态地添加到单个对象中,而不会影响同一类中其他对象的行为。装饰器模式对于遵守单一责任原则通常是有用的,因为它允许将功能划分到具有独特关注区域的类之间

装饰模式也叫包装(Wrapper)模式,它可以在不创建更多子类的情况下让对象功能得以扩展。

装饰模式的结构

在装饰模式结构图中包含如下几个角色:

  • Component(抽象构件):它是具体构件和抽象装饰类的共同父类,声明了在具体构件中实现的业务方法,它的引入可以使客户端以一致的方式处理未被装饰的对象以及装饰之后的对象,实现客户端的透明操作。
  • Concrete Component(具体构件):它是抽象构件类的子类,用于定义具体的构件对象,实现了在抽象构件中声明的方法,装饰器可以给它增加额外的职责(方法)。
  • Decorator(抽象装饰类):它也是抽象构件类的子类,用于给具体构件增加职责 ,但是具体职责在其子类中实现。它维护一个指向抽象构件对象的引用,通过该引用可以调用装饰之前构件对象的方法,并通过其子类扩展该方法,以达到装饰的目的。
  • Concrete Decorator(具体装饰类):它是抽象装饰类的子类,负责向构件添加新的职责。每一个具体装饰类都定义了一些新的行为,它可以调用在抽象装饰类中定义的方法,并可以增加新的方法用以扩充对象的行为。

装饰模式的实现(透明装饰模式)

类图设计

这个例子是这样的

  • Troll(巨魔):角色是Component,定义了巨魔的基本行为
  • SimpleTroll:角色是SimpleTroll,基础巨魔 。拥有初始的 hpattackdefense 等属性
  • DecoratorTroll:角色是Decorator,装饰器基类 。持有一个 Troll 引用
  • AddAttackTroll:角色是ConcreteDecorator,武器装饰器。额外逻辑:累计5次无法破防时,穿戴武器增加攻击力。
  • AddDefenseTroll:角色是ConcreteDecorator,防具装饰器。额外逻辑:血量低于 10% 时,穿戴防具增加防御力。

通常情况下,巨魔是徒手战斗的,在某些情况下我们需要给它穿戴上武器或防具时候,不可能把巨魔杀了重新造一个,所以可以引入装饰器模式。

代码实现

  • Troll:巨魔接口

    java 复制代码
    public interface Troll {
      int attack(Troll target);
      void hit(int hitVal);
      int getAttackPower();
      void setAttackPower(int ap);
      int getDefensePower();
      void setDefensePower(int dp);
      int getHp();
    }
  • Simple:基础巨魔

    java 复制代码
    public class SimpleTroll implements Troll {
      private int hp;
      private int attack;
      private int defense;
      private String name;
    
      public SimpleTroll(int hp, int attack, int defense, String name) {
        this.hp = hp;
        this.attack = attack;
        this.defense = defense;
        this.name = name;
      }
    
      @Override
      public int attack(Troll target) {
        //计算伤害
        int hitVal = this.getAttackPower() - target.getDefensePower();
        //破防失败强制伤害为1
        if (hitVal <= 0) {
          hitVal = 1;
        }
        //目标受到伤害
        target.hit(hitVal);
        //返回本次伤害值
        return hitVal;
      }
    
      @Override
      public void hit(int hitVal) {
        //扣除生命值
        this.hp = this.hp - hitVal;
        //死亡判断
        if (this.hp <= 0) {
          System.out.println(name + ",你已被击杀!");
        } else {
          System.out.println(name + ",你受到" + hitVal + "点伤害,剩余血量" + this.hp);
        }
      }
    
      @Override
      public int getAttackPower() {
        return this.attack;
      }
    
      @Override
      public void setAttackPower(int ap) {
        this.attack = ap;
      }
    
      @Override
      public int getDefensePower() {
        return this.defense;
      }
    
      @Override
      public void setDefensePower(int dp) {
        this.defense = dp;
      }
    
      @Override
      public int getHp() {
        return this.hp;
      }
    }
  • 装饰器基类

    java 复制代码
    public class DecoratorTroll implements Troll {
      private Troll troll;//拥有抽象构件对象的引用
    
      public DecoratorTroll(Troll troll) {
        this.troll = troll;
      }
    
      @Override
      public int attack(Troll target) {
        return troll.attack(target);
      }
    
      @Override
      public void hit(int hitVal) {
        troll.hit(hitVal);
      }
    
      @Override
      public int getAttackPower() {
        return troll.getAttackPower();
      }
    
      @Override
      public int getDefensePower() {
        return troll.getDefensePower();
      }
    
      @Override
      public int getHp() {
        return troll.getHp();
      }
    
      @Override
      public void setAttackPower(int ap) {
        troll.setAttackPower(ap);
      }
    
      @Override
      public void setDefensePower(int dp) {
        troll.setDefensePower(dp);
      }
    }
  • 具体装饰类

    java 复制代码
    public class AddAttackTroll extends DecoratorTroll {
      private int notDefense;
      private boolean isAddAttack;
    
      public AddAttackTroll(Troll troll) {
        super(troll);
      }
    
      @Override
      public int attack(Troll target) {
        //调用一次攻击
        int hitVal = super.attack(target);
        //如果伤害值为1表示没有破防
        if (hitVal == 1 && !isAddAttack) {
          //累计未破防次数
          notDefense++;
          //未破防次数超过5次,加攻击力
          if(notDefense > 5){
            super.setAttackPower(super.getAttackPower() + 100);
            System.out.println("给我一根狼牙棒!!!");
            isAddAttack = true;
          }
        }
        return hitVal;
      }
    }
    java 复制代码
    public class AddDefenseTroll extends DecoratorTroll {
      private boolean isAddDefense;
      private int percentHp;
    
      public AddDefenseTroll(Troll troll) {
        super(troll);
        percentHp = (int) (getHp() * 0.1);
      }
    
      @Override
      public void hit(int hitVal) {
        //执行父类的伤害
        super.hit(hitVal);
        //判断是否可以加防御力了
        if (!isAddDefense && getHp() < percentHp && getHp() > 0) {
          super.setDefensePower(super.getDefensePower() + 100);
          System.out.println("给我一个头盔!!!");
          isAddDefense = true;
        }
      }
    }
  • 客户端测试

    java 复制代码
    public class App {
      public static void main(String[] args) {
        // 模拟普通怪对战加攻击力的怪物
        Troll xm1 = new SimpleTroll(300, 20, 30, "小明");
        Troll xh = new SimpleTroll(300, 20, 10, "小红");
    
        // 给小红加装备
        Troll atXh = new AddAttackTroll(xh);
    
        do {
          xm1.attack(atXh);
          if (atXh.getHp() <= 0) {
            break;
          }
          atXh.attack(xm1);
          if (xm1.getHp() < 0) {
            break;
          }
        } while (true);
    
        System.out.println("==============================================");
    
        // 模拟普通怪对战加防御力的怪物
        Troll xm2 = new SimpleTroll(300, 40, 5, "小明");
        Troll xl = new SimpleTroll(300, 40, 5, "小亮");
    
        // 给小亮加装备
        Troll atXl = new AddDefenseTroll(xl);
    
        do {
          xm2.attack(atXl);
          if (atXl.getHp() <= 0) {
            break;
          }
          atXl.attack(xm2);
          if (xm2.getHp() <= 0) {
            break;
          }
        } while (true);
      }
    }

透明与半透明

透明装饰模式

透明装饰模式 中,要求客户端完全针对抽象编程 ,装饰模式的透明性要求客户端程序不应该将对象声明为具体构件类型或具体装饰类型,而应该全部声明为抽象构件类型。对于客户端而言,具体构件对象和具体装饰对象没有任何区别。也就是应该使用如下代码:

java 复制代码
Component c1 = new ConcreteComponent();
Component c2 = new ConcreteDecorator(c1);

而不应该使用如下代码

java 复制代码
ConcreteComponent c1 = new ConcreteComponent();
ConcreteDecorator c2 = new ConcreteDecorator(c1);

透明装饰模式可以让客户端透明地使用装饰之前的对象和装饰之后的对象 ,无须关心它们的区别,此外,还可以对一个已装饰过的对象进行多次装饰,得到更为复杂、功能更为强大的对象。

上面巨魔的例子就是使用的透明装饰模式!

半透明装饰模式

透明装饰模式的设计难度较大,而且有时我们需要单独调用新增的业务方法

为了能够调用到新增方法,我们不得不用具体装饰类型来定义装饰之后的对象 ,而具体构件类型还是可以使用抽象构件类型来定义,这种装饰模式即为半透明装饰模式。

也就是说:

  • 对于客户端而言,具体构件类型无须关心,是透明的;

  • 但是具体装饰类型必须指定,这是不透明的。

解释什么是透明跟不透明:

  • 定义: 当你把对象看作是"抽象接口"时,它是透明的。
  • 定义: 当你必须把对象看作"具体实现类"时,它就是不透明的。
java 复制代码
// 对于客户端来说,此时 c1 是透明的,只知道它是一个 Component
Component c1 = new ConcreteComponent();
//这里的 c2 是不透明的,因为客户端明确知道它是一个 ConcreteDecorator。
ConcreteDecorator c2 = new ConcreteDecorator(c1);
c2.operation();
c2.addFunc();

半透明装饰模式可以给系统带来更多的灵活性设计相对简单,使用起来也非常方便;

但是其最大的缺点在于不能实现对同一个对象的多次装饰 ,而且客户端需要有区别地对待装饰之前的对象和装饰之后的对象

半透明装饰模式例子

  • 抽象构件类、具体构件类

    java 复制代码
    // 抽象构件:手机接口
    public abstract class Phone {
      public abstract void incomingCall();
    }
    java 复制代码
    // 具体构件:普通手机
    public class SimplePhone extends Phone {
      @Override
      public void incomingCall() {
        System.out.println("响铃");
      }
    }
  • 装饰器基类

    java 复制代码
    // 装饰器基类
    public abstract class DecoratorPhone extends Phone {
      protected Phone phone; // 持有对 Phone 的引用
    
      public DecoratorPhone(Phone phone) {
        this.phone = phone;
      }
    
      @Override
      public void incomingCall() {
        phone.incomingCall();
      }
    }
  • 具体装饰类(未添加新功能)

    java 复制代码
    public class JarPhone extends DecoratorPhone {
      public JarPhone(Phone phone) {
        super(phone);
      }
    
      @Override
      public void incomingCall() {
        super.incomingCall();
        System.out.println("震动");
      }
    }
  • 具体装饰类(增加新功能)

    java 复制代码
    // 增加了"灯光闪烁"逻辑,以及"屏幕弹窗"新功能
    public class ComplexPhone extends DecoratorPhone {
      public ComplexPhone(Phone phone) {
        super(phone);
      }
    
      @Override
      public void incomingCall() {
        super.incomingCall();
        System.out.println("灯光闪烁");
      }
    
      // 装饰器新增的业务方法,接口 Phone 中并没有定义此方法
      public void showNotification() {
        System.out.println("屏幕弹窗:您有一个未接来电");
      }
    }
  • 客户端

    java 复制代码
    public class App {
      public static void main(String[] args) {
        //创建具体构件
        Phone simple = new SimplePhone();
    
        // 这一步是透明的,把 JarPhone 声明为 Phone,客户端只把它当普通手机看,因为它没提供新功能
        Phone jarPhone = new JarPhone(simple);
    
        // 这一步是【不透明】的:必须声明为具体类 ComplexPhone
        // 如果这里写成 Phone,下面的 showNotification 就没法用了
        ComplexPhone complexPhone = new ComplexPhone(jarPhone);
    
        // 统一调用接口方法
        complexPhone.incomingCall(); 
    
        // 调用 ComplexPhone 特有的功能,这里是不透明的地方
        complexPhone.showNotification(); 
      }
    }

装饰模式的适用环境

使用注意事项

  • 尽量保持装饰类的接口与被装饰类的接口相同 ,这样,对于客户端而言,无论是装饰之前的对象还是装饰之后的对象都可以一致对待。这也就是说,在可能的情况下,我们应该尽量使用透明装饰模式
  • 尽量保持具体构件类ConcreteComponent是一个"轻"类,也就是说不要把太多的行为放在具体构件类中,我们可以通过装饰类对其进行扩展。
  • 如果只有一个具体构件类 ,那么抽象装饰类可以作为该具体构件类的直接子类

主要优点

  • 对于扩展一个对象的功能 ,装饰模式比继承更加灵活性不会导致类的个数急剧增加
  • 可以通过一种动态的方式来扩展一个对象的功能,通过配置文件可以在运行时选择不同的具体装饰类,从而实现不同的行为。
  • 可以对一个对象进行多次装饰 ,通过使用不同的具体装饰类以及这些装饰类的排列组合,可以创造出很多不同行为的组合,得到功能更为强大的对象。
  • 具体构件类与具体装饰类可以独立变化 ,用户可以根据需要增加新的具体构件类和具体装饰类,原有类库代码无须改变,符合"开闭原则"

主要缺点

  • 使用装饰模式进行系统设计时将产生很多小对象 ,这些对象的区别在于它们之间相互连接的方式有所不同,而不是它们的类或者属性值有所不同,大量小对象的产生势必会占用更多的系统资源,在一定程序上影响程序的性能。
  • 装饰模式提供了一种比继承更加灵活机动的解决方案,但同时也意味着比继承更加易于出错,排错也很困难,对于多次装饰的对象,调试时寻找错误可能需要逐级排查,较为繁琐。

适用环境

  • 在不影响其他对象的情况下,以动态、透明的方式给单个对象添加职责

  • 不能采用继承的方式对系统进行扩展或者采用继承不利于系统扩展和维护时可以使用装饰模式。

  • 不能采用继承的情况主要有两类:

    • 第一类是系统中存在大量独立的扩展,为支持每一种扩展或者扩展之间的组合将产生大量的子类,使得子类数目呈爆炸性增长;
    • 第二类是因为类已定义为不能被继承(如final类)。

如果这篇文章对你有帮助,欢迎点赞、评论、关注、收藏。你们的支持是我前进的动力!

相关推荐
菜鸟学Python2 小时前
Python生态在悄悄改变:FastAPI全面反超,Django和Flask还行吗?
开发语言·python·django·flask·fastapi
朝新_2 小时前
【Spring AI 】图像与语音模型实战
java·人工智能·spring
RH2312113 小时前
2026.4.16Linux 管道
java·linux·服务器
zmsofts3 小时前
java面试必问13:MyBatis 一级缓存、二级缓存:从原理到脏数据,一篇讲透
java·面试·mybatis
浪浪小洋3 小时前
c++ qt课设定制
开发语言·c++
charlie1145141913 小时前
嵌入式C++工程实践第16篇:第四次重构 —— LED模板,从通用GPIO到专用抽象
c语言·开发语言·c++·驱动开发·嵌入式硬件·重构
故事和你913 小时前
洛谷-数据结构1-4-图的基本应用1
开发语言·数据结构·算法·深度优先·动态规划·图论
程序猿编码4 小时前
给你的网络流量穿件“隐形衣“:手把手教你用对称加密打造透明安全隧道
linux·开发语言·网络·安全·linux内核
sg_knight4 小时前
设计模式实战:责任链模式(Chain of Responsibility)
python·设计模式·责任链模式