设计模式手册:用 C# 代码示例掌握常用设计模式
设计模式是针对软件设计中常见问题的可复用解决方案。可以把它们理解为蓝图:不是写好的成品代码,而是经过验证的模板,你可以按需调整,用来解决自己代码库里的具体问题。
本手册是一份理解软件设计模式的实用指南,面向所有开发者,不限编程语言。示例代码用 C# 编写,但其中的概念同样适用于 Python、Java、TypeScript、Go 等其他语言。
源码地址:github.com/Clifftech123/design-patterns-handbook。
需要注意的几点:
设计模式不是代码。 它是一种组织代码结构的思维方式。它是解决特定设计问题的工具,而不是银弹。
这些概念是通用的。 示例虽然用 C# 编写,但同样的模式存在于所有语言中。如果你写 Python、Java、Go 或 TypeScript,你可能已经在不知不觉中用上了其中一些。
没有万能的模式。 每种模式都是为了解决某一类问题而存在的。理解模式解决的是什么问题,比死记硬背实现方式更重要。
我选用 C# 作为教学语言,因为它清晰、易读、认知度高。本手册的目标是让你真正理解模式本身,而不只是看懂那几行 C# 代码。
设计模式主要分为三类:创建型(Creational)、结构型(Structural)和行为型(Behavioral)。下面我们依次讲解,从创建型模式开始。
本文内容:
创建型设计模式
简单来说,Creational 模式关注的就是对象如何被创建。它分为类创建模式——通过继承决定实例化哪个类,和对象创建模式——通过委托来完成创建工作。
维基百科的定义是:
"Creational 模式旨在将系统与对象的创建、组合和表示方式解耦,从而提升系统在对象创建层面——创建什么、谁来创建、怎么创建、何时创建——的灵活性。"
(来源)
Creational 模式将对象创建的细节隐藏起来,不让客户端代码直接看到,让系统更易于管理和维护。
它们同时把对象的创建、组合和表示方式抽象掉,其余代码无需关心这些细节。
Creational 设计模式共有五种,下面逐一介绍:
Singleton:确保一个类只有一个实例,并提供一个全局访问入口。
Factory Method:定义创建对象的接口,但把具体实例化哪个类交给子类决定。
Abstract Factory:提供创建一组相关或依赖对象的接口,而无需指定具体类。
Builder:将复杂对象的构建过程与其表示解耦,使同一套构建流程能产出不同的结果。
Prototype:通过克隆已有实例来创建新对象,而非从头构建。
1. Singleton 模式
现实生活中的例子:
想想管弦乐队的指挥。一支乐队只有一位指挥,台上所有乐手都看他的指挥:何时起、何时停、节奏多快、力度多大。
指挥是唯一的权威节点,所有乐手都从同一个人那里获取指令。不可能让两位指挥同时站在台前给出不同的指示,那只会造成混乱。无论哪位乐手需要指导,最终都指向同一个人。
Singleton 在代码中的工作方式正是如此:只有一个实例,供所有需要的人共享,所有决策都由这一个地方做出。
它解决的问题:
如果两位乐手拿到不同的指挥、收到不同的指令怎么办?演出就乱套了。必须有一个指挥,所有乐手无一例外都要听从他。
乐手怎么找到指挥?他们不需要到处找。大家都知道一个固定的位置,指挥永远在那里。
怎么防止有人再指定第二个指挥?由乐团自己来把控。一旦指挥站上指挥台,别人就不能再站上去。
简单来说,这个类只有一个实例,系统中所有需要它的地方拿到的都是同一个实例(绝不会是新实例)。
以下是 Wikipedia 对 Singleton 模式的描述:
“在面向对象编程中,单例模式是一种软件设计模式,它将类的实例化限制为单一实例。当系统中恰好只需要一个对象来协调各项操作时,这种模式非常有用。”(来源)
编程示例:
我们现在直接用代码实现这个类比。OrchestraConductor 就是 Singleton:只有一个实例,所有乐手共享,由它做所有决策。
public class OrchestraConductor
{
// 第一步:在这里持有唯一实例
private static OrchestraConductor _instance;
// 第二步:私有构造函数 - 外部无法执行:new OrchestraConductor()
private OrchestraConductor() { }
// 第三步:获取指挥的唯一途径
public static OrchestraConductor GetInstance()
{
if (_instance == null)
{
_instance = new OrchestraConductor();
}
return _instance;
}
// 指挥做出的决策
public void Start() => Console.WriteLine("Conductor: Begin playing.");
public void Stop() => Console.WriteLine("Conductor: Stop playing.");
public void SetTempo(string tempo) => Console.WriteLine($"Conductor: Tempo is now {tempo}.");
}
下面看看它的实际运行效果:
// 小提琴手向指挥要人
OrchestraConductor violinist = OrchestraConductor.GetInstance();
// 钢琴手向指挥要人
OrchestraConductor pianist = OrchestraConductor.GetInstance();
// 他们找的是同一个指挥吗?
Console.WriteLine(object.ReferenceEquals(violinist, pianist)); // True
violinist.SetTempo("Allegro");
pianist.Start();
输出:
True
Conductor: Tempo is now Allegro.
Conductor: Begin playing.
两位音乐家拿到的是同一个指挥。构造函数只执行了一次。这就是单例模式(Singleton)。
何时使用单例模式
当整个应用需要共享一个资源——比如日志记录器、配置管理器或数据库连接池——就该用单例。
如果多个实例会导致行为异常或状态冲突,单例也是上策。
当你想全局访问某个对象,又不想在每个方法间层层传递它时,单例同样很方便。
2. 工厂方法(Factory Method)
想象一家招聘机构。公司打电话说"我们需要一个干活的人"。公司自己不会去"造"一个人,只是提需求。
机构根据需求决定派谁过去:开发者、设计师还是测试员。公司不关心具体来的是谁,只要那个人能胜任就行。
这就是工厂方法。你的代码要一个对象,工厂决定具体创建哪种类型,然后交给你。你只管用,不需要知道底层具体是什么。
它解决的问题:
公司不需要知道具体派来的是谁,只要有人能干活就行。派谁由机构决定,公司不用操心细节。
如果公司明天需要另一种类型的工人怎么办?还是打给同一家机构,由机构来决定。公司的流程不变,只是机构的决策变了。
如果要引入一种全新的工种呢?新设一家专业机构来处理,其他一切照旧。
简单来说,我们定义一个创建对象的接口,但把"实例化哪个类"的决定权交给子类。工厂方法模式让类可以延迟实例化,由子类来具体实现。
维基百科的描述如下:
"在面向对象编程中,工厂方法模式是一种借助工厂方法来解决对象创建问题的设计模式,调用方无需指定具体的类。工厂方法可以定义在接口中并由子类实现,也可以写在基类中,供子类选择性重写。"(来源)
编程示例:
在这个例子里,中介公司就是工厂,不同工种就是产品,企业则是客户端。
// 员工接口 —— 所有员工都能干活
public interface IWorker
{
void DoWork();
}
// 具体的员工类型
public class Developer : IWorker
{
public void DoWork() => Console.WriteLine("Developer: Writing code.");
}
public class Designer : IWorker
{
public void DoWork() => Console.WriteLine("Designer: Creating designs.");
}
// 中介基类 —— 声明工厂方法
public abstract class RecruitmentAgency
{
// 这就是工厂方法,由子类决定招哪种人
public abstract IWorker HireWorker();
}
// 具体的中介 —— 各自决定派哪种员工
public class TechAgency : RecruitmentAgency
{
public override IWorker HireWorker() => new Developer();
}
public class DesignAgency : RecruitmentAgency
{
public override IWorker HireWorker() => new Designer();
}
来看实际运行效果:
// A 公司需要技术人员
RecruitmentAgency agency = new TechAgency();
IWorker worker = agency.HireWorker();
worker.DoWork();
// B 公司需要设计人员
RecruitmentAgency agency2 = new DesignAgency();
IWorker worker2 = agency2.HireWorker();
worker2.DoWork();
输出结果:
Developer: Writing code.
Designer: Creating designs.
企业全程没有直接写 new Developer() 或 new Designer(),这个决定完全由中介(工厂)做出。这就是工厂方法模式的核心思想。
3. 抽象工厂模式(Abstract Factory)
现实案例
想象一家按系列售卖家具的店。你走进店里,先选定一种风格:现代风或维多利亚风。选好之后,你买到的所有家具都来自同一系列——沙发、椅子、茶几全都风格统一。店里有规矩,绝不会让你拎着现代风沙发配上维多利亚风椅子走出大门。你不需要挑单件再去祈祷它们能搭配,系列本身就保证了这一点。
这就是 Abstract Factory(抽象工厂)。你选定一个"家族",工厂就会从这个家族里生产出你需要的每一个对象,并保证它们都能协同工作。
它解决的问题:
如果客户混搭了不同系列的家具怎么办?整个房间会显得不伦不类。商店通过把家具归入系列来解决这个问题:你选定一个系列,所有家具都出自它。
如果商店想推出新系列怎么办?只需新增一个系列,已有的系列完全不受影响。客户的体验不变,只是选择变多了。
如果不同门店售卖的系列不同怎么办?每家店就是一个独立的工厂。客户走进任何一家店,流程都一样,具体提供哪些家具由门店自己决定。
简单来说,它提供了一个创建相关对象家族的接口,而无需指定这些对象的具体类。
Wikipedia 是这样描述的:
"抽象工厂模式提供了一种创建相关对象家族的方式,它通过封装一组具有共同主题的独立工厂,使客户端无需指定对象的具体类。"
代码示例:
家具店就是抽象工厂,Modern 和 Victorian 是具体工厂,Sofa 和 Chair 是产品。
// 产品接口 —— 每种家具类型都有一份契约
public interface ISofa { void Describe(); }
public interface IChair { void Describe(); }
// 现代系列
public class ModernSofa : ISofa
{
public void Describe() => Console.WriteLine("沙发:简约现代设计。");
}
public class ModernChair : IChair
{
public void Describe() => Console.WriteLine("椅子:极简现代风格。");
}
// 维多利亚系列
public class VictorianSofa : ISofa
{
public void Describe() => Console.WriteLine("沙发:华丽维多利亚设计。");
}
public class VictorianChair : IChair
{
public void Describe() => Console.WriteLine("椅子:经典维多利亚风格。");
}
// 抽象工厂——每个工厂都能生产一套沙发和椅子
public interface IFurnitureFactory
{
ISofa CreateSofa();
IChair CreateChair();
}
// 具体工厂——各自生产对应的系列
public class ModernFurnitureFactory : IFurnitureFactory
{
public ISofa CreateSofa() => new ModernSofa();
public IChair CreateChair() => new ModernChair();
}
public class VictorianFurnitureFactory : IFurnitureFactory
{
public ISofa CreateSofa() => new VictorianSofa();
public IChair CreateChair() => new VictorianChair();
}
来看实际效果:
// 客户订购现代系列
IFurnitureFactory factory = new ModernFurnitureFactory();
ISofa sofa = factory.CreateSofa();
IChair chair = factory.CreateChair();
sofa.Describe();
chair.Describe();
// 客户订购维多利亚系列
IFurnitureFactory factory2 = new VictorianFurnitureFactory();
ISofa sofa2 = factory2.CreateSofa();
IChair chair2 = factory2.CreateChair();
sofa2.Describe();
chair2.Describe();
输出:
沙发:简约现代设计。
椅子:极简现代风格。
沙发:华丽维多利亚设计。
椅子:经典维多利亚风格。
每一件家具都来自同一个系列。客户端从未直接写过 new ModernSofa() 或 new VictorianChair(),是工厂把整套产品绑定在了一起。这就是抽象工厂。
何时使用抽象工厂:
当你的系统需要同时处理多组相关联的对象家族,并且必须保证它们总是一起使用时,就该上抽象工厂了。
当你想在一处统一替换整组对象,而无需改动其他代码时,这个模式同样适用。
当你需要让一组相关对象保持一致性、避免不同"家族"的东西被意外混在一起时,它是个不错的选择。
4. Builder 设计模式
现实中的类比
想想裁缝做西装的场景。每位走进来的顾客都要走同一套流程:量尺寸、选面料、选里料、选纽扣、定翻领样式。
裁缝对每单都走同样的步骤,但做出来的西装却因人而异。商务人士拿走的是一套利落正装,婚礼宾客拿走的则完全不同。流程一样、裁缝一样,结果每次却不一样。
这就是 Builder。构建流程始终不变,变的是每一步里做的选择。
它解决的问题:
如果西装必须一口气组装好、不能分步骤呢?那你就得提前把所有细节都想清楚,一次搞定。裁缝把流程拆成若干步,让每个决定都清清楚楚、逐个落实。
如果两位顾客要的西装截然不同,但都找同一个裁缝呢?裁缝对两人走的是同一套流程,步骤不变,只是每步里的选择不同。
如果要引入一种新式西装呢?只需为这个新款式定义一组新的选项,裁缝的整套流程本身不用动。
简单来说,就是把复杂对象的构建过程与其最终形态解耦,让同一套构建流程能产出不同的结果。
Wikipedia 是这样描述的:
"Builder 模式将复杂对象的构建过程与其最终表示分离,使同一构建流程可以生成不同的表示。" (来源)
编程示例:
裁缝对应 Director,西装对应 Product,Builder 负责逐步构建。
// The product
public class Suit
{
public string Fabric { get; set; }
public string Lining { get; set; }
public string Buttons { get; set; }
public void Describe()
{
Console.WriteLine($"Suit: {Fabric} fabric, {Lining} lining, {Buttons} buttons.");
}
}
// 建造者接口 - 定义构建步骤
public interface ISuitBuilder
{
void SetFabric();
void SetLining();
void SetButtons();
Suit GetSuit();
}
// 商务西装建造者
public class BusinessSuitBuilder : ISuitBuilder
{
private Suit _suit = new Suit();
public void SetFabric() => _suit.Fabric = "Dark wool";
public void SetLining() => _suit.Lining = "Silk";
public void SetButtons() => _suit.Buttons = "Black horn";
public Suit GetSuit() => _suit;
}
// 婚纱西装建造者
public class WeddingSuitBuilder : ISuitBuilder
{
private Suit _suit = new Suit();
public void SetFabric() => _suit.Fabric = "Ivory linen";
public void SetLining() => _suit.Lining = "Satin";
public void SetButtons() => _suit.Buttons = "Pearl";
public Suit GetSuit() => _suit;
}
// 裁缝 - 负责整个流程的指挥者
public class Tailor
{
public Suit MakeSuit(ISuitBuilder builder)
{
builder.SetFabric();
builder.SetLining();
builder.SetButtons();
return builder.GetSuit();
}
}
下面看看实际运行效果:
Tailor tailor = new Tailor();
Suit businessSuit = tailor.MakeSuit(new BusinessSuitBuilder());
businessSuit.Describe();
Suit weddingSuit = tailor.MakeSuit(new WeddingSuitBuilder());
weddingSuit.Describe();
输出:
Suit: Dark wool fabric, Silk lining, Black horn buttons.
Suit: Ivory linen fabric, Satin lining, Pearl buttons.
同一位裁缝,遵循同样的流程,却做出了两套完全不同的西装。这就是 Builder 模式。
何时使用 Builder 设计模式
当一个对象包含很多组成部分或配置项,一次性构建会显得混乱时,可以使用 Builder 模式。
如果希望用同一套构建流程,根据每一步的不同选择产生不同的结果,它也是不错的选择。
此外,当你想把构建逻辑与对象本身分离,让两者可以独立变化时,也应考虑使用它。
5. Prototype 设计模式
现实中的例子
假设你在开发一个绘图应用,用户可以创建圆形、矩形、三角形等形状,每个形状都有自己的颜色、大小和位置。
现在假设用户想在画布上摆放十个同样大小的红色圆形。每个都从零创建,意味着同样的配置要重复十遍。若形状本身很复杂、带大量已配置的属性呢?开销和重复度都会急剧上升。
Prototype 模式的思路是:取一个已配置好的形状,clone 一份出来。副本初始是原形的精确拷贝,之后用户可以独立地移动、改色、调整大小,原形始终不受影响。这还意味着运行时新增形状类型无需应用事先感知。
解决的问题:
每次都从零创建形状开销很大,属性越多、反复设置越浪费资源。clone 一个已配置好的对象要经济得多。
应用无需知道正在复制的是哪种具体形状。运行时形状可以动态增删,应用只管调用 clone,拿回一个就绪对象,不管它是什么类型。
修改副本绝不影响原形。每个 clone 出来的形状完全独立,改动只留在副本上。
简单来说,复制一个已有对象就能得到新对象。副本初始与原形完全一致,之后可独立修改。
Wikipedia 是这样描述的:
"Prototype 模式在需要由一个原型实例决定要创建的对象类型时使用,通过 clone 该实例来产生新对象。"(来源)
编程示例:
每个形状都知道如何 clone 自身。运行时应用不会直接调用 new Circle() 或 new Rectangle(),而是 clone 已有的对象。
// Prototype 接口 —— 每个形状都必须能 clone 自身
public abstract class Shape
{
public string Colour { get; set; }
public int Size { get; set; }
public abstract Shape Clone();
public abstract void Describe();
}
// 具体形状类
public class Circle : Shape
{
public override Shape Clone() => (Shape)this.MemberwiseClone();
public override void Describe() => Console.WriteLine($"Circle | Colour: {Colour} | Size: {Size}");
}
public class Rectangle : Shape
{
public override Shape Clone() => (Shape)this.MemberwiseClone();
public override void Describe() => Console.WriteLine($"Rectangle | Colour: {Colour} | Size: {Size}");
}
下面看看实际效果:
// 创建一个配置好的圆形
Circle original = new Circle { Colour = "Red", Size = 50 };
// 克隆它,而不是从头构建
Shape clone1 = original.Clone();
Shape clone2 = original.Clone();
// 独立修改各个克隆对象
clone2.Colour = "Blue";
original.Describe();
clone1.Describe();
clone2.Describe();
输出:
Circle | Colour: Red | Size: 50
Circle | Colour: Red | Size: 50
Circle | Colour: Blue | Size: 50
clone2 变成了蓝色,原始对象仍是红色,每个对象都完全独立。这就是 Prototype 模式。
何时使用 Prototype 设计模式:
当从头创建新对象的代价很高或流程复杂,而已有对象已经完成了所有配置时,可以使用 Prototype。
此外,如果应用需要在运行时创建对象,又无法提前确定对象的具体类型,Prototype 也很有用。
当你需要一个对象的多种变体,并希望从一个已知的良好状态出发,而不是每次都重新构建时,Prototype 同样是很好的选择。
结构型设计模式
简单来说,结构型模式关注的是如何将类和对象组合成更大的结构。它借助继承和组合,让你无需从头重写代码,就能构建灵活高效的结构。
Wikipedia 是这样定义的:
"在软件工程中,结构型模式是一类设计模式,它通过识别实现实体间关系的简便方式来简化设计。"(来源)
结构型设计模式描述的是如何将对象和类组合成更大、更复杂的结构,同时保持这些结构的灵活与高效。
它们强调组合优于继承:关注的是如何连接各个组件,而不仅仅是组件本身是什么。
结构型(Structural)设计模式共有七种:
Adapter:将一个接口转换为客户期望的另一个接口,让原本不兼容的接口协同工作。
Bridge:将抽象与实现解耦,使两者可以独立变化。
Composite:将对象组合成树形结构来表示部分与整体的层次关系,让客户端可以一致地处理单个对象和组合对象。
Decorator:动态地为对象附加额外职责,是子类化的灵活替代方案。
Facade:为复杂子系统提供一个简化的统一接口。
Flyweight:通过共享机制高效支持大量细粒度对象。
Proxy:为另一个对象提供替身或占位符,以控制对该对象的访问。
1. Adapter 设计模式
现实中的例子
想象一场商务会议上的翻译。一位英国 CEO 要面对一个日本团队,CEO 只会说英语,团队只会说日语。翻译坐在中间,把每一句英文转成日文传达给团队。双方各自说自己的语言,CEO 和团队都不用改变自己的沟通方式——翻译让两边变得兼容。
这就是 Adapter。客户端使用一个接口,而另一侧使用不同的接口。Adapter 居中协调,让双方协同工作,任何一方都不需要改变。
它能解决的问题:
CEO 不会日语,团队不会英语,两者天然不兼容。翻译把一方适配到另一方,双方本身无需改动。
如果 CEO 现在要面对一个法国团队?换一位法语翻译就行。CEO 的流程不变,只有翻译换了。
如果现有类有一个好用的方法但接口不匹配?用 Adapter 包一层。系统其余部分与 Adapter 交互,原有类保持不动。
简单来说,就是给现有类套一层新接口,客户端无需任何改动就能直接使用。
Wikipedia 的定义如下:
"在软件工程中,适配器模式(Adapter Pattern)是一种软件设计模式(也称 Wrapper),它允许将一个现有类的接口当作另一个接口来使用。通常用于让现有类与其他类协作,而无需修改其源代码。" (来源)
编程示例:
在这个类比中,CEO 是客户端,日本团队成员是被适配者(adaptee)。他们很有价值,但"说"的是不对的接口。翻译员就是适配器。
// CEO 期望的接口:一个能用英语接收信息的人
public interface IEnglishSpeaker
{
void Speak(string message);
}
// 日本团队成员,只会说日语(被适配者)
public class JapaneseTeamMember
{
public void SpeakJapanese(string message)
{
Console.WriteLine($"Team member (Japanese): {message}");
}
}
// 翻译员,将日语使用者适配到英语接口
public class Translator : IEnglishSpeaker
{
private readonly JapaneseTeamMember _teamMember;
public Translator(JapaneseTeamMember teamMember)
{
_teamMember = teamMember;
}
public void Speak(string message)
{
string translated = TranslateToJapanese(message);
_teamMember.SpeakJapanese(translated);
}
private string TranslateToJapanese(string english) => english switch
{
"Good morning, team." => "おはようございます、チームの皆さん。",
"Please review the proposal." => "提案書を確認してください。",
_ => $"[Japanese: {english}]"
};
}
// CEO,只会跟 IEnglishSpeaker 打交道
public class CEO
{
private readonly IEnglishSpeaker _speaker;
public CEO(IEnglishSpeaker speaker)
{
_speaker = speaker;
}
public void Address(string message)
{
Console.WriteLine($"CEO (English): {message}");
_speaker.Speak(message);
}
}
来看看实际运行效果:
JapaneseTeamMember teamMember = new JapaneseTeamMember();
IEnglishSpeaker translator = new Translator(teamMember);
CEO ceo = new CEO(translator);
ceo.Address("Good morning, team.");
ceo.Address("Please review the proposal.");
输出:
CEO (English): Good morning, team.
Team member (Japanese): おはようございます、チームの皆さん。
CEO (English): Please review the proposal.
Team member (Japanese): 提案書を確認してください。
CEO 从始至终不知道 JapaneseTeamMember 的存在,团队也不知道 CEO 用的是什么接口。Translator 在两边都没改一行代码的情况下让它们顺利协作。这就是适配器模式。
使用场景
当你想复用一个已有的类,但它的接口和你的代码所期望的不一致时,就用适配器模式。
当你想创建一个能和多个接口不兼容的类协同工作的可复用类时,它同样很有用。
另外,当你需要集成第三方库或遗留代码而又不想改动它们时,也可以用它。
2. 桥接模式(Bridge)
现实世界的例子
想想电视遥控器和电视的关系。遥控器是一个东西,电视是另一个东西。遥控器有基础版也有智能版,电视有索尼也有三星,但任何遥控器都能配任何电视——你不会被绑死在某个组合上。买了新三星电视,旧遥控器照样能用;换了个智能万能遥控器,家里所有电视也都能控制。双方都不需要了解对方的内部细节。
这就是桥接模式。抽象部分(遥控器)和实现部分(电视)是两个独立的层级结构,可以各自独立地扩展和变化。
它解决的问题:
如果每个遥控器都和某个特定电视品牌绑死会怎样?你就得写 SonyBasicRemote、SamsungBasicRemote、SonySmartRemote、SamsungSmartRemote……每种组合一个类。新增一个电视品牌,遥控器的类数量就要翻倍。桥接模式正是为了阻止这种类爆炸。
如果你想新增一种遥控器类型,又不想动电视的代码怎么办?用桥接模式,你只需新建一个遥控器类,电视那边完全不用改。
反过来,想新增一个电视品牌而不动遥控器呢?答案一样。加一个新电视类就行,现有的所有遥控器都自动兼容。
简单来说,就是把一个大类拆成两层独立的继承体系(抽象层和实现层),这样各自都能独立变更和扩展,互不影响。
Wikipedia 是这样描述的:
"桥接模式是一种软件工程中的设计模式,旨在将抽象与实现解耦,使两者可以独立变化。"(来源)
编程示例:
遥控器是抽象层,电视品牌是实现层。两者通过桥接(即 ITV 接口)相连,但彼此不依赖对方的内部细节。
// 实现接口——任何电视都必须具备的能力
public interface ITV
{
void TurnOn();
void TurnOff();
void SetChannel(int channel);
void SetVolume(int volume);
}
// 具体实现——每个品牌按自己的方式处理
public class SonyTV : ITV
{
public void TurnOn() => Console.WriteLine("Sony TV: Powering on. BRAVIA display ready.");
public void TurnOff() => Console.WriteLine("Sony TV: Shutting down.");
public void SetChannel(int ch) => Console.WriteLine($"Sony TV: Switching to channel {ch}.");
public void SetVolume(int vol) => Console.WriteLine($"Sony TV: Volume set to {vol}.");
}
public class SamsungTV : ITV
{
public void TurnOn() => Console.WriteLine("Samsung TV: Turning on. Smart Hub loading.");
public void TurnOff() => Console.WriteLine("Samsung TV: Powering off.");
public void SetChannel(int ch) => Console.WriteLine($"Samsung TV: Channel {ch} selected.");
public void SetVolume(int vol) => Console.WriteLine($"Samsung TV: Volume at {vol}.");
}
// 抽象层——遥控器持有所控制电视的引用
public abstract class RemoteControl
{
protected ITV _tv;
protected RemoteControl(ITV tv) { _tv = tv; }
public abstract void TurnOn();
public abstract void TurnOff();
public abstract void SetChannel(int channel);
}
// 精化抽象层——基础遥控器,仅执行电视本身具备的功能
public class BasicRemote : RemoteControl
{
public BasicRemote(ITV tv) : base(tv) { }
public override void TurnOn() => _tv.TurnOn();
public override void TurnOff() => _tv.TurnOff();
public override void SetChannel(int ch) => _tv.SetChannel(ch);
}
// 精化抽象层——智能遥控器,在基础功能上叠加额外行为
public class SmartRemote : RemoteControl
{
public SmartRemote(ITV tv) : base(tv) { }
public override void TurnOn()
{
Console.WriteLine("Smart Remote: Activating voice control.");
_tv.TurnOn();
}
public override void TurnOff()
{
Console.WriteLine("Smart Remote: Saving watch history.");
_tv.TurnOff();
}
public override void SetChannel(int ch)
{
Console.WriteLine("Smart Remote: Looking up channel guide.");
_tv.SetChannel(ch);
}
public void SetVolume(int vol) => _tv.SetVolume(vol);
}
来看看实际运行效果:
// 基础遥控器搭配索尼电视
Console.WriteLine("--- Basic Remote + Sony TV ---");
RemoteControl basicSony = new BasicRemote(new SonyTV());
basicSony.TurnOn();
basicSony.SetChannel(5);
basicSony.TurnOff();
// 智能遥控器搭配三星电视
Console.WriteLine("\n--- Smart Remote + Samsung TV ---");
SmartRemote smartSamsung = new SmartRemote(new SamsungTV());
smartSamsung.TurnOn();
smartSamsung.SetChannel(10);
smartSamsung.SetVolume(20);
smartSamsung.TurnOff();
// 自由替换——智能遥控器搭配索尼电视,无需任何代码改动
Console.WriteLine("\n--- Smart Remote + Sony TV ---");
SmartRemote smartSony = new SmartRemote(new SonyTV());
smartSony.TurnOn();
smartSony.SetChannel(3);
smartSony.TurnOff();
输出:
--- 基础遥控器 + 索尼电视 ---
Sony TV: 正在开机。BRAVIA 显示就绪。
Sony TV: 切换到频道 5。
Sony TV: 正在关机。
--- 智能遥控器 + 三星电视 ---
Smart Remote: 正在启用语音控制。
Samsung TV: 正在开机。Smart Hub 加载中。
Smart Remote: 正在查询频道指南。
Samsung TV: 已选择频道 10。
Samsung TV: 音量设为 20。
Smart Remote: 正在保存观看历史。
Samsung TV: 正在关机。
--- 智能遥控器 + 索尼电视 ---
Smart Remote: 正在启用语音控制。
Sony TV: 正在开机。BRAVIA 显示就绪。
Smart Remote: 正在查询频道指南。
Sony TV: 切换到频道 3。
Smart Remote: 正在保存观看历史。
Sony TV: 正在关机。
同一个 SmartRemote 无需任何修改就能同时配合索尼和三星电视工作。要新增一个电视品牌(比如 LG),只需创建一个新类,所有现有遥控器立刻就能与它配合使用。这就是 Bridge(桥接)模式。
适用场景
当你想避免抽象与其实现之间的永久绑定,使两者都能在运行时切换时,可以使用 Bridge 模式。
当抽象和实现都需要通过子类独立扩展时,也适合使用它。
此外,当实现的变化不应影响客户端代码时,它也是不错的选择——客户端无需重新编译。
3. Composite 组合模式
现实世界的例子
想象一张公司的组织架构图:公司有一位 CEO,CEO 之下是各部门主管,每位主管领导一个由众多员工组成的部门,有些部门下面还设有更小的子团队。
现在假设你想知道工资总成本。询问单个员工,他会告诉你自己的工资;询问整个部门,它会汇总部门内所有人的工资,包括嵌套的子团队;询问整个公司,则会汇总所有层级的全部工资。
同样的问题,用同样的方式提问,无论面对的是一个人还是成千上万人。
这就是 Composite 模式。单个对象和对象组合共享同一个接口,调用者永远不需要知道自己面对的是哪一种。
它解决的问题:
如果处理单个员工和处理整个部门要写不同的代码,会怎样?你将不得不到处写
if判断,只为搞清楚自己在操作什么。Composite 模式彻底消除了这种麻烦:永远只用一个接口。如果一个部门下面还能再嵌套其他部门呢?Composite 天然支持任意深度的嵌套。调用方只需要向树顶发起请求,操作就会自动向下传递。
如果想新增一种团队或角色类型怎么办?只需实现同一个接口,树中上层的逻辑完全不用改。
简单来说,Composite 把对象组合成树形结构,让单个对象和对象组都通过同一个接口来操作,调用方完全不用区分它们。
维基百科的定义如下:
"Composite 模式将一组对象当作与单个同类型对象相同的方式来处理。其意图是将对象组合成树形结构,以表示整体与部分之间的层级关系。" (来源)
编程示例:
树中的每个节点——无论是一个员工还是一个完整部门——都实现 IEmployee 接口,调用方以完全相同的方式对待它们。
// 组件接口——所有叶子节点和组合节点共用这份契约
public interface IEmployee
{
string Name { get; }
int GetSalary();
void GetDetails(string indent = "");
}
// 叶子节点——没有下属的单个员工
public class Employee : IEmployee
{
private readonly int _salary;
public string Name { get; }
public Employee(string name, int salary)
{
Name = name;
_salary = salary;
}
public int GetSalary() => _salary;
public void GetDetails(string indent = "") => Console.WriteLine($"{indent}- {Name} (£{_salary:N0})");
}
// 组合模式示例 —— 部门可以包含员工或其他部门
public class Department : IEmployee
{
private readonly List<IEmployee> _members = new();
public string Name { get; }
public Department(string name) { Name = name; }
public void Add(IEmployee employee) => _members.Add(employee);
public void Remove(IEmployee employee) => _members.Remove(employee);
public int GetSalary() => _members.Sum(m => m.GetSalary());
public void GetDetails(string indent = "")
{
Console.WriteLine($"{indent}[{Name}] Total: £{GetSalary():N0}");
foreach (var member in _members)
member.GetDetails(indent + " ");
}
}
下面看看实际运行效果:
// 各个员工
var ceo = new Employee("Alice (CEO)", 120_000);
var cto = new Employee("Bob (CTO)", 95_000);
var dev1 = new Employee("Carol (Developer)", 65_000);
var dev2 = new Employee("David (Developer)", 62_000);
var cfo = new Employee("Eve (CFO)", 90_000);
var accountant = new Employee("Frank (Accountant)", 55_000);
// 构建工程部
var engineering = new Department("Engineering");
engineering.Add(cto);
engineering.Add(dev1);
engineering.Add(dev2);
// 构建财务部
var finance = new Department("Finance");
finance.Add(cfo);
finance.Add(accountant);
// 构建整个公司
var company = new Department("Acme Corp");
company.Add(ceo);
company.Add(engineering);
company.Add(finance);
// 查询整个公司 —— 一次调用即可汇总所有信息
Console.WriteLine("=== Full Company ===");
company.GetDetails();
// 只查询一个部门 —— 同样的调用,相同的接口
Console.WriteLine("\n=== Engineering Only ===");
engineering.GetDetails();
// 查询单个员工 —— 同样的调用,相同的接口
Console.WriteLine("\n=== Single Employee ===");
dev1.GetDetails();
输出:
=== 全公司 ===
[Acme Corp] 总计:£487,000
- Alice (CEO)(£120,000)
[Engineering] 总计:£222,000
- Bob (CTO)(£95,000)
- Carol (Developer)(£65,000)
- David (Developer)(£62,000)
[Finance] 总计:£145,000
- Eve (CFO)(£90,000)
- Frank (Accountant)(£55,000)
=== 仅工程部 ===
[Engineering] 总计:£222,000
- Bob (CTO)(£95,000)
- Carol (Developer)(£65,000)
- David (Developer)(£62,000)
=== 单个员工 ===
- Carol (Developer)(£65,000)
company.GetDetails()、engineering.GetDetails()、dev1.GetDetails()——在树的不同层级上调用同一个方法。调用方根本不用关心自己操作的是什么。这就是 Composite 模式。
何时使用
当你需要表示部分与整体的层次结构时,就用 Composite,比如单个元素和元素组需要以统一方式使用的树形结构。
当你希望客户端代码以同样的方式处理单个对象和对象集合,不需要任何特殊判断时,它也很合适。
另外,如果结构可以嵌套到任意深度,而这种深度又不该影响调用方的使用方式,那 Composite 就是很好的选择。
4. 装饰器模式(Decorator)
现实中的例子
想想在咖啡馆点咖啡的过程:先点一杯浓缩咖啡(espresso),然后加牛奶,再加香草糖浆,最后加奶油。每次添加都叠加在已有的东西上,带来自己的价格和描述。中间那杯浓缩咖啡始终不变,你只是一层一层往上加而已。你可以加双份糖浆,也可以完全不加牛奶。无论怎么组合,都不需要为每种组合专门创建一种新咖啡。
这就是 Decorator 模式。你从一个基础对象开始,一层层地包装它。每一层都添加自己的行为,然后把调用委托给下一层。
它解决的问题:
如果每种组合都要一个类怎么办?EspressoWithMilk、EspressoWithMilkAndVanilla、EspressoWithMilkAndVanillaAndCream……类的数量会爆炸式增长。而 Decorator 在运行时动态添加行为,完全不需要这些类。
如果基础咖啡不应该被改动怎么办?它确实没被改动。espresso 类保持原样,各个 decorator 只是包装并独立地扩展它。
需要加一种新配料怎么办?新建一个装饰器类即可,所有已有的组合照旧不受影响。
简单来说,就是把一个对象包裹在一层或多层里,每层在委托给下一层之前或之后,加上自己的行为。
维基百科是这样描述的:
"装饰器模式允许在运行时动态地为单个对象附加行为,而不影响同类的其他实例。" (来源)
编程示例:
咖啡是组件(component),每种配料是一个装饰器。每个装饰器包裹组件,并在描述和价格上叠加自己的部分。
// 组件接口——无论原味还是加了配料,所有咖啡都实现这个接口
public interface ICoffee
{
string GetDescription();
double GetCost();
}
// 基础组件——一杯纯浓缩咖啡
public class Espresso : ICoffee
{
public string GetDescription() => "Espresso";
public double GetCost() => 1.00;
}
// 装饰器基类——包裹任意 ICoffee 并委托给它
public abstract class CoffeeDecorator : ICoffee
{
protected readonly ICoffee _coffee;
protected CoffeeDecorator(ICoffee coffee) { _coffee = coffee; }
public virtual string GetDescription() => _coffee.GetDescription();
public virtual double GetCost() => _coffee.GetCost();
}
// 具体装饰器——各自添加一层
public class Milk : CoffeeDecorator
{
public Milk(ICoffee coffee) : base(coffee) { }
public override string GetDescription() => _coffee.GetDescription() + ", Milk";
public override double GetCost() => _coffee.GetCost() + 0.30;
}
public class VanillaSyrup : CoffeeDecorator
{
public VanillaSyrup(ICoffee coffee) : base(coffee) { }
public override string GetDescription() => _coffee.GetDescription() + ", Vanilla Syrup";
public override double GetCost() => _coffee.GetCost() + 0.50;
}
public class WhippedCream : CoffeeDecorator
{
public WhippedCream(ICoffee coffee) : base(coffee) { }
public override string GetDescription() => _coffee.GetDescription() + ", Whipped Cream";
public override double GetCost() => _coffee.GetCost() + 0.75;
}
来看实际效果:
// A plain espresso
ICoffee order = new Espresso();
Console.WriteLine($"{order.GetDescription()} => £{order.GetCost():F2}");
// Wrap it with milk
order = new Milk(order);
Console.WriteLine($"{order.GetDescription()} => £{order.GetCost():F2}");
// Wrap it with vanilla syrup on top
order = new VanillaSyrup(order);
Console.WriteLine($"{order.GetDescription()} => £{order.GetCost():F2}");
// Wrap it with whipped cream on top of that
order = new WhippedCream(order);
Console.WriteLine($"{order.GetDescription()} => £{order.GetCost():F2}");
输出:
Espresso => £1.00
Espresso, Milk => £1.30
Espresso, Milk, Vanilla Syrup => £1.80
Espresso, Milk, Vanilla Syrup, Whipped Cream => £2.55
每一行都是在上一步的基础上再套一层。Espresso 本身从未改变,只是成本和描述随着每层包装不断增长。这就是 Decorator 模式。
适用场景
当你想给某个对象动态添加职责,又不希望影响同类其他对象时,用 Decorator 模式。
如果靠继承来实现,行为组合一多就会类爆炸,这时候 Decorator 也是更好的选择。
当你需要在运行时按任意顺序、相互独立地叠加多种行为时,也应该用它。
5. 外观模式(Facade)
现实类比
想象你在购物网站上点了"提交订单"。就这一次点击,后台其实做了一串事:检查库存、扣款支付、生成物流单、发送确认邮件。你完全看不到这些过程,点一下按钮,拿到一个结果。四个独立系统的复杂度,被藏在一个干净的操作后面。
这就是 Facade 模式:在一大堆复杂的内部组件前面,暴露一个简单的统一接口。调用方不需要知道背后发生了什么。
解决的问题:
如果客户端必须直接调用每个子系统会怎样?查库存、处理支付、生成物流单、发邮件,还得保证顺序正确、逐一处理各种失败。Facade 把这一切封装成一次调用。
如果某个子系统内部变了会怎样?Facade 吸收这次变更,客户端代码完全无感知,只需要更新 Facade 本身。
如果不同的客户端需要同样的流程怎么办?它们都调用同一个 Facade 方法。逻辑集中在一处,不会在每个调用方重复出现。
简单来说,Facade 提供一个统一、简单的接口,把一组子系统的复杂性都隐藏在它背后。
Wikipedia 是这样描述的:
"Facade 模式(也拼作 façade)是一种常用于面向对象编程的软件设计模式。正如建筑中的外立面一样,Facade 是一个对象,充当面向前端的接口,掩盖其背后更复杂的底层或结构性代码。"(来源)
编程示例:
每个子系统各司其职,而 OrderFacade 是唯一的入口,负责协调所有子系统。客户端只需要跟 Facade 打交道。
// 子系统 1:检查商品是否有库存
public class InventoryService
{
public bool CheckStock(string item)
{
Console.WriteLine($"Inventory: Checking stock for {item}.");
return true;
}
}
// 子系统 2:处理支付
public class PaymentService
{
public bool ProcessPayment(string cardNumber, double amount)
{
Console.WriteLine($"Payment: Charging £{amount:F2} to card ending {cardNumber[^4..]}.");
return true;
}
}
// 子系统 3:生成物流面单
public class ShippingService
{
public string GenerateLabel(string item, string address)
{
Console.WriteLine($"Shipping: Generating label for {item} to {address}.");
return "TRACK-29384";
}
}
// 子系统 4:发送确认邮件
public class EmailService
{
public void SendConfirmation(string email, string trackingCode)
{
Console.WriteLine($"Email: Confirmation sent to {email}. Tracking code: {trackingCode}.");
}
}
// Facade 模式 — 一个方法,隐藏全部四个子系统
public class OrderFacade
{
private readonly InventoryService _inventory = new();
private readonly PaymentService _payment = new();
private readonly ShippingService _shipping = new();
private readonly EmailService _email = new();
public void PlaceOrder(string item, string cardNumber, double amount, string address, string email)
{
Console.WriteLine("=== Placing Order ===");
if (!_inventory.CheckStock(item))
{
Console.WriteLine("Order failed: item out of stock.");
return;
}
if (!_payment.ProcessPayment(cardNumber, amount))
{
Console.WriteLine("Order failed: payment declined.");
return;
}
string trackingCode = _shipping.GenerateLabel(item, address);
_email.SendConfirmation(email, trackingCode);
Console.WriteLine($"\nOrder complete. Your tracking code is {trackingCode}.");
}
}
下面看看实际效果:
OrderFacade store = new OrderFacade();
store.PlaceOrder(
item: "Wireless Headphones",
cardNumber: "4111111111111234",
amount: 79.99,
address: "42 Maple Street, London",
email: "customer@email.com"
);
输出:
=== Placing Order ===
Inventory: Checking stock for Wireless Headphones.
Payment: Charging £79.99 to card ending 1234.
Shipping: Generating label for Wireless Headphones to 42 Maple Street, London.
Email: Confirmation sent to customer@email.com. Tracking code: TRACK-29384.
Order complete. Your tracking code is TRACK-29384.
调用方只调了一个方法,四个子系统就按正确顺序依次跑完了,调用者完全感知不到背后的复杂性。这就是 Facade。
适用场景
当需要为复杂子系统提供简洁接口、让调用方无需了解内部细节时,用 Facade。它也适合分层设计——让高层代码只跟 Facade 打交道,而不是直接调用底层子系统。此外,当你需要一个统一入口来协调跨多个服务的多步操作时,Facade 也是好选择。
6. Flyweight 设计模式
现实类比
想象一个游戏要渲染一片森林。森林里有上万棵树,每棵树都有类型名、颜色和纹理。但其中绝大多数是橡树,而且所有橡树长得一模一样。
如果为每棵树都建一个独立对象、各自存一份名称、颜色和纹理,内存浪费极大。更好的做法是:创建一个共享的橡树对象来集中存放这些数据,森林中每棵橡树都指向这同一个对象,自身只记录地图上的位置。
这就是 Flyweight(享元)模式。大量实例中相同的数据被共享,每个实例独有的数据单独存放,需要时才传入。
解决的问题:
如果为每棵树都创建完整对象,上万棵树就意味着同一套名称、颜色和纹理被存了上万次。Flyweight 只存一份共享数据,到处复用。
如果新增一种树,工厂只需为它创建一个共享对象,所有该类型的树立刻就能使用,无需额外内存。
如果森林要按各自位置渲染每棵树,位置是每棵树独有的数据,存在树自身上,仅在渲染时传给共享对象,共享对象从不保存位置。
简单来说,就是把对象的数据拆成两部分:跨实例共享的部分和每实例独有的部分。共享部分只存一份,独有部分需要时才传入。
维基百科的描述如下:
"享元对象通过与其他相似对象尽可能多地共享数据,将内存占用降到最低。这是在大量对象场景下,避免简单重复表示导致不可接受的内存消耗的一种手段。" (来源)