翻译自《10 object oriented design patterns every programmer should learn》
大家好,如果你是一个程序员,那么你一定听过OOP(面向对象)设计模式。面向对象设计模式,对通用的软件设计问题,提供了一系列可信的解决方案。通过学习这些模式,程序员可以写出灵活性、可维护、可重用的代码。
过去,我已经讨论过几个微服务设计模式,例如CQRS,SAGA,包括每个微服务一个数据库,但是面向对象设计模式和它们并不一样。本文中,我们将会研究十个极其重要的面向对象设计模式,每一个程序员都应该学习。理解并应用这些设计模式,将有助于提高你的编码技巧,同时帮助你构建健壮且易扩展的软件系统。
十个程序员必须学会的面向对象设计模式
尽管当前有着超过23中面向对象设计模式,我这里重点介绍其中最有用的十种。基于我个人的经验,这是种是最重要的而且必须要理解的。
1. 单例模式——Singleton Pattern
单例模式保证一个类只能有一个实例被创建,而且提供一个全局的访问方式。当你有一个独一无二的共享资源需要全局共享的时候,单例模式很有用。
单例模式的优点
- 单例:单例保证在给定时间里,一个类只有一个实例存在。当你有一个对象需要贯穿整个系统存在的时候,这很重要。
- 全局访问:既然只存在一个实例,那么提供一个全局访问点就理所当然了。这允许其它对象或者组件更容易使用这个单例对象;
- 资源管理:单例可以用来管理贡献资源,注入数据库连接或者线程池。它保证了资源的合理共享和实用性。
- 懒加载:单例对象可以懒加载,这意味着初始化可以推迟到第一次使用的时候。仅仅在必须的时候,才初始化单例对象,可以显著提高性能。
单例模式的缺点:
- 全局状态:既然单例对象可以全局访问,那么就必然会引入全局状态的共享。这可能会导致代码难以理解和测试,同时会因为状态改变导致预料之外的影响;
- 强耦合:单例的使用会将不同代码块强耦合到一起。这会导致很难无感修改关联代码。
- 测试困难:单例对象很难独立测试,因为它们总是依赖着内部状态或者其它的全局依赖。在测试单例对象时,单元测试会变得复杂已经更不可靠;
- 多线程问题:当多个线程同时访问一个单例对象时,可能会引入同步问题以及潜在的竞争问题。需要使用合适的同步技术,以保证线程安全。
从好的方面来说,我们应该了解这个设计模式,因为有很多应用的流行API,以及常见库例如
java.lang.Runtime
都用到了。但从另一方面来说,在现代软件开发中我们应该避免使用这个模式,因为该模式会增加测试的难度,这也是为什么这个模式现在会被认为是反模式。
下图为单例的UML图:
2. 工厂模式——Factory Pattern:
工厂模式通过提供创建不同对象接口的方式,实现了对对象创建过程的封装。它促进了松耦合,同时增强了代码灵活性以及扩展性。
工厂模式的优点:
- 封装和抽象:工厂模式封装了对象创建过程,对客户端代码隐藏了对象创建的复杂性。还提供了一个清晰的抽象创建对象的接口隔离了实现世界。
- 灵活性对象创建:工厂模式允许灵活对象创建,通过使用工厂方法或者工厂类,你可以轻松改变对象创建,引入新的类型的对象而不需要修改客户端代码;
- 解耦:工厂模式将客户端代码从特定类实例的创建中解放出来。客户端代码只需要知道通用的接口或者基类,而不需要知道具体的实现类;
- 代码组织和维护:通过集中对象创建逻辑到工厂中,代码库的组织更加具有逻辑性,更容易维护。修改和更新对象创建代码的操作被限制在了工厂里,而不是在代码库里到处修改;
工厂模式的缺点:
- 增加复杂性:实现工厂模式会引入额外的复杂性,尤其是需要处理多个类型的对象和工厂实例。这可能会使代码更加难以理解和维护;
- 依赖于工厂:使用工厂模式的客户端代码都依赖着工厂类或者工厂方法。这导致了工厂类或者工厂实例与客户端代码耦合在了一起,很难进行替换;
- 运行时依赖:工厂模式运行时通常要求客户端代码依赖于工厂。这会限制代码的灵活性,使之更难在不同的工厂之间切换;
- 降低了对于代码创建的控制:工厂模式中,对象创建被分配给了工厂完成。这意味着客户端代码对于特定的实例化过程很难施加控制,必须依赖于工厂来完成。
下图为工厂模式的UML图:
3. 观察者模式——Observer Patternl:
观察者与被观察者是1:n的关系,会对被观察者的变动产生反应。允许高效率的事件掌控,以及接口观察对象和被观察对象。
观察者模式的优点:
- 松耦合:观察者模式推荐了观察者和被观察者之间的松耦合。被观察者不需要知道观察者的全部信息,反之亦然。代码可以更好模块化和更具有灵活性,系统也可以无感添加或者移除对象,不影响其他组件;
- 扩展性:观察者模式可以轻松添加新的观察者,而不需要修改现有代码。观察者们可以独立开发并添加到系统中,实际上是提供了一种无需修改被观察者或者观察者即可增强函数功能的方式;
- 事件驱动架构:对于事件驱动架构,可以成套使用观察者模式。它允许对象对于一个解耦行为产生的状态或者时间进行响应。当被观察者的状态改变时,所有注册过的观察者会自动被通知,并采取合适的行为;
- 复用性:观察者可以跨目标或系统进行重用。因为观察者和被观察者是解耦的,所以观察者可以被用在不同的被观察者上,这种行为可以显著提高复用性。
观察者模式的缺点:
- 性能瓶颈:观察者模式会引入一些性能瓶颈,尤其是被观察数量众多或者状态改变频繁。通知和更新所有的观察者是会消耗时间的,这可能会导致全局系统性能收到影响;
- 状态不一致问题:在使用观察者模式的时候,需要特别保证状态一致性。如果观察者依赖于彼此的状态或者通知顺序,那么管理系统的一致性将会面临挑战。
- 调试和测试复杂性:天然解耦的特性,会导致调试和测试观察者模式变得困难和复杂。当多个观察者同时工作并且彼此产生了交互,那么跟踪问题和验证行为的正确定就变得非常困难;
- 内存管理:一定要关注观察者的生命周期,尤其是那些长期运行的实例。如果没有在观察者生命周期结束后取消注册,就容易导致内存泄漏以及内存不足的问题。
以下为观察者模式的UML图:
4. 策略模式——Strategy Pattern:
测录模式定义了一个算法族,将它们进行封装以便于交换。它允许在运行时动态选择和切换算法,提升了代码的灵活性和可重用性。
策略模式的优点:
- 灵活性以及扩展性:通过允许灵活增删改策略而不影响客户端代码,策略模式提升了代码的灵活性。新的策略可以独立引入,以适应系统的新行为;
- 封装和抽象:策略模将算法封装进入不同的类里,提高了代码组织性及模块化水平。每一个策略类都关注着一个特定的策略,同时提供一个清晰的接口将实现与客户端隔离开来;
- 提高代码可重用性:策略们可以被系统中不同上下文使用。因为策略经过封装与客户端代码产生了解耦,策略可以在不同的场景中共享,避免了代码耦合;
- 运行时算法可选:策略模式允许运行时动态选择合适的策略。系统可以根据具体的场景和条件选择适宜的策略。
策略模式的缺点:
- 增加复杂性:引入策略模式模式会引入额外的复杂性。多个策略类的工作需要额外进行管理以保持同步,对于小型项目来说这是一个较大的开销;
- 额外的开销:策略模式引入了一个额外的层级,客户端代码需要通过一个公共的接口来使用策略。这可能会导致轻微的性能问题,但是在大多数项目中可以忽略不计;
- 对象创建和内存消耗问题:初始化多个策略,可能会引入多个对象以及它们相应的依赖,造成过大的开销。需要小心且有效管理策略对象的生命周期。
- 增加类的数量:由于给每个策略创建了一个策略类,所以会导致引入了很多新的对象。如果代码组织混乱,会影响代码管理。
策略模式UML图如下所示:
5. 装饰器模式——Decorator Pattern
装饰器模式允许你动态给一个对象的函数添加新的行为。该模式符合开闭原则,允许在不修改原始对象结构的情况下增强对象功能。
装饰器模式的优点:
- 弹性扩展:装饰模式允许在运行时动态灵活扩展。通过将一个对象包装进一个或者多个装饰器接口达成目标,而不需要修改原始代码。这提高了代码的重用性和扩展性;
- 符合开闭原则:装饰器模式符合开闭原则,允许在不修改现有代码的情况下增强功能。可以在不影响系统现有功能的情况下,添加新的特性或者变量。
- 使用组合而不是继承:装饰器模式倾向于使用组合而不是继承实现,使得代码灵活性和扩展性更好。同时可以在运行时动态添加或删除,比传统的子类方式更加灵活;
- 细粒度控制:装饰器模式可以用一种更加优雅的方式添加或删除行为,同时行为的控制也更精细化。不同装饰器的组合可以实现不同的功能,提供了更加灵活以及可定制化的选择。
装饰器模式的缺点:
- 增加复杂度:装饰器模式引入了额外的复杂性,同时管理了多个类已经不同层级的封装。这可能会使得代码难以理解和维护,装饰器越多,难度越高;
- 潜在的性能影响:每个装饰器都引入了额外的包装层级,可能会导致轻微的性能损耗。通常来说这种损耗微乎其微,但是随着装饰器数量的显著增加,影响依然不容小觑;
- 顺序依赖:使用装饰器的顺序非常重要,因为他们总是依赖其它装饰器。如果没有正确管理装饰器的顺序,可能会导致未知错误。
- 对象标识混乱:装饰器通常改变了包装原始对象以扩展他们的行为,但这种表示会改变对象标识。这会潜在地影响依赖于对象标识或者需要对象标识比较的代码。
装饰器模式UML图如下:
6. 适配器模式——Adapter Pattern
适配器模式允许两个接口不兼容的类共同工作,方法就是完成接口之间的转换关系。构造了两个类之间的桥梁,提高了重用性。
适配器模式的优点:
- 接口兼容:适配器模式可以通过提供一个公共的接口,来让两个不兼容接口类共同工作。它使得不同接口的两个类可以通信和交互,提高了重用性;
- 无缝衔接:适配器模式允许新的代码无缝先街道已经存在的系统或者代码中。可以在不修改已有代码的情况了,完成对于新的类或者组建的适应,减少了对于整个系统的影响;
- 弹性和扩展性:适配器模式是的系统可以轻松扩展或者集成新的类。可以在不影响核心功能的情况下,插拔式加入新的功能。
- 简化客户端代码:给客户端提供了简单的接口,将潜在的不兼容代码,抽象成了兼容的接口。客户端代码可以使用统一且熟悉的接口,这可以提高代码可维护性和可读性;
适配器模式的缺点:
- 增加复杂性:适配器模式引入了新的复杂性,尤其是在处理较多或者复杂接口时。对于小型系统,适配器的代码会影响代码的可读性和可维护性。
- 性能损耗:适配器模式引入了新的封装层级。适配和翻译接口间的数据会影响系统的性能,但是需要注意的是,实际影响取决于具体实现以及性能需求;
- 潜在的设计难题:如果不曾小心设计,适配器模式会导致代码继承或臃肿或复杂。对于不同版本不同功能的类的适配器构建,会导致适配器类的数量膨胀,这也是一个难题;
- 依赖于适配器:客户端代码在使用适配器的时候,依赖于或者说耦合于适配器代码。修改适配器代码便需要修改客户端代码,这可能会影响整个系统的可维护性。
以下是适配器的UML图:
7. 组合模式——Composite Pattern
组合模式对于不同的对象和对象组一视同仁,可以创建复杂树形的结构。简化了继承结构中的管理和转换。
组合模式的优点:
- 灵活且统一的访问:组合模式提供了一个统一的访问不同对象和对象组的方式。客户端对于对象或者对象组的处理一视同仁,这大大简化了客户端但代码,同时增强了灵活性。
- 简化客户端代码:通过简化对象复杂结构来简化客户端代码。客户端代码不需要知道代码或者代码组的不同之处,只需要统一的使用就可以,这使得代码可读性更高,维护性更好;
- 递归行为:组合模式允许对象结构上的递归行为。对于独立对象或者独立对象组的递归访问,是一种强力且灵活的行为。
- 动态结构:组合模式允许动态修改对象结构。可以在运行时动态添加或者删除对象,提升了系统的扩展性;
组合模式的缺点:
- 增加复杂性:组合模式引入了额外的复杂性。对于小型系统,组合模式的代码会影响代码的可读性和可维护性。
- 性能考量:组合模式天然的可递归属性可能会影响性能。当对象结构庞大且深度嵌套,那么对于对象结构的递归会带来不小的性能开销;
- 限制编译器检查错误:组合模式会限制编译器编译时的类型检查。因为独立的对象和对象组是被一视同仁的,有可能会执行一个不可用或者不支持的操作。这可能会导致运行时错误或者不一致;
- 潜在的继承类膨胀问题:如果对象结构复杂且包含了很多层级,那么代表了不同类层级的组合类继承可能会使得对象结构变得臃肿。这回影星代码的组织性和维护性,尤其是架构演进和需求修改的时候。
以下是组合模式的UML图:
8. 迭代器模式——Iterator Pattern
迭代器模式提供了一个标准的访问集合类内部元素的方式,不需要暴露集合自己内部实现。简化并且解耦了复杂对象结构的遍历。
迭代器模式的优点:
- 简化客户端代码:因为提供了统一的标准化迭代访问集合内部元素的方式。使用同一个接口迭代访问了不同种集合或者说聚合对象,将具体实现抽象成一个统一接口。
- 封装了迭代逻辑:迭代器模式使用迭代器对象封装了迭代逻辑,使之与聚合对象分开来。使迭代功能不在和聚合对象耦合在一起;
- 支持多种多样的迭代方式:迭代器模式支持对同一对象的多种多样的迭代方式,这些方式可以并行使用。每个迭代器维护了自己的迭代状态,可以独立进行遍历和迭代。
- 灵活性和扩展性:通过允许对同一个对象的不同迭代器的实现,迭代器模式提高了代码的灵活性和扩展性。新的迭代器类型可以在不修改现有代码的情况下加入。
迭代器模式的缺点:
- 增加复杂性:引入了新的复杂性。对于小型系统,组合模式的代码会影响代码的可读性和可维护性。
- 性能损耗:因为引入了抽象层,所以存在额外的性能损耗。迭代器需要维护自己的状态,同时执行迭代操作,相对于直接操作聚合对象会带来性能损耗。
- 限制函数功能:迭代器主要关注线性访问元素数据。一般不提供其他行为,不能满足例如过滤或者排序这种需求。需要使用更多的模式来支持迭代器模式增加新的行为。
- 依赖于聚合对象:迭代器模式强依赖于它聚合的对象。聚合对象的任何一点改动,都有可能需要迭代器对象进行修改,这显然印象了真个系统的可维护性。
迭代器模式的UML图如下:
9. 模板方法模式——Template Method Pattern
模板方法在基类中顶一个一个算法的框架,子类可以覆盖这些方法的具体实现来实现对于算法的修改,但算法执行的步骤不会改变。对于算法设计来说,大大促进了代码弹性和复用性。
模板方法模式的优点:
- 代码重用和一致性:模板方法定了一套公共算法步骤在基类中。这提升了代码复用性,子类只需要去修改特定实现,可以复用父类的其它代码,修改并不影响代码执行步骤。这种方式,保证了不同的算法实现使用同一种执行步骤。
- 封装和抽象:模板方法模式在基类中封装了代码实现,客户端代码不需要了解细节即可使用。进行了关注点分离,允许客户端代码只关注高级别交互,将低级别的代码实现细节留给子类。
- 灵活性和扩展性:模板模式方法通过允许子类重写算法中的步骤来实现灵活性。使得在不修改全局算法结构的基础上,可以对代码进行变动和定制。新子类的添加可以增加系统的扩展性;
- 推动最佳实践和指导:模板方法模式鼓励坚持最佳实践和指导,通过强制算法的标准结构实现。这可以提高代码的质量、可维护性以及可读性,因为开发真知道了怎么去修改算法的实现细节。
模板方法的缺点:
- 限制运行时灵活性:模板方法在编译器就确定了算法的结构,限制了运行时灵活性。在需要根据请求或者场景进行灵活更改的时候,就有些水土不服了;
- 增加复杂性:在需要处理复杂算法或者多个子类时,引入了额外的复杂性。对于小系统来讲,基类和子类的交互会显著增加理解和维护的难度;
- 控制反转:模板方法模式要求子类坚持由基类所定义的结构。这回限制子类对于算法的控制,使得子类很难去对算法进行变更;
- 限制了每个独立步骤的重用性:模板方法模式推进了整个算法的重用性,但同时也限制了每个渎职步骤或者说方法的重用性。子类通常需要实现每一个父类方法,如果其他子类中也用到了同样的实现,通常只能通过复制解决。
一下为模板方法模式的UML图:
10. 代理模式——Proxy Pattern
代理模式提供了对一个对象的代理或者占位符访问的方法。它可以被用于达成很多目标,例如懒加载,远程通信,或者访问控制。
代理模式的优点:
- 访问控制:代理模式可以对一个真实对象进行访问控制。它可以强制进行访问控制,执行额外的鉴权或者安全检查,或者提供一个受到限制的接口供客户端使用,以此来加强真实对象的安全访问。
- 通过抽象降低复杂性:代理模式可以将真实对象的访问控制抽象出来。客户端可以通过一个简化的接口来控制注入缓存、懒加载以及网络通信等任务。这种关注点分离的情况可以提升代码组织性。
- 性能提升:代理可以通过缓存机制提升性能。可以缓存代价昂贵的操作结果,然后在后续的请求中直接返回,降低重复访问真实对象的开销;
- 远程对象访问:代理对象可以作为一个远程对象的代理,允许客户端访问和交互不同进程或者不同系统。这使得代理模式可以被用在分布式应用以及远程通信上。
代理模式的缺点:
- 增加复杂性:道理模式会引入额外的复杂性。对于小的简单的系统来说,代理的出现需要额外的管理代理和真实对象通信的投入,而且代码会变得更加难理解和维护;
- 性能问题:因为多一个层级,所以会影响性能。客户端、代理、真实对象之间的通信,都会增加延迟并且消耗额外的系统资源,影响性能,尤其是在频繁的交互请求时影响更明显;
- 强耦合:代理会和被代理对象强耦合。真实对象的的接口或者和行为发生改变,要求代理对象也要随之进行更改,这显然影响了系统的可维护性;
- 额外代码以及实现的复杂性:实现代理需要额外的代码以及基础设施,来控制代理对象交互、鉴权、缓存或者其它的函数功能。这回增加系统的复杂性,同时在开发维护阶段要求额外的投入;
代理模式的UML图
总结
以上就是所有是个程序员必须学会的面向对象设计模式的内容了。通过熟悉这是种面向对象设计模式,你可以提升自己的编程技巧,并逐步成长为一个效率更高更有影响力的 软件开发者。
这些模式提供了一个对于当前的设计难题可信的解决方案,它可以提升代码复用、灵活性以及可维护性。当你在自己的项目中使用这些模式是,你会对此有更加深刻的了解。
拥抱设计模式不仅仅可以增强你的编程能力,同时也会让你能够写出结构更好、更具有扩展性同时更具有维护性的代码。
同样的,这些知识也是Java面试中常考的,可以预见,任何一场Java的面试中你都会面对几个这样的问题。
顺便说一下,这些只是程序设计模式的冰山一角,你还有很长的路要走。