一、设计模式的分类与设计原则
设计模式总体分为三大类:创建型模式(5种)、结构型模式(7种)和行为模式(11种)。在深入了解各种设计模式之前,先明确一下设计所遵循的原则,主要包括单一职责、开放封闭原则 - OCP(功能开放,代码闭合)、里氏替换原则(子类型能够替换掉父类型,松耦合)以及接口隔离原则 - ISP(不能出现胖接口,应该打散dao,多定义子接口)。这些原则为设计模式的合理运用提供了指导方针,确保软件系统具备良好的可维护性、扩展性和灵活性。
(一)创建型模式
单例模式(Singleton)
特征描述:
构造函数必须声明为私有,防止外部直接实例化该类。
类中声明静态常量,用于保存类的惟一实例。
为客户提供一个静态的方法,该方法能够返回惟一实例。
应用场景:
当类只能有一个实例且客户可从一个众所周知的访问点访问它时,例如在一些全局配置类中,整个系统只需要一个配置实例来保证配置的一致性。
当这个惟一实例应通过子类化可扩展,且客户无需更改代码就能使用扩展实例时,如在数据库连接池的实现中,单例模式可以确保在整个应用中只有一个连接池实例,同时可以根据不同的数据库类型进行子类扩展。
伪单例模式:
线程单例:保证在一个线程中只有一个实例,在Java中可通过ThreadLocal对象实现。在Hibernate中,Session的使用就运用了线程单例,确保每个线程都有自己独立的Session实例,避免线程安全问题。在自定义应用中,对于那些没有实现线程安全但又必须保证线程安全的对象,可设置为线程单例。通过线程同步实现线程安全,这样做的好处是可以提高程序在多线程环境下的稳定性和可靠性。
类加载器单例:保证在一个类加载器中只有一个类的实例。在BeanUtils中,与Utils相对应的Bean采用了类加载器单例。在自定义应用中,可将需要在应用间相互隔离但又必须使用单例的对象设置成类加载器单例,常用于隔离WEB应用程序之间的单例。其具体作法是使用一个Map,键为类加载器引用,值存储对象。若能通过类加载器对象得到实例则不再创建,否则创建并存储。
相对特定上下文单例:其他相对特定上下文的伪单例模式均可采用类似方法,确保在特定的上下文环境中只有一个实例存在,满足不同场景下对单例的需求。
原型模式(Prototype)
特征描述:
实现原型模式的类必须由统一的接口或类继承而来,并且实现接口或类中定义的clone方法。
在clone方法中实现对象克隆,从而创建新的对象。
应用场景:
当要实例化的类在运行时刻指定时,例如通过动态装载,可利用原型模式快速创建对象。
为避免创建与产品类层次平行的工厂类层次时,原型模式提供了一种简洁的对象创建方式。
当一个类的实例只有几种不同状态组合中的一种时,建立相应数目的原型并克隆它们比每次用合适状态手工实例化该类更方便,比如在游戏开发中,不同状态的游戏角色可以通过原型模式快速创建。
工厂方法模式(factory method)
特征描述:
顶级类定义创建对象的抽象方法,将对象的创建延迟到子类。
具体子类实现该方法,返回具体产品。
可将工厂方法参数化,以满足不同的创建需求。
应用场景:
当一个类不知道它所必须创建的对象的类时,可使用工厂方法模式,让子类来决定创建何种对象。
当一个类希望由它的子类来指定所创建的对象时,例如在一个图形绘制系统中,父类定义了绘制图形的方法,但具体绘制哪种图形(如圆形、矩形等)由子类决定。
当类将创建对象的职责委托给多个帮助子类中的某一个,且希望将哪一个帮助子类是代理者这一信息局部化时,工厂方法模式可以很好地实现这种职责委托。
典型案例:
Collection的iterator方法返回Iterator可认为是工厂方法的一种典型应用,它为遍历集合提供了统一的方式,不同类型的集合可以返回相应类型的迭代器。
Hibernate中,SessionFactory.openSession是工厂方法模式的体现,根据不同的配置和需求创建合适的Session实例。
Spring中,BeanFactory.getBean和FactoryBean.getObject也是工厂方法模式的应用,用于获取不同类型的Bean实例。
抽象工厂模式(AbstractFactory)
特征描述:
定义抽象工厂,用于声明创建一系列相关或相互依赖对象的接口。
定义抽象产品系列,确定产品的类型和结构。
一个具体工厂对应一个具体产品系列,通过实现抽象工厂的方法来创建所需的产品对象。
可以使用工厂方法模式或原型模式实现(也可参数化工厂方法),并且通常工厂类应为单例模式,以保证整个系统中只有一个工厂实例来创建产品。
存在的问题是无法在已有产品系中增加新的产品,这是其局限性所在。
应用场景:
当一个系统要独立于它的产品的创建、组合和表示时,抽象工厂模式可以将产品的创建和使用分离,提高系统的灵活性和可维护性。
当一个系统要由多个产品系列中的一个来配置时,通过抽象工厂模式可以方便地切换不同的产品系列,满足不同的配置需求。
当强调一系列相关的产品对象的设计以便进行联合使用时,抽象工厂模式能够确保所创建的产品对象之间具有良好的兼容性和一致性。
当提供一个产品类库,只想显示它们的接口而不是实现时,抽象工厂模式可以隐藏产品的具体实现细节,只提供统一的接口供用户使用。
工厂模式实现:
为每一种产品声明一个工厂方法,在具体工厂类中实现这些方法来创建相应的产品对象。
必须为不同的产品系列定义相应的工厂,以确保能够创建出完整的产品系列。
原型模式实现:
使产品系列中的每一种产品都实现原型模式,即每种产品都可以通过原型复制来创建新的对象。
只要为工厂提供某类型产品系的原型,工厂就可以生产出相应产品系的产品,无需为每个产品系都定义相应工厂,简化了工厂的实现。
典型案例:
抽象工厂模式在Java世界中非常常见,如JMS中的抽象工厂模式用于创建消息发送和接收相关的对象,JNDI中的抽象工厂模式用于获取命名服务相关的对象,XML解析中的抽象工厂模式用于创建解析XML文档所需的各种对象。
抽象工厂与工厂方法的区别:
抽象工厂与工厂方法的联系大于区别,抽象工厂一般是通过工厂方法来实现的。抽象工厂模式侧重于创建一系列相关的产品对象,而工厂方法模式则侧重于让子类决定创建何种具体的产品对象。
Builder模式(生成器模式)
特征描述:将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。通过逐步构建对象的各个部分,最后得到完整的复杂对象。
典型案例:
XML解析中DocumentBuilder用于构建XML文档对象,它将构建XML文档的过程和最终的文档表示分离开来,用户可以根据自己的需求构建不同结构的XML文档。
BeanUtils中的ConverterUtils与Converter用于数据类型的转换,通过构建不同的转换规则来实现不同类型数据之间的转换。
Spring MVC中的ViewResolver可以看成是Builder模式的一种应用,它根据不同的视图名称和配置构建相应的视图对象,用于展示给用户。
(二)结构型模式
适配器模式(adapter)
特征描述:
通常由三个主要对象组成Target、Adaptee和Adapter,一般有对象模式和类模式两种。
Target需要调用Adaptee中的方法,但Target只能调用Adapter类型中定义的接口,Adapter起到了将Adaptee的接口转换为Target所期望接口的作用。
应用场景:
当想使用一个已经存在的类,而其接口不符合需求时,适配器模式可以将不兼容的接口转换为可用的接口,例如在将旧系统中的接口适配到新系统中时。
当想创建一个可复用的类,使其能与其他不相关或不可预见的类协同工作时,适配器模式可以解决接口不兼容的问题,提高类的复用性。
(仅适用于对象Adapter)当想使用一些已经存在的子类,但不可能对每一个都进行子类化以匹配它们的接口时,对象适配器可以适配其父类接口,方便地将这些子类集成到系统中。
桥模式(bridge)
特征描述:将抽象和接口分离开,使抽象部分与它的实现部分分离,两者都可以独立地变化。选择主系类,主系类中包含次系类的顶层类,在主系类的其他方法中调用次系类的方法作为实现。
应用场景:
当不希望在抽象和它的实现部分之间有固定的绑定关系时,桥模式可以使两者解耦,方便在运行时切换不同的实现。
当类的抽象以及它的实现都可以通过生成子类的方法加以扩充时,桥模式提供了一种灵活的架构,便于扩展抽象和实现部分。
对抽象的实现部分的修改应对客户不产生影响,即客户的代码不必重新编译,这提高了系统的可维护性。
(C++)当想对客户完全隐藏抽象的实现部分时,桥模式可以将实现细节封装在次系类中,只暴露抽象部分给客户。
当有许多类要生成,且类层次结构表明必须将一个对象分解成两个部分时(如Rumbaugh称的“嵌套的普化”),桥模式可以清晰地划分抽象和实现部分。
当想在多个对象间共享实现(可能使用引用计数),但同时要求客户并不知道这一点时,桥模式可以实现这种隐藏共享实现的需求。
典型应用:
JDBC中Connection与Driver的关系是桥模式的应用,Connection抽象了数据库连接的概念,而Driver则是具体的数据库驱动实现,两者通过桥模式解耦,使得可以方便地切换不同的数据库驱动。
Hibernate中Query与Dialect的关系也是桥模式的体现,Query提供了查询的抽象,Dialect则负责与具体数据库方言相关的实现,提高了Hibernate在不同数据库上的兼容性和灵活性。
组合模式(Composite)
特征描述:将对象组合成树形结构以表示“部分 - 整体”的层次结构,用户对单个对象和组合对象的使用具有一致性。需要组件和容器两类成员,实现的关键是容器本身应该是组件的子类。
应用场景:
当想表示对象的部分 - 整体层次结构时,例如在文件系统中,文件和文件夹可以组成树形结构,文件夹可以包含文件和其他文件夹,组合模式可以很好地描述这种结构。
当希望用户忽略组合对象与单个对象的不同,用户将统一地使用组合结构中的所有对象时,组合模式提供了一种简单一致的操作方式,无论是单个对象还是组合对象,用户都可以使用相同的方法进行操作。
安全式和透明式:
透明式要求在组件和容器中都定义操作子组件的方法,在这种情况下,叶子结点因为不含有子组件,所以有可能会出现问题,例如在叶子节点调用操作子组件的方法时可能会出现空指针异常等情况。
安全式则只在容器中定义操作子组件的方法,避免了叶子节点出现不必要的问题,但在使用时需要用户清楚对象的类型是组件还是容器。
典型应用:
AWT中的组件和容器,如Container和Component,使用了组合模式来构建用户界面的层次结构,方便管理和操作界面元素。
JUnit中的TestSuite和TestCase也运用了组合模式,TestSuite可以包含多个TestCase,用户可以像对待单个TestCase一样对待TestSuite,统一进行测试执行等操作。
装饰模式(Decorator)
特征描述:包含组件和装饰类两个构件,关键点在于装饰类一要从组件继承,二是装饰类要由组件组成。通过装饰类可以动态地给一个对象添加一些额外的职责。
应用场景:
在不影响其他对象的情况下,以动态、透明的方式给单个对象添加职责,例如在一个图形绘制系统中,可以动态地给图形对象添加边框、阴影等装饰效果。
处理那些可以撤消的职责,比如在一个权限管理系统中,可以临时给用户添加某些权限,之后又可以方便地撤消这些权限。
当不能采用生成子类的方法进行扩充时,一种情况是可能有大量独立的扩展,为支持每一种组合将产生大量的子类,使得子类数目呈爆炸性增长;另一种情况可能是因为类定义被隐藏,或类定义不能用于生成子类,此时装饰模式是一种很好的替代方案。
典型应用:
IO中的很多类都用到了Decorator,如BufferReader等,BufferReader对Reader进行了装饰,增加了缓冲功能,提高了读取效率。
外观模式(Facade)
特征描述:关键是为系统提供一个统一对外的接口,将子系统中的一组接口进行整合,形成一个简单易用的高层接口。
应用场景:
当要为一个复杂子系统提供一个简单接口时,外观模式可以隐藏子系统的复杂性,使外部用户能够更方便地使用子系统的功能,例如在一个电商系统中,订单处理、库存管理、支付等多个子系统可以通过外观模式提供一个统一的订单处理接口给前端应用。
当客户程序与抽象类的实现部分之间存在着很大的依赖性时,外观模式可以降低这种依赖性,使客户程序只依赖于外观接口,而不直接依赖于子系统的具体实现,提高系统的可维护性。
理和控制子系统的访问。例如在企业级应用中,不同的业务模块可能有各自复杂的内部结构,通过外观模式为每个业务模块定义一个统一的入口,便于系统的整体架构设计和维护。
典型应用:
Hibernate中的Session就是外观模式的体现,它为开发者提供了一个简单统一的接口来操作数据库,隐藏了底层复杂的数据库连接、事务管理、对象持久化等操作。开发者只需通过Session接口进行数据的增删改查等操作,而无需关心其背后复杂的实现细节。
代理模式(proxy)
应用场景:
远程代理(Remote Proxy):用于在本地代表远程对象,使得客户端可以像调用本地对象一样调用远程对象,隐藏了远程对象的分布式特性,例如在分布式系统中,客户端通过远程代理访问远程服务器上的对象。
虚代理(Virtual Proxy):根据需要创建开销很大的对象。当对象的创建成本较高(如创建一个大型数据库连接对象或复杂的图形对象)时,虚代理可以先返回一个占位符对象,等到真正需要使用对象时才创建实际对象,提高系统性能。
保护代理(Protection Proxy):控制对原始对象的访问,例如在权限管理系统中,保护代理可以根据用户的权限决定是否允许对某个对象进行访问或操作,增强系统的安全性。
智能指引(Smart Reference):取代了简单的指针,在访问对象时执行一些附加操作。典型用途包括对指向实际对象的引用计数,当该对象没有引用时自动释放它(也称为SmartPointers[Ede92]);当第一次引用一个持久对象时将它装入内存;在访问一个实际对象前检查是否已经锁定了它,以确保其他对象不能改变它。
典型应用:
EJB(Enterprise JavaBeans)中使用了代理模式,容器为EJB组件创建代理对象,负责管理EJB的生命周期、事务、安全等方面,客户端通过代理对象与EJB组件进行交互。
Hibernate延迟加载也是代理模式的应用,当加载一个包含关联对象的实体时,Hibernate不会立即加载关联对象,而是返回一个代理对象,只有当真正访问关联对象时才从数据库中加载实际数据,提高了数据查询的效率,减少了不必要的数据加载。
享元模式(Flyweight,也称蝇头模式)
特征描述:关键是不能直接创建对象,应该先到享元工厂中查找是否存在具有相同状态的对象,如果存在就不应该再创建了,而是直接使用已有的对象,通过共享技术有效地支持大量细粒度的对象,通常共享一个小的模块。
应用场景:
当一个应用程序使用了大量的对象,且完全由于使用大量的对象造成很大的存储开销时,享元模式可以通过共享对象来减少内存占用。例如在一个文字处理软件中,可能有大量的字符对象,如果每个字符都单独创建对象会占用大量内存,而使用享元模式可以共享相同字符的对象。
当对象的大多数状态都可变为外部状态,且如果删除对象的外部状态,可以用相对较少的共享对象取代很多组对象时,享元模式可以优化对象的存储和使用。例如在图形绘制中,对于颜色等可以作为外部状态的属性,通过共享形状对象来减少对象数量。
当应用程序不依赖于对象标识时,由于Flyweight对象可以被共享,对于概念上明显有别的对象,标识测试将返回真值,这使得享元模式可以在不影响程序逻辑的前提下共享对象。例如在一个游戏中,多个相同类型的敌人对象可能只需要共享一些基本的属性和行为,而它们在游戏中的位置等外部状态可以不同。
(三)行为模式
职责链模式(Chain of Responsibility)
特征描述:使多个对象都有机会处理请求,将这些对象连成一条链,并沿着这条链传递请求,直到有一个对象处理它为止。这种模式的最大好处是回避了switch case的使用,使得代码结构更加灵活和可扩展。
应用场景:
当有多个的对象可以处理一个请求,且哪个对象处理该请求在运行时刻自动确定时,职责链模式可以动态地选择合适的处理对象。例如在一个请假审批系统中,不同级别的领导可以组成一个职责链,请假请求根据请假天数等条件依次传递给不同级别的领导进行审批。
当想在不明确指定接收者的情况下,向多个对象中的一个提交一个请求时,职责链模式可以自动找到合适的处理对象,无需提前知道具体的接收者。
当可处理一个请求的对象集合应被动态指定时,职责链模式可以方便地添加或移除处理对象,适应系统的变化。例如在一个事件处理系统中,可以根据不同的事件类型动态地添加或改变处理事件的对象链。
典型应用:
Servlet中的Filter就是职责链模式的应用,多个Filter可以组成一个链,对请求进行预处理和后处理,如权限验证、日志记录等操作。
Struts2中的拦截器也是职责链模式的体现,拦截器链可以对请求进行一系列的处理,如参数校验、国际化处理等。
命令模式(Command)
特征描述:将一个请求封装为一个对象,从而可用不同的请求对客户进行参数化;对请求排队或记录请求日志,以及支持可撤消的操作。命令对象内部包含真正的执行者,命令可以单独处理,也可以拿到任何其他的地方再次运行,这保证了回退的可能。
应用场景:
当抽象出待执行的动作以参数化某对象时,Command模式是回调机制的一个面向对象的替代品。例如在一个图形编辑软件中,各种图形操作(如绘制直线、圆形等)可以封装成命令对象,方便进行参数化和管理。
当在不同的时刻指定、排列和执行请求,可将负责该请求的命令对象传送给另一个不同的进程并在那儿实现该请求时,命令模式可以方便地实现分布式系统中的任务调度和执行。
当需要支持取消操作时,命令模式可以记录操作的历史,方便进行回退操作。例如在文档编辑软件中,撤销和重做功能可以通过命令模式实现。
当需要支持修改日志,这样当系统崩溃时,这些修改可以被重做一遍时,命令模式可以记录每个命令的执行情况,用于系统恢复。
当用构建在原语操作上的高层操作构造一个系统时,命令模式可以将复杂的操作分解为多个简单的命令对象,便于系统的构建和维护。
典型应用:
JUnit中的TestCase可以看作是命令模式的应用,每个测试方法可以视为一个命令,JUnit框架负责执行这些命令并记录测试结果。
解释器模式(Interceptor)
特征描述:给定一个语言,定义它的文法的一种表示,并定义一个解释器,这个解释器使用该表示来解释语言中的句子。当有一个语言需要解释执行,并且可将该语言中的句子表示为一个抽象语法树时,可使用解释器模式。
应用场景:
当该文法简单时,解释器模式效果较好。对于复杂的文法,文法的类层次会变得庞大而无法管理,此时语法分析程序生成器等工具是更好的选择,它们无需构建抽象语法树即可解释表达式,能节省空间和时间。但即使在这种情况下,转换器仍可用解释器模式实现,该模式仍是有用的。例如在一些简单的配置文件解析、规则引擎等场景中,如果文法不太复杂,解释器模式可以方便地实现对特定语言的解释执行。
迭代器模式(Iterator)
特征描述:提供一种方法顺序访问一个聚合对象中各个元素,而又不需暴露该对象的内部表示。一般会在迭代器中包含集合的引用,在迭代方法中通过访问集合的相应方法实现对集合的迭代。
应用场景:
当访问一个聚合对象的内容而无需暴露它的内部表示时,迭代器模式可以提供一种安全的访问方式。例如在一个列表对象中,用户可以通过迭代器遍历列表元素,而无需了解列表的内部存储结构。
当支持对聚合对象的多种遍历时,迭代器模式可以定义不同类型的迭代器,如正向迭代、反向迭代等,满足不同的遍历需求。
当为遍历不同的聚合结构提供一个统一的接口(即支持多态迭代)时,迭代器模式使得代码更加通用和可复用。例如,无论是数组、链表还是其他集合类型,都可以使用相同的迭代器接口进行遍历。
中介者模式(Mediator)
特征描述:用一个中介对象来封装一系列的对象交互,中介者使各个对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。同事间互不相知,由中介者完成同事间交互,同事只知道中介者,即具有一个中介者属性。
应用场景:
当一组对象以定义良好但是复杂的方式进行通信,产生的相互依赖关系结构混乱且难以理解时,中介者模式可以将这些复杂的交互集中到中介者对象中,简化对象之间的关系。例如在一个多人游戏中,玩家之间的交互、游戏状态的更新等复杂操作可以通过中介者来协调。
当一个对象引用其他很多对象并且直接与这些对象通信,导致难以复用该对象时,中介者模式可以将对象之间的通信解耦,提高对象的复用性。
当想定制一个分布在多个类中的行为,而又不想生成太多的子类时,中介者模式可以在中介者对象中集中处理这些行为,避免在多个类中重复实现相同的行为逻辑。
典型应用:
MVC(Model - View - Controller)架构中的Controller可以看作是中介者模式的应用,它协调Model和View之间的交互,处理用户输入、更新模型数据并通知视图进行更新。
备忘录模式(Memento)
特征描述:在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。重点是将对象具备的状态提出到备忘录中,以使不暴露原有对象的结构信息。
应用场景:
当必须保存一个对象在某一个时刻的(部分)状态,这样以后需要时它才能恢复到先前的状态时,备忘录模式可以方便地实现状态的保存和恢复。例如在一个文本编辑器中,保存文档的历史版本状态,以便用户可以撤销操作回到之前的版本。
当如果用接口来让其他对象直接得到这些状态,将会暴露对象的实现细节并破坏对象的封装性时,备忘录模式通过备忘录对象来封装状态,保证了对象的封装性。
典型应用:
在Web开发中,ActionForm可能会使用备忘录模式来保存表单数据的状态,以便在页面回退或表单提交失败时恢复数据。
观察者模式(Observer)
特征描述:定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。观察者要向被观察者注册,当状态发生变化时,观察者要迭代向所有观察者发消息。
应用场景:
当必须保存一个对象在某一个时刻的(部分)状态,这样以后需要时它才能恢复到先前的状态时,观察者模式可以用于实现对象状态的监听和更新。例如在一个股票价格监控系统中,多个观察者(如投资者)关注股票价格(被观察者)的变化,当价格变化时,观察者会收到通知并进行相应的操作。
当如果用接口来让其他对象直接得到这些状态,将会暴露对象的实现细节并破坏对象的封装性时,观察者模式通过通知机制来传递状态变化,而不直接暴露对象的内部状态。
典型应用:
监听器是观察者模式的常见应用,如在Java图形用户界面中,按钮点击事件的监听器,当按钮被点击(状态改变)时,注册的监听器会收到通知并执行相应的操作。
状态模式(State)
特征描述:允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类。将行为提取到状态中,对象行为通过状态实现。
应用场景:
当一个对象的行为取决于它的状态,并且它必须在运行时刻根据状态改变它的行为时,状态模式可以将不同状态下的行为封装到不同的状态类中,使代码结构更加清晰。例如在一个电梯控制系统中,电梯的运行状态(上升、下降、停止等)不同,其行为(如开门、关门、运行速度等)也不同,状态模式可以很好地处理这种情况。
当一个操作中含有庞大的多分支的条件语句,且这些分支依赖于该对象的状态时,状态模式将每一个条件分支放入一个独立的类中,这使得可以根据对象自身的情况将对象的状态作为一个对象,该对象可以不依赖于其他对象而独立变化。例如在一个订单处理系统中,订单的状态(已提交、已付款、已发货等)不同,处理订单的操作也不同,使用状态模式可以避免复杂的条件判断。
策略模式(Strategy)
特征描述:定义一系列的算法,把它们一个个封装起来,并且使它们可相互替换。在对象中包含算法对象,通过选择不同的算法对象来实现不同的行为。
应用场景:
当许多相关的类仅仅是行为有异时,“策略”提供了一种用多个行为中的一个行为来配置一个类的方法。例如在一个游戏中,不同的角色可能有不同的攻击策略,通过策略模式可以方便地为每个角色配置不同的攻击算法。
当需要使用一个算法的不同变体时,例如定义一些反映不同的空间/时间权衡的算法,策略模式可以轻松切换不同的算法实现。
当算法使用客户不应该知道的数据时,可使用策略模式以避免暴露复杂的、与算法相关的数据结构。例如在一个加密算法库中,不同的加密策略可以封装起来,用户只需选择合适的加密策略,而无需了解算法内部的数据结构。
当一个类定义了多种行为,并且这些行为在这个类的操作中以多个条件语句的形式出现时,将相关的条件分支移入它们各自的Strategy类中以代替这些条件语句,使代码更加清晰和易于维护。
模板模式(Template Method)
特征描述:定义一个操作中的算法骨架,而将一些步骤延迟到子类中。模板定义逻辑骨架,由子类给出步骤中的实现,这样可以在保证算法整体结构不变的情况下,实现部分步骤的个性化定制。
应用场景:
当一次性实现一个算法的不变的部分,并将可变的行为留给子类来实现时,模板模式可以提高代码的复用性。例如在一个抽象的排序算法中,算法的整体
框架(如排序的比较操作等)可以在父类中定义,而具体的排序方式(如冒泡排序、快速排序等)可以由子类实现,这样可以方便地扩展不同的排序算法。
当各子类中公共的行为应被提取出来并集中到一个公共父类中以避免代码重复时,模板模式提供了一种有效的方式。例如在多个子类都需要进行初始化操作时,可以在父类中定义模板方法,在子类中实现具体的初始化步骤,避免了代码在多个子类中的重复编写。
当需要控制子类扩展时,模板模式可以通过定义模板方法的流程和可扩展点,限制子类的扩展方式,确保子类在扩展时遵循一定的规则和结构。
访问者模式(Visitor)
特征描述:表示一个作用于某对象结构中的各元素的操作,它在不改变各元素的类的前提下定义作用于这些元素的新操作。通过访问者模式,可以将对对象结构的操作与对象结构本身分离开来,使得在不修改对象结构类的情况下添加新的操作变得更加容易。
应用场景:
当一个对象结构包含很多类对象,它们有不同的接口,而想对这些对象实施一些依赖于其具体类的操作时,访问者模式可以提供一种统一的方式来处理不同类型的对象。例如在一个图形处理系统中,有不同类型的图形(圆形、矩形、三角形等),每个图形有自己的绘制方法,使用访问者模式可以定义一个访问者来实现对所有图形的统一操作,如计算图形面积总和等。
当需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而想避免让这些操作“污染”这些对象的类时,访问者模式可以将这些操作封装在访问者类中,而不是在对象类中添加大量的方法。例如在一个电商系统中,对于商品对象,可能有计算价格、统计库存、生成报表等不同操作,这些操作可以通过访问者模式来实现,而不影响商品类的简洁性。
当该对象结构被很多应用共享时,用Visitor模式让每个应用仅包含需要用到的操作。这样可以根据不同应用的需求定制访问者,减少不必要的代码和资源占用。例如在一个企业级架构中,多个子系统可能共享一个数据结构,不同子系统可以通过各自的访问者来处理该数据结构,提高系统的灵活性和可维护性。
当定义对象结构的类很少改变,但经常需要在此结构上定义新的操作时,访问者模式非常适用。改变对象结构类需要重定义对所有访问者的接口,这可能需要很大的代价。如果对象结构类经常改变,那么可能还是在这些类中定义这些操作较好。但如果结构相对稳定,访问者模式可以方便地添加新操作,而无需修改对象结构类。
通过对这23种设计模式的详细介绍,我们可以看到它们在不同的场景下发挥着重要的作用,帮助开发者构建更加灵活、可维护和可扩展的软件系统。无论是创建型模式对对象创建过程的优化,结构型模式对系统结构的合理组织,还是行为模式对对象交互和行为的有效管理,都为软件开发提供了丰富的解决方案。在实际的项目开发中,根据具体的需求和场景选择合适的设计模式,可以大大提高软件的质量和开发效率。同时,理解这些设计模式之间的区别和联系,也有助于开发者更好地运用它们,避免不恰当的使用导致的问题。总之,设计模式是软件开发中宝贵的经验总结,对于提升软件设计水平具有重要意义。