从BufferedInputStream包住FileInputStream再到GZIPInputStream再包一层Java IO 里这种一层套一层的写法就是装饰器模式最经典的现场。我一直觉得这个模式是那种“你天天在用却不一定叫得出名字”的设计模式。这篇我就以数据流处理为主线把装饰器模式从头到尾拆开聊清楚它解决什么问题、怎么设计、怎么写以及实战里最容易踩的坑。1. 装饰器模式到底在解决什么问题1.1 从一个真实的数据流痛点说起先设想一个场景。你负责一个日志采集系统数据从各台机器上报经过一个处理管道最后写入存储。刚开始很简单一个方法搞定读到字节写出去。接着需求来了要加加密。你复制了一份代码在读写之间塞了一段加密逻辑。又过了一阵要加压缩。好再复制一份。然后要加缓冲、要加校验、要加过滤……你会发现维护成本开始失控每次新增一个功能就要把所有排列组合都写一遍我见过有人这么干写一个SuperStream类构造参数里塞了五个布尔值分别表示要不要加密、要不要压缩、要不要缓冲。类里面全是if (encrypt) {...} else {...}每次读一个字节都要过五六个开关。这个类很快就膨胀到上千行而且每加一个新特性所有老调用方都得跟着改构造参数。这种代码就是典型的“坏味道”它的本质问题是把多个维度的变化耦合在一个类里。装饰器模式就是专门来治这个病的。1.2 继承方案为什么越走越窄可能有人会说我用继承不也行吗EncryptedInputStream继承InputStreamCompressedInputStream再继承EncryptedInputStream不是也能实现叠加这个思路在功能只有两三个维度的时候勉强能跑但一旦维度多起来就彻底崩了。加密和压缩是正交的继承要求你为每一种组合创建一个类。三个功能就是7个类四个功能就是15个类五个功能就是31个类。这就是组合爆炸。而且子类和父类之间是强绑定的父类改了构造方法所有子类都要跟着动。继承还有一个更隐蔽的问题它无法在运行时动态改变行为。你拿到一个已经实例化的对象想让它“从非加密变成加密”继承做不到装饰器可以——你再包一层就行了。1.3 装饰器模式的核心解法组合优于继承装饰器模式的思路其实非常朴素不改变原有对象不通过继承扩展而是把新功能放进一个“包装对象”里通过一层层包裹来叠加能力。每个装饰器对象和它包裹的对象实现同一个接口对外看起来是同一个东西但内部每一层都多做了点事。还是拿数据流打比方。原始数据是裸的字节流穿上一件“加密外套”再套一件“压缩外套”再套一件“缓冲外套”。每件外套都实现“读写字节”这个统一接口你从最外层拿数据的时候数据会依次经过所有外套的处理。想脱掉哪件不引用就行了想换顺序换个嵌套顺序就行。这个模式之所以叫“装饰器”是因为它真的像给核心对象一层层加装饰。核心对象本身不需要知道自己被装饰了装饰器之间也互相不知道对方存在。这种低耦合特性让它成了处理管道、中间件、过滤器这类场景的首选方案。2. 数据流装饰器的整体设计思路2.1 五个角色先对齐再动手写代码之前先把模式里的角色理清楚。装饰器模式一共有四个标准角色在数据流场景里对应关系如下角色作用数据流场景中的映射抽象组件定义统一接口Stream接口声明read和write具体组件被装饰的原始对象FileStream、SocketStream等基础实现装饰器基类持有组件引用转发调用StreamDecorator抽象类具体装饰器在转发前后附加逻辑EncryptedStream、CompressedStream等这个结构的关键在于装饰器基类要持有原始组件的引用并且原样转发接口调用。这样装饰器才能像俄罗斯套娃一样无限嵌套。我在实际设计的时候还会加上一种隐含角色——工厂。装饰器多了以后组装逻辑容易散落在业务代码里这时候用一个工厂方法统一装配能省掉很多重复代码。2.2 接口设计是成败关键很多初学者觉得装饰器模式简单随手写了就上结果越用越别扭。十有八九是接口没设计好。接口要做到两点。第一功能内聚。一个接口只表达一个核心概念读写就是读写不要混入flush、close之外的一堆杂项。第二粒度适中。接口方法太少装饰器能做的事有限太多每个装饰器都要实现一堆无关方法。比如你给Stream接口加了seek、length这些方法那EncryptedDecorator就得跟着实现可加密流根本不支持随机访问被迫抛异常——这不合理。我当时设计数据流接口的时候最后只留了四个方法read、write、flush、close。够用也好实现。Buffer装饰器只需要在read和write里做手脚Encryption装饰器也只需要关注这两个方法。多余的逻辑比如统计流量、打印日志这些我干脆让装饰器实现一个额外的StatsProvider接口用的时候靠类型判断来拿而不是污染主接口。2.3 “透明性”是装饰器的灵魂装饰器模式强调一个概念叫透明性客户端面向抽象编程不感知自己用的是原始组件还是装饰后的组件。你可以把装饰器想象成外卖包装袋。袋子上印着商家logo你从袋子里拿出餐盒从餐盒里拿出食物。你只关心“吃到了东西”不关心包装有几层。装饰器也一样调用方只关心“拿到的是能读能写的流”不关心里面套了几层。这种透明性带来一个巨大好处调用方代码零改动。管道从单层变成五层业务代码只需要在装配处多包几个对象读写逻辑一个字不用改。我在实际项目里深有体会——有一次要在全链路加一层流量审计改动量比我预估的小了一个数量级就是因为装饰器保持了透明性所有上游代码无感知。透明性也有代价后面在第四节我会专门讲它带来的两个坑。3. 实战给数据流加“特技”下面直接上代码。我写了一个精简版的字节流框架配合你理解装饰器模式的每个环节。这里用Python演示但逻辑和Java、Go、C#完全一致思想是通用的。3.1 基础组件先让字节跑起来第一步是定义抽象组件也就是统一的接口。class Stream: 抽象组件定义数据流的统一接口 def read(self, n: int) - bytes: raise NotImplementedError def write(self, data: bytes) - None: raise NotImplementedError def close(self) - None: raise NotImplementedError然后是具体组件。一个简单的内存字节流它是最底层、没有被装饰的原始对象。class MemoryStream(Stream): 具体组件基础的内存字节流 def __init__(self): self._buffer b self._pos 0 def read(self, n: int) - bytes: end min(self._pos n, len(self._buffer)) data self._buffer[self._pos:end] self._pos end return data def write(self, data: bytes) - None: self._buffer data def close(self) - None: self._buffer b self._pos 0这个类本身没有任何“特技”就是字节往里写、往外读像一根直通的水管。后面所有特技都是在它外面套东西。3.2 装饰器基类所有特技的底座装饰器基类是连接“原始组件”和“具体装饰器”的桥梁。它做两件事存下被包装的组件引用原样转发所有接口调用。class StreamDecorator(Stream): 装饰器基类持有组件引用并转发调用 def __init__(self, inner: Stream): self._inner inner def read(self, n: int) - bytes: return self._inner.read(n) def write(self, data: bytes) - None: self._inner.write(data) def close(self) - None: self._inner.close()这个基类看起来像“什么都没干”但其实它就是整个模式的基石。所有具体装饰器都继承它只需要重写自己关心的那一个方法。转发逻辑被统一收口在基类里具体装饰器就不会漏掉某些方法的转发这是避免BUG的关键。3.3 特技一加密装饰器加密装饰器的作用很直白写入时加密读取时解密让底层存储的都是密文。class EncryptedStream(StreamDecorator): 具体装饰器给数据流加加密特技 def __init__(self, inner: Stream, key: int): super().__init__(inner) self._key key def _transform(self, data: bytes) - bytes: # 这里用一个简单的XOR异或运算演示正式项目请使用标准加密算法 return bytes(b ^ self._key for b in data) def write(self, data: bytes) - None: encrypted self._transform(data) self._inner.write(encrypted) def read(self, n: int) - bytes: data self._inner.read(n) return self._transform(data)注意到没有这个装饰器只重写了read和writeclose被基类原样转发了。数据从EncryptedStream这里经过时每写一个字节都被异或了一次每读一个字节都被异或回来。对MemoryStream来说它看到的就是一串看不懂的字节。3.4 特技二压缩装饰器压缩装饰器比加密复杂一点因为压缩是分块进行的不能压缩一个字节就立即写出去。压一个字节不仅压缩率糟糕还会产生巨大的头部开销。所以压缩装饰器要引入缓冲区攒够一批数据再压。class CompressedStream(StreamDecorator): 具体装饰器给数据流加压缩特技 def __init__(self, inner: Stream): super().__init__(inner) self._buffer b self._dictionary {} # 模拟一个简易字典压缩仅用于演示装饰器工作方式 def write(self, data: bytes) - None: self._buffer data # 攒够64字节才压缩写入 if len(self._buffer) 64: compressed self._compress(self._buffer) self._inner.write(compressed) self._buffer b def read(self, n: int) - bytes: # 演示起见直接透传读取后解压 data self._inner.read(n) return self._decompress(data) def _compress(self, data: bytes) - bytes: # 真实项目请用zlib/lz4等成熟压缩库这里只演示链路 result [] for b in data: result.append(b ^ 0xAA) return bytes(result) def _decompress(self, data: bytes) - bytes: result [] for b in data: result.append(b ^ 0xAA) return bytes(result) def close(self) - None: # 关闭前把缓冲区里剩余的数据压完避免数据丢失 if self._buffer: self._inner.write(self._compress(self._buffer)) self._buffer b super().close()这个例子里引入了一个细节缓冲区在close时要flush。这是很多新手写装饰器容易漏掉的地方。如果一个装饰器内部有状态比如攒数据的缓冲一定要在close里做收尾处理否则最后一批数据会凭空消失。3.5 特技三缓冲装饰器缓冲装饰器的目的和压缩有点像但方向不同。压缩是减少字节数缓冲是减少IO次数。底层存储如果是磁盘或网络每次write都是一次系统调用系统调用是有代价的。缓冲装饰器把多次小写入合并成一次大写入能显著提升吞吐量。class BufferedStream(StreamDecorator): 具体装饰器给数据流加缓冲特技 def __init__(self, inner: Stream, size: int 4096): super().__init__(inner) self._size size self._write_buf b self._read_buf b self._read_pos 0 def write(self, data: bytes) - None: self._write_buf data if len(self._write_buf) self._size: self._inner.write(self._write_buf) self._write_buf b def read(self, n: int) - bytes: # 缓冲区里还有数据就直接返回没有才从内层读取 if self._read_pos len(self._read_buf): self._read_buf self._inner.read(self._size) self._read_pos 0 if not self._read_buf: return b start self._read_pos end min(start n, len(self._read_buf)) self._read_pos end return self._read_buf[start:end] def close(self) - None: if self._write_buf: self._inner.write(self._write_buf) self._write_buf b super().close()这里read的逻辑是先从内层流读一大块到自己的缓冲区然后按需切小块返回。这样如果业务代码一次次地只读几个字节也不用每次都触发底层IO。缓冲区是装饰器模式里最典型的“带状态装饰器”写法和压缩装饰器有很多共通点。3.6 任意叠加特技组合的正确姿势三个特技都写好了怎么组合# 从内到外MemoryStream - Buffered - Compressed - Encrypted raw MemoryStream() stream EncryptedStream( CompressedStream( BufferedStream(raw) ), key42 ) # 写入数据自动经过 缓冲 - 压缩 - 加密 stream.write(bhello, decorator pattern!) # 读取数据自动经过 解密 - 解压 - 缓冲切块 data stream.read(1024)这就是装饰器模式最爽的地方能力的叠加顺序由嵌套顺序决定想换组合方式就换嵌套。从内到外的顺序是Buffered-Compressed-Encrypted那么写入的数据会先缓冲再压缩最后加密落到底层。读取则反过来先解密再解压最后从缓冲里切数据。你可以把这三层装饰理解为流水线上的三道工序工序顺序不同出来的产品也不同。比如你想先加密再压缩那就把压缩放在外层stream CompressedStream( EncryptedStream( BufferedStream(raw), key42 ) )这个例子里顺序反了压缩对密文压缩率通常很差所以实际业务都会先压缩后加密。这个点我在第五节还会专门展开。4. 装饰器模式里的几个关键决策4.1 为什么组合优于继承从“行为维度”说起继承讲的是一种“is-a”关系装饰器讲的是一种“has-a”关系。为什么组合在这种场景里更合适我换个角度解释。继承是编译期静态绑定的。你写下一个类继承另一个类的那一刻这个类的行为就固定死了。想要动态组合只能靠类的排列组合去实现前面说过功能一多就爆炸。组合是运行期动态绑定的。对象之间的关系是在new的时候建立的你可以在程序运行中决定谁包住谁。甚至同一个基础组件可以同时被两个不同的装饰器链条包装互相不干扰。还有一点很多人没想到继承会破坏封装。子类往往需要知道父类实现细节才能正确覆写方法。比如你要继承InputStream重写read你很可能会触碰到父类内部那个pos状态。而装饰器只需要面向接口编程完全不需要关心对方内部怎么实现只要对端遵守接口约定就行了。4.2 接口稳定的价值和它的代价装饰器模式要求每个装饰器都实现同一套接口。这套接口就是整个模式的地基。地基稳上面的楼层随便盖地基歪盖得越高塌得越快。我见过一个项目接口设计时图省事把所有可能用到的功能全部塞进一个IStream接口包括定位、截断、多路复用这些高级能力。结果每一个装饰器都要被迫实现一堆自己不关心的方法不支持的只能抛NotSupportedException。链条一深偶尔有人不小心调用了某个不支持的组合分支运行期才炸排查起来特别痛苦。接口设计的建议是小而专。核心接口只包含所有实现都支持的基础操作特殊能力通过额外的接口下沉用类型判断或能力检测来选用。比如我上面说的StatsProvider不写在核心接口里装饰器链里有就拿来用没有就不管这样各层的自由度都更大。4.3 透明性的双刃剑你知道你调的是谁吗透明性好但也有两个让人头疼的副作用。第一个副作用是层级过深带来的定位困难。对象套了五六层以后报错信息里层层转发的调用栈会特别长。出错的不一定是装饰器本身可能是中间某层对数据动了手脚。我们后来在调试时加了一个手段每个装饰器构造时打日志把装配链条打印出来——谁包了谁一目了然。生产环境这条日志可以关掉排查问题时再打开。第二个副作用是滥用导致的过度包装。因为包一层太简单了有人就一个功能包一层包了七八层。层级多到一定程度性能损耗和大脑负担都会上升。我给自己定过一个规矩如果某个装饰器只是把一个方法调用转发给另一个对象、自己什么额外逻辑都不干那它就是多余的应该合并或去掉。装饰器模式是为了简化问题不是为了制造新的复杂度。5. 实战中常见问题与排查方法5.1 装饰顺序导致的“灵异现象”你以为装饰顺序只是性能问题不它还会导致功能直接出错。举个真实例子。有个项目在数据写入时套了Encrypted - Compressed - Buffered三层。看着没什么问题因为每个装饰器只在自己的write方法里处理数据。但读数据的时候从最外层开始调read顺序反过来了先是Encrypted.read解密然后Compressed.read解压最后Buffered.read切块。如果读写两边的装饰顺序不一致数据就全乱了。我的经验是把整个装饰链的装配逻辑收敛到一个工厂里读和写都用同一个工厂产出对称的链条。不要在业务代码里手动拼装拼装逻辑一旦散落各处早晚有人改了一处忘了另一处。另外还有一个常见的顺序坑是压缩和加密的先后。历史上有个经典教训有人先加密再压缩结果压缩算法面对加密后的高熵数据完全没有用武之地压缩率基本为0白白浪费了CPU。正确做法永远是先压缩后加密因为压缩要利用数据中的重复模式加密恰恰会摧毁这些模式。5.2 异常处理装饰器链中的异常传播数据流处理中IO异常是家常便饭。装饰器链的每一层都可能抛异常那么异常应该在哪里捕获我的经验是在链的边界处理不要在每一层都处理。最外层是天然的异常汇聚点在上游捕获一次统一做日志和安全兜底。如果每层都自己吞异常——你可能根本不知道底层已经坏了上层还在傻傻地写数据最后整个链路数据不一致。有一种特殊情况需要考虑装饰器内部有半截状态缓冲区里还有未写完的数据如果此时底层抛异常缓冲区到底该保留还是丢弃我倾向于先保留记录异常上下文给上层一个重试的机会。但要注意防止死循环重试要有次数限制。5.3 性能损耗评估装饰器真的“免费”吗装饰器模式不是零成本的。每包一层方法调用就多一层栈帧。对高频读写的数据流来说这个开销虽然单次很小但累积起来不能忽视。我做过一个简单基准测试裸流每秒能处理的数据量套上三层装饰以后大约只有原来的60%左右。损耗主要不是方法调用本身而是各层的数据拷贝。比如加密那层会把一个字节数组整个复制一遍压缩那层又会复制一遍缓冲那层再复制一遍——三层下来内存里数据被搬了三次。优化措施有几种一是减少拷贝次数各层之间尽量复用缓冲区二是把字节级的操作改成块级操作一次处理一批数据减少层间交互次数三是如果某个装饰器实际用不到就别套别背着空装备上战场。5.4 调试技巧给装饰器链装上“仪表盘”装饰器链排错最大的难点是不知道数据在某一层变成了什么样。我的做法是写一个调试用的LoggingStream装饰器它不打业务日志只把流经的数据片段特征打出来。class DebugStream(StreamDecorator): 调试装饰器打印流经的数据摘要 def write(self, data: bytes) - None: print(f[write] len{len(data)}, md5{md5(data[:32]).hexdigest()}) super().write(data) def read(self, n: int) - bytes: data self._inner.read(n) print(f[read] len{len(data)}, md5{md5(data[:32]).hexdigest()}) return data把这层加在任意位置就能看到数据在链条不同环节的变化快速定位是哪一层搞坏了数据。排查完删掉它就行不影响业务代码。6. 装饰器模式的高级玩法与扩展思路6.1 和工厂模式组合用工厂收口装配逻辑装饰器多了以后装配逻辑是一种业务规则。比如生产环境要加密、压缩、缓冲三层测试环境只要缓冲一层。这种规则适合放在工厂里统一管理。def create_stream(source: Stream, config: Config) - Stream: stream source if config.buffered: stream BufferedStream(stream, config.buffer_size) if config.compressed: stream CompressedStream(stream) if config.encrypted: stream EncryptedStream(stream, config.encrypt_key) return stream好处很明显装配顺序、条件判断都收口在一个地方业务代码只依赖工厂。下次要调整策略比如增加限流装饰器只需要改工厂一处。6.2 和策略模式结合装饰器内嵌可替换策略有些装饰器的处理逻辑本身也可能变化。比如加密装饰器今天用AES明天可能换ChaCha20。如果加密逻辑直接写死在装饰器里换算法就得改装饰器。解法是让装饰器内部持有一个策略对象class EncryptedStream(StreamDecorator): def __init__(self, inner: Stream, algorithm: EncryptionAlgorithm): super().__init__(inner) self._algorithm algorithm def write(self, data: bytes) - None: self._inner.write(self._algorithm.encrypt(data))这样装饰器负责“何时加密”的框架策略负责“怎么加密”的细节两个维度各自独立变化。数据流处理中这种组合非常常见我甚至觉得这是装饰器模式的最佳实践配置。6.3 装饰器模式与其他模式的关系辨析经常有人把装饰器和适配器搞混。两者都是包装但意图完全不同适配器是改变接口装饰器是保持接口并扩展行为。客户端用适配器时面对的是一个新接口用装饰器时面对的还是原来的接口。装饰器和代理模式也容易混淆。关键区别在于代理通常控制访问、管理生命周期往往在创建代理时对象还不存在装饰器则专注于增强功能被装饰对象一定已经存在。一句话代理是“管控者”装饰器是“增强者”。厘清这几个“亲戚关系”对做出正确的模式选型很有帮助。结尾关于装饰器模式我最后想说的这些年和数据流打交道最大的感受是装饰器模式不是那种“看起来很酷但用不上”的理论知识。它是实实在在地每天都在帮我们解决问题的工具。你用的缓冲流、加密流、压缩流底层全是它你在中间件框架里写的每个过滤器、拦截器本质上也是它。我个人在实际操作中的经验是小步快跑组装收口。装饰器可以一个一个加但链条一旦超过三层就必须用工厂把组装逻辑管理起来并且把调试用的装饰器准备好。这样装饰器模式带来的灵活性和秩序的平衡才能维持住。如果你在项目里遇到“给一个东西叠加多种能力而且以后还要继续加新能力”的场景不用犹豫装饰器模式就是这个问题的正确答案。用的时候记得想想今天聊的这几个坑尤其是顺序问题和接口粒度问题这两点做好了其他都是加分项。