设计方法论”这一理念已经发展成为一个庞大的学术领域。 而我被认为是所谓设计方法论的主要倡导者之一。 我对这一现象感到非常遗憾,并公开声明,我拒绝将设计方法论作为研究对象。 因为我认为将设计实践与设计研究分开是荒谬的。 事实上,那些只研究设计方法论而不进行设计实践的人,几乎总是失败的设计师。 他们缺乏内在的生命力,失去了创造的欲望,或者从一开始就没有这种欲望。(Alexander,1971)
人们常说,设计模式(Design Patterns)是万能解决方案。
在OOP(面向对象编程,Object-Oriented Programming)出现后,GOF(四人组,Gang of Four)的设计模式书问世并成为某种福音书。
许多开发者在阅读模式书籍时,仅看结构图就开始编写代码,结果往往导致僵化的实现。
我们在学习设计方法和模式时,总是在“图式理解”和“实际经验”之间的鸿沟中摇摆不定。
正如亚历山大所说,将设计方法从设计实体中分离出来,最终只会谈论空洞的幻影。
其实,在讨论模式之前,我们可以谈论习语(Idiom)等其他内容,但这个话题留到以后再说。
本文我们将讨论设计模式。
(艾伦·凯2003年的电子邮件故事)
我们今天所讨论的模式是如何产生的?
通常我们认为的面向对象编程(OOP)起源于Simula,
但一般认为第一个提出这个概念的人是艾伦·凯(Alan Kay),他在1967年初使用了这个术语。
然而,艾伦·凯在2003年的采访中表示,
他所说的面向对象编程指的是
消息传递(Messaging)、封装(Encapsulation)、动态绑定(Dynamic Binding)。
但如果这样说,这并不是我们现在所理解的OOP概念。
那么,我们现在所理解的概念是从哪里来的呢?
(我们实际认知中的OOP概念)
目前我们使用的OOP概念是由格雷迪·布奇(Booch)提出的,并在此基础上扩展的。
- OOP是一种逻辑构建组件,使用对象而非算法。
- 每个对象是某个类的实例。
- 类通过继承关系相互关联。
通常我们将其总结为三个要素:
封装(Encapsulation)、继承(Inheritance)、多态(Polymorphism)。
这就是我们常说的面向对象编程的起点。
随着这种面向对象编程方式迅速成为主流,一本伟大的编程经典诞生了。
那就是
GOF(四人组,Gang of Four)。
埃里希·伽玛(Erich Gamma)、理查德·赫尔姆(Richard Helm)、拉尔夫·约翰逊(Ralph Johnson)和约翰·弗利西德斯(John Vlissides)
撰写的《设计模式》成为了现代编程的经典之作。
GOF展示了23种设计模式,并将其分为三种类型:
三种类型:
创建型模式(Creational Pattern)- 结构型模式(Structural Pattern)- 行为型模式(Behavioral Pattern)
创建型模式(Creational Pattern)
- 与对象创建相关的模式。
- 提高对象创建过程的灵活性,便于代码维护。
示例:单例模式(Singleton Pattern)、抽象工厂模式(Abstract Factory Pattern)、建造者模式(Builder Pattern)、工厂方法模式(Factory Method Pattern)、原型模式(Prototype Pattern)。
结构型模式(Structural Pattern)
- 与程序结构相关的模式。
- 设计程序中的数据结构或接口结构。
- 通过组合类或对象形成更大的结构。
示例:适配器模式(Adapter Pattern)、桥接模式(Bridge Pattern)、组合模式(Composite Pattern)、装饰器模式(Decorator Pattern)、外观模式(Facade Pattern)、享元模式(Flyweight Pattern)、代理模式(Proxy Pattern)。
行为型模式(Behavioral Pattern)
- 将对象间的重复交互模式化。
- 关注对象间算法或责任分配。
- 最小化耦合度(解耦)。
示例:命令模式(Command Pattern)、解释器模式(Interpreter Pattern)、迭代器模式(Iterator Pattern)、中介者模式(Mediator Pattern)、备忘录模式(Memento Pattern)、观察者模式(Observer Pattern)、状态模式(State Pattern)、策略模式(Strategy Pattern)、模板方法模式(Template Method Pattern)、访问者模式(Visitor Pattern)。
这些例子引起了爆炸性的流行。
它们成为了OOP中解决问题的一种模板。
然而,随着这些模式的流行,滥用模式的问题也随之而来,
很多人开始把这些模式当作“公理”,而不是“前辈程序员的经验产物”。
本文旨在强调,编程模式不是“公理”,
而是“前辈程序员创造的通用工具”的观点。
了解模式确实可以增加应对问题的选择,
但重要的是如何解决当前面临的问题,
而不是死记硬背模式目录。
利用模式进行重构,
乔舒亚的名著中也提到以下内容:
“模式的结构图只是示例,而不是规范。它描述了我们最常见的实现方式。
因此,结构图很可能与你自己的实现方式有许多共同点,
但差异是不可避免的,甚至是可取的。至少你会根据你的领域更改参与者的名称。
在调整实现上的优缺点后,你的实现方式可能与结构图有很大不同。”
——John Vlissides
这是GOF作者之一约翰·弗利西德斯的话。
这句话的重点是什么?
也就是说,即使是同一个模式,也可以用多种方式实现。
让我们简单举个例子。
// 策略接口
interface IDiscountStrategy
{
double ApplyDiscount(double price);
}
// 具体策略类 1
class FixedDiscountStrategy : IDiscountStrategy
{
private double discountAmount;
public FixedDiscountStrategy(double amount)
{
discountAmount = amount;
}
public double ApplyDiscount(double price)
{
return price - discountAmount;
}
}
// 具体策略类 2
class PercentageDiscountStrategy : IDiscountStrategy
{
private double discountRate;
public PercentageDiscountStrategy(double rate)
{
discountRate = rate;
}
public double ApplyDiscount(double price)
{
return price * (1 - discountRate);
}
}
// 上下文类
class ShoppingCart
{
private IDiscountStrategy discountStrategy;
public ShoppingCart(IDiscountStrategy strategy)
{
discountStrategy = strategy;
}
public double CalculateTotalPrice(double originalPrice)
{
return discountStrategy.ApplyDiscount(originalPrice);
}
}
// 使用示例
ShoppingCart cart1 = new ShoppingCart(new FixedDiscountStrategy(1000));
ShoppingCart cart2 = new ShoppingCart(new PercentageDiscountStrategy(0.1));
Console.WriteLine($"Fixed Discount: {cart1.CalculateTotalPrice(10000)}"); // 固定折扣:9000
Console.WriteLine($"Percentage Discount: {cart2.CalculateTotalPrice(10000)}"); // 百分比折扣:9000
常见的策略模式(Strategy Pattern)。
让我们尝试应用Lambda表达式。
这样就不需要定义接口和具体实现类。
// 折扣策略委托(函数类型定义)
delegate double DiscountStrategy(double price);
// 上下文类
class ShoppingCart
{
private DiscountStrategy discountStrategy;
public ShoppingCart(DiscountStrategy strategy)
{
discountStrategy = strategy;
}
public double CalculateTotalPrice(double originalPrice)
{
return discountStrategy(originalPrice);
}
}
// 使用示例(使用Lambda表达式直接定义策略)
ShoppingCart cart1 = new ShoppingCart(price => price - 1000); // 固定折扣策略(Lambda表达式)
ShoppingCart cart2 = new ShoppingCart(price => price * 0.9); // 百分比折扣策略(Lambda表达式)
Console.WriteLine($"Fixed Discount (Lambda): {cart1.CalculateTotalPrice(10000)}"); // 固定折扣(Lambda):9000
Console.WriteLine($"Percentage Discount (Lambda): {cart2.CalculateTotalPrice(10000)}"); // 百分比折扣(Lambda):9000
代码量显著减少,各种策略可以用Lambda表达式轻松定义,
并且可以根据需要更加灵活地动态更改策略!
接下来,让我们尝试用Lambda表达式/方法引用(Method Reference)来实现命令模式(Command Pattern)。
// 命令接口
interface ICommand
{
void Execute();
}
// 具体命令类 1
class LightOnCommand : ICommand
{
private Light light;
public LightOnCommand(Light light)
{
this.light = light;
}
public void Execute()
{
light.TurnOn();
}
}
// 具体命令类 2
class LightOffCommand : ICommand
{
private Light light;
public LightOffCommand(Light light)
{
this.light = light;
}
public void Execute()
{
light.TurnOff();
}
}
// 接收者类
class Light
{
public void TurnOn() { Console.WriteLine("Light On"); }
public void TurnOff() { Console.WriteLine("Light Off"); }
}
// 调用者类
class RemoteControl
{
private ICommand onCommand;
private ICommand offCommand;
public RemoteControl(ICommand on, ICommand off)
{
this.onCommand = on;
this.offCommand = off;
}
public void PressOnButton() { onCommand.Execute(); }
public void PressOffButton() { offCommand.Execute(); }
}
// 使用示例
Light livingRoomLight = new Light();
RemoteControl remote = new RemoteControl(new LightOnCommand(livingRoomLight), new LightOffCommand(livingRoomLight));
remote.PressOnButton(); // Light On
remote.PressOffButton(); // Light Off
// 命令委托 (void 返回类型,无参数)
delegate void Command();
// 接收者类 (同上)
class Light
{
public void TurnOn() { Console.WriteLine("Light On"); }
public void TurnOff() { Console.WriteLine("Light Off"); }
}
// 调用者类
class RemoteControl
{
private Command onCommand;
private Command offCommand;
public RemoteControl(Command on, Command off)
{
this.onCommand = on;
this.offCommand = off;
}
public void PressOnButton() { onCommand(); }
public void PressOffButton() { offCommand(); }
}
// 使用示例 (使用 Lambda 表达式、方法引用直接定义命令)
Light livingRoomLight = new Light();
RemoteControl remote = new RemoteControl(
livingRoomLight.TurnOn, // 方法引用 (替代 LightOnCommand)
() => livingRoomLight.TurnOff() // Lambda 表达式 (替代 LightOffCommand)
);
remote.PressOnButton(); // Light On
remote.PressOffButton(); // Light Off
代码量显著减少,各种策略可以用Lambda表达式轻松定义,
并且可以根据需要更加灵活地动态更改策略!
接下来,让我们尝试用Lambda表达式/方法引用(Method Reference)来实现命令模式(Command Pattern)。
代码行数再次减少,各种命令可以通过方法引用轻松定义,
并且可以根据需要动态更改命令,变得更加灵活!
现在让我们实现工厂模式(Factory Pattern)。
// 产品接口
interface IProduct
{
void DoSomething();
}
// 具体产品类 1
class ConcreteProductA : IProduct
{
public void DoSomething() { Console.WriteLine("Product A"); }
}
// 具体产品类 2
class ConcreteProductB : IProduct
{
public void DoSomething() { Console.WriteLine("Product B"); }
}
// 工厂接口
interface IFactory
{
IProduct CreateProduct();
}
// 具体工厂类 1
class ConcreteFactoryA : IFactory
{
public IProduct CreateProduct() { return new ConcreteProductA(); }
}
// 具体工厂类 2
class ConcreteFactoryB : IFactory
{
public IProduct CreateProduct() { return new ConcreteProductB(); }
}
// 客户端代码
class Client
{
public void UseProduct(IFactory factory)
{
IProduct product = factory.CreateProduct();
product.DoSomething();
}
}
// 使用示例
Client client = new Client();
client.UseProduct(new ConcreteFactoryA()); // Product A
client.UseProduct(new ConcreteFactoryB()); // Product B
(典型的工厂模式)
// 产品接口 (同上)
interface IProduct
{
void DoSomething();
}
// 具体产品类 (同上)
class ConcreteProductA : IProduct
{
public void DoSomething() { Console.WriteLine("Product A"); }
}
class ConcreteProductB : IProduct
{
public void DoSomething() { Console.WriteLine("Product B"); }
}
// 工厂委托 (返回产品接口,无参数)
delegate IProduct ProductFactory();
// 工厂字典
class ProductFactoryDictionary
{
private Dictionary<string, ProductFactory> factories = new Dictionary<string, ProductFactory>();
public void RegisterFactory(string productName, ProductFactory factory)
{
factories.Add(productName, factory);
}
public IProduct CreateProduct(string productName)
{
if (factories.ContainsKey(productName))
{
return factories[productName]();
}
return null; // 或者异常处理
}
}
// 客户端代码
class Client
{
public void UseProduct(ProductFactoryDictionary factoryDict, string productName)
{
IProduct product = factoryDict.CreateProduct(productName);
if (product != null)
{
product.DoSomething();
}
}
}
// 使用示例 (使用 Lambda 表达式注册工厂)
ProductFactoryDictionary factoryDict = new ProductFactoryDictionary();
factoryDict.RegisterFactory("A", () => new ConcreteProductA()); // 使用 Lambda 表达式注册工厂
factoryDict.RegisterFactory("B", () => new ConcreteProductB()); // 使用 Lambda 表达式注册工厂
Client client = new Client();
client.UseProduct(factoryDict, "A"); // Product A
client.UseProduct(factoryDict, "B"); // Product B
使用Lambda表达式和字典实现
使用字典可以在运行时动态注册和管理工厂,便于从配置文件或外部数据源读取工厂信息。
可以说变得更加灵活了。
看到这里,有人可能会问:
“你是想说使用Lambda表达式的方法就是正确答案吗?”
不。
使用Lambda表达式本质上是在传统的OOP中临时引入FP(函数式编程)语法糖。
有些人可能会说应该使用FP,
但这只是表明除了OOP之外,还可以使用其他“范式”来解决问题。
但这并不是我想表达的本质。
使用Lambda表达式也会带来另一个问题:
“容易破坏团队代码的一致性。”
在编写代码时,我们通常遵循软件规则。
这意味着我们要遵循编码规范和代码指南,而团队代码的基本原则是
“看起来像一个人写的”才是正确编写的代码。
但如果开始使用Lambda表达式,为了保持代码一致性,其余部分也需要改用Lambda表达式,这就带来了问题。
此外,FP并不总是正确的答案。实际上,FP会增加内存使用量,带来不必要的开销。
(当然,在异步编程中使用FP是防止数据竞争的好方法,但这不是我们现在讨论的重点。)
实际上,在OOP中,一些模式也在进化为更好的模式。
例如,曾经使用单例模式(Singleton Pattern)的部分场景被服务定位器模式(Service Locator Pattern)取代。
(不过,单例模式和服务定位器模式在某些情况下都被视为反模式,因此依赖注入的方式也被广泛采用。)
还有,建造者模式(Builder Pattern)也出现了Fluent Builder这样的变体。
这些都是通过单纯记忆模式无法得出的改进。
因此,我想说的是:
“始终要在团队约定下追求更好的答案。”
在学习模式的过程中,沉迷于模式可能是不可避免的。
事实上,我们大多数人都是通过错误来学习的。我也曾多次沉迷于模式。
模式真正的乐趣在于明智地使用它们。
重构通过消除重复、简化代码以及清晰传达代码意图,帮助我们做到这一点。
当模式通过重构自然融入系统时,过度设计的风险就会降低。
随着重构能力的提高,发现模式真正乐趣的可能性也会增加。
模式的真正乐趣在于明智地使用它们。
大家在使用模式之前,不妨再思考一下?
也许你会发现属于自己的新方法。
虽然这只是我的个人观点,
但我认为学习与实践可以提高人生的分辨率。
随着新知识的积累,我们会逐渐认识周围的事物,
并对它们的意义有更深的理解。
我们的未来充满了不确定,但如果回头看时,风景以高分辨率呈现且美丽,那便已足够有趣,不是吗?
就此结束这篇文章。