设计模式的意义
设计模式是软件设计中常见问题的典型解决方案,是前人经验的总结。设计模式不是代码,而是解决特定问题的通用思路。使用设计模式可以提高代码的可读性、可维护性、可扩展性。设计模式让开发者之间有共同的语言,提高沟通效率。但不要过度使用设计模式,要根据实际需求选择合适的模式。
《设计模式:可复用面向对象软件的基础》(Gang of Four,GoF)一书总结了23种设计模式,分为三类:创建型模式(关注对象创建)、结构型模式(关注对象组合)、行为型模式(关注对象交互)。设计模式基于面向对象的原则,如封装、继承、多态。理解设计原则比记住具体模式更重要。
SOLID原则
单一职责原则(Single Responsibility Principle):一个类应该只有一个引起它变化的原因。类的职责应该单一,只做一件事。这样可以降低类的复杂度,提高可读性和可维护性。如果一个类承担了太多职责,修改一个职责可能影响其他职责。
开闭原则(Open-Closed Principle):软件实体应该对扩展开放,对修改关闭。当需求变化时,应该通过扩展来实现新功能,而不是修改现有代码。这样可以降低引入bug的风险。实现开闭原则的关键是抽象,通过接口或抽象类定义稳定的抽象层。
里氏替换原则(Liskov Substitution Principle):子类应该能够替换父类并出现在父类能够出现的任何地方。子类可以扩展父类的功能,但不能改变父类原有的功能。违反里氏替换原则会导致继承体系混乱,使用多态时出现意外行为。
接口隔离原则(Interface Segregation Principle):客户端不应该依赖它不需要的接口。一个类对另一个类的依赖应该建立在最小的接口上。接口应该小而专一,不要设计臃肿的接口。这样可以降低耦合度,提高灵活性。
依赖倒置原则(Dependency Inversion Principle):高层模块不应该依赖低层模块,两者都应该依赖抽象。抽象不应该依赖细节,细节应该依赖抽象。依赖倒置的核心是面向接口编程,而不是面向实现编程。这样可以降低模块间的耦合度。
创建型模式
单例模式(Singleton)确保一个类只有一个实例,并提供全局访问点。单例模式适用于需要全局唯一对象的场景,如配置管理器、日志记录器、数据库连接池。实现单例模式要注意线程安全,可以使用懒汉式(延迟初始化)或饿汉式(提前初始化)。JavaScript中可以使用闭包或模块实现单例。
工厂模式(Factory)定义一个创建对象的接口,让子类决定实例化哪个类。工厂模式将对象的创建和使用分离,客户端不需要知道具体类名。简单工厂使用一个工厂类创建对象,工厂方法让子类决定创建哪个对象,抽象工厂创建一系列相关对象。工厂模式提高了代码的灵活性和可扩展性。
建造者模式(Builder)将复杂对象的构建与表示分离,使得同样的构建过程可以创建不同的表示。建造者模式适用于对象有很多可选参数的场景,避免构造函数参数过多。建造者模式使用链式调用,代码更易读。JavaScript中可以使用对象字面量或类实现建造者模式。
原型模式(Prototype)通过复制现有对象来创建新对象。原型模式适用于创建对象成本较高的场景,如需要大量计算或网络请求。JavaScript天生支持原型模式,每个对象都有原型。使用Object.create()或Object.assign()可以实现对象克隆。注意深拷贝和浅拷贝的区别。
结构型模式
适配器模式(Adapter)将一个类的接口转换成客户端期望的另一个接口。适配器模式让原本接口不兼容的类可以一起工作。适配器模式分为类适配器(使用继承)和对象适配器(使用组合)。适配器模式常用于集成第三方库、兼容旧代码。
装饰器模式(Decorator)动态地给对象添加额外的职责。装饰器模式比继承更灵活,可以在运行时添加或删除功能。装饰器模式遵循开闭原则,不修改原有代码就能扩展功能。JavaScript中可以使用高阶函数或ES7装饰器语法实现装饰器模式。装饰器模式常用于日志、权限检查、缓存等场景。
代理模式(Proxy)为其他对象提供一个代理以控制对这个对象的访问。代理模式可以在不改变原对象的情况下,添加额外的功能。代理模式分为:远程代理(访问远程对象)、虚拟代理(延迟创建开销大的对象)、保护代理(控制访问权限)、缓存代理(缓存结果)。ES6的Proxy对象提供了原生的代理支持。
外观模式(Facade)为子系统提供一个统一的接口。外观模式隐藏了子系统的复杂性,让客户端更容易使用。外观模式常用于简化复杂的API、提供高层接口。外观模式不是封装子系统,客户端仍然可以直接访问子系统。外观模式降低了客户端与子系统的耦合度。
行为型模式
观察者模式(Observer)定义对象间的一对多依赖关系,当一个对象状态改变时,所有依赖它的对象都会收到通知。观察者模式实现了发布-订阅机制,降低了对象间的耦合度。观察者模式广泛应用于事件处理、数据绑定、消息队列。JavaScript的事件监听就是观察者模式的应用。
策略模式(Strategy)定义一系列算法,把它们封装起来,并使它们可以互相替换。策略模式让算法的变化独立于使用算法的客户端。策略模式避免了大量的if-else或switch语句。策略模式常用于表单验证、排序算法、支付方式选择等场景。策略模式遵循开闭原则,添加新策略不需要修改现有代码。
命令模式(Command)将请求封装成对象,从而可以用不同的请求对客户端进行参数化。命令模式将请求的发送者和接收者解耦。命令模式支持撤销、重做、日志记录、事务等功能。命令模式常用于菜单操作、宏命令、任务队列。命令模式让请求可以排队、记录、撤销。
迭代器模式(Iterator)提供一种方法顺序访问聚合对象的元素,而不暴露其内部表示。迭代器模式将遍历算法从聚合对象中分离出来。JavaScript的for...of、Array.prototype.forEach()、Generator都是迭代器模式的应用。ES6的Iterator接口和Symbol.iterator提供了标准的迭代器实现。
架构模式
MVC(Model-View-Controller)是最经典的架构模式。Model负责数据和业务逻辑,View负责展示,Controller负责处理用户输入和协调Model和View。MVC实现了关注点分离,提高了代码的可维护性和可测试性。MVC的变体包括MVP(Model-View-Presenter)、MVVM(Model-View-ViewModel)。
分层架构将系统分为多个层次,每层只依赖下层,不依赖上层。常见的分层:表示层(UI)、业务逻辑层(Service)、数据访问层(DAO)。分层架构的优点是关注点分离、易于维护、易于测试。缺点是可能导致性能损失、过度设计。分层架构适合传统的企业应用。
微服务架构将应用拆分为多个小型服务,每个服务独立部署、独立扩展。微服务架构提高了系统的可扩展性、可维护性、技术灵活性。但微服务也带来了分布式系统的复杂性,如服务间通信、数据一致性、运维复杂度。微服务适合大型、复杂、快速变化的系统。
设计原则
组合优于继承:优先使用对象组合而不是类继承来实现代码复用。继承是白盒复用,子类可以看到父类的实现细节,耦合度高。组合是黑盒复用,只依赖接口,耦合度低。组合更灵活,可以在运行时改变行为。但继承也有其优势,如代码简洁、类型关系清晰。
针对接口编程,而不是针对实现编程:依赖抽象而不是具体类。这样可以降低耦合度,提高灵活性。使用接口或抽象类定义契约,具体实现可以随时替换。这是依赖倒置原则的体现。
封装变化:找出应用中可能需要变化的部分,把它们独立出来,不要和不需要变化的代码混在一起。将变化封装起来,可以在不影响其他部分的情况下修改或扩展。这是开闭原则的实现方式。
高内聚、低耦合:模块内部的元素应该紧密相关(高内聚),模块之间的依赖应该尽量少(低耦合)。高内聚让模块更易理解和维护,低耦合让模块更独立、更易复用。这是软件设计的核心目标。
反模式
反模式是看似合理但实际上会导致问题的设计。常见的反模式:上帝对象(God Object,一个类做太多事)、意大利面条代码(Spaghetti Code,代码结构混乱)、复制粘贴编程(Copy-Paste Programming,重复代码)、过早优化(Premature Optimization,在不必要时优化)、金锤子(Golden Hammer,过度使用某个技术)。
识别和避免反模式很重要。反模式通常是由于缺乏经验、时间压力、需求变化等原因产生的。重构是消除反模式的主要手段。定期进行代码审查、遵循编码规范、使用静态分析工具可以帮助发现反模式。学习反模式可以让我们避免犯同样的错误。