ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

装饰器模式与开闭原则

装饰器模式与开闭原则

📌 本系列将持续更新「设计模式」相关内容
🧩 从核心设计原则到设计模式,帮你真正用起来
🔔 觉得有帮助,欢迎点个关注
💬 一起交流学习,共同进步

前文

本章可以称为“为爱用继承的家伙打开设计之眼”。
我们即将再度探讨典型的继承滥用问题。你将学到如何在运行时使用对象组合的形式装饰你的类。为什么?一旦知道装饰的技巧,你将能给你的(或别人的)对象一个新的责任,而不需要对底层类的代码做任何改变。

本文的灵感与知识骨架主要来自神书《Head First 设计模式》中的装饰者模式章节。为了更易读,笔者用自己的“文件读取与后缀装饰”案例进行了重构,并加入了个人理解。水平有限,如有错漏欢迎指正~
本文讲解逻辑:简单继承实现 → 发现问题 → 引入开闭原则 → 装饰器模式。

1.一个简单的文件读取程序

1.1 程序介绍

  • 假设公司要求我们开发一个文件读取的程序,该程序可以读取数字,字母还有特殊符号三个功能,基础类图如下:

DataReader

+read() : String

NumberReader

+read() : String

LetterReader

+read() : String

SpecialCharReader

+read() : String

1.2新的需求

  • 现在老板需要我们给读取到的数据都增加一个后缀.data。

2. 第一种实现方式,有误导

2.1使用继承来复用代码

  • 使用继承的复用功能,我们可以在父类的读取方法中实现后缀data功能,然后子类的方法中在自己的逻辑基础上调用父类的读取方法即可,代码如下:
abstractclassDataReader{publicStringread(){return".data";}}classNumberReaderextendsDataReader{@OverridepublicStringread(){return"12345"+super.read();}}classLetterReaderextendsDataReader{@OverridepublicStringread(){return"abcde"+super.read();}}classSpecialCharReaderextendsDataReader{@OverridepublicStringread(){return"!@#$%"+super.read();}}
  • 测试一下:
publicclassClient{publicstaticvoidmain(String[]args){DataReaderreader=newNumberReader();System.out.println(reader.read());// 12345.datareader=newLetterReader();System.out.println(reader.read());// abcde.datareader=newSpecialCharReader();System.out.println(reader.read());// !@#$%.data}}

2.2又有新的需求

  • 上面的代码运行得似乎很完美,但现在老板需要我们给除了数字之外的数据都添加上后缀 NotNumber。为了让两个子类复用同一段后缀逻辑,我们很自然地想到把它放进父类的 read() 里,代码如下:
abstractclassDataReader{publicStringread(){return".data"+"NotNumber";}}

此时继承复用的问题就暴露了:

1.为了复用,只能改父类。想让 LetterReader 和 SpecialCharReader 共享 NotNumber 逻辑,就得动 DataReader;若不想动父类,就只能在这两个子类里各编码 .data 和 NotNumber,重复且难维护——两条路都不好走。
2.数字被误伤。NumberReader 通过 super.read() 被动继承了父类的返回值,明明不在需求范围内,却被加上了 NotNumber。
3.继承是编译期绑定。.data、NotNumber 都写死在 read() 的返回值里,运行时既无法切换后缀,也无法决定"这个对象要不要加",更无法叠加多种后缀。

3.开闭原则

3.1为什么要开闭原则

  • 分析2.2的例子,我们可以发现,一个类的职责功能是会变化的,所以我们应该对外提供扩展的功能,同时不能修改原有的代码,例子2.2中,当增加扩展功能的时候,我们不得不修改原有父类代码,同时子类中的逻辑也会受到改变,并且数字类还出错了,这就是为什么需要对修改关闭~

3.2开闭原则定义

软件实体应当对扩展开放,但对修改关闭。

4.装饰器模式

4.1装饰器模式原理,定义和类图

  • 有个模式是比较符合开闭原则的,就是装饰器模式,我们来看看它的原理:

装饰器模式的本质是动态地给一个对象添加一些额外的职责。它通过组合的方式,把核心功能和附加功能一层层包裹起来,形成类似“洋葱”或“俄罗斯套娃”的结构。

  • 定义:

动态地给一个对象添加一些额外的职责。就增加功能而言,装饰者模式比生成子类更为灵活。

  • 类图:

4.2让我们的程序使用装饰器模式

  • 类图:

«abstract»

DataReader

+read() : String

NumberReader

+read() : String

LetterReader

+read() : String

SpecialCharReader

+read() : String

«abstract»

DataReaderDecorator

#DataReader reader

+DataReaderDecorator(DataReader reader)

+read() : String

DataSuffixDecorator

+DataSuffixDecorator(DataReader reader)

+read() : String

NotNumberDecorator

+NotNumberDecorator(DataReader reader)

+read() : String

  • 代码:
// 抽象构件:只负责定义读取行为,不再承载任何后缀逻辑abstractclassDataReader{publicabstractStringread();}// 具体构件:各自只关心自己的核心数据classNumberReaderextendsDataReader{@OverridepublicStringread(){return"12345";}}classLetterReaderextendsDataReader{@OverridepublicStringread(){return"abcde";}}classSpecialCharReaderextendsDataReader{@OverridepublicStringread(){return"!@#$%";}}// 抽象装饰者:持有被装饰对象,并保持与构件相同的接口abstractclassDataReaderDecoratorextendsDataReader{protectedDataReaderreader;publicDataReaderDecorator(DataReaderreader){this.reader=reader;}@OverridepublicStringread(){returnreader.read();}}// 具体装饰者:追加 .data 后缀classDataSuffixDecoratorextendsDataReaderDecorator{publicDataSuffixDecorator(DataReaderreader){super(reader);}@OverridepublicStringread(){returnreader.read()+".data";}}// 具体装饰者:追加 NotNumber 后缀classNotNumberDecoratorextendsDataReaderDecorator{publicNotNumberDecorator(DataReaderreader){super(reader);}@OverridepublicStringread(){returnreader.read()+"NotNumber";}}
  • 代码测试:
publicclassClient{publicstaticvoidmain(String[]args){// 1. 老板的需求一:所有数据都加 .dataDataReadernumber=newDataSuffixDecorator(newNumberReader());System.out.println(number.read());// 12345.data// 2. 老板的需求二:数字之外的再加 NotNumberDataReaderletter=newNotNumberDecorator(newDataSuffixDecorator(newLetterReader()));System.out.println(letter.read());// abcde.dataNotNumberDataReaderspecial=newNotNumberDecorator(newDataSuffixDecorator(newSpecialCharReader()));System.out.println(special.read());// !@#$%.dataNotNumber// 3. 数字不在 NotNumber 的需求范围内,就不加这层装饰,按需组合// (此处 number 已经是 12345.data,没有被 "误伤")// 4. 想调整顺序?装饰顺序由客户端自由决定DataReaderanother=newDataSuffixDecorator(newNotNumberDecorator(newLetterReader()));System.out.println(another.read());// abcdeNotNumber.data// 5. 想不加任何后缀也行,直接用具体构件DataReaderraw=newLetterReader();System.out.println(raw.read());// abcde// 6. 甚至可以把两个装饰者同时叠加到同一个对象上DataReadertwice=newDataSuffixDecorator(newDataSuffixDecorator(newNumberReader()));System.out.println(twice.read());// 12345.data.data}}
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进