关于本教程
我应该学习本教程吗?
本教程适用于希望学习设计模式以提高其面向对象的设计和开发技能的Java程序员。 阅读本教程后,您将:
- 了解什么是设计模式,以及如何在几个著名的目录中对它们进行描述和分类
- 能够使用设计模式作为词汇来理解和讨论面向对象的软件设计
- 了解一些最常见的设计模式,并知道何时以及如何使用它们
本教程假定您熟悉Java语言和基本的面向对象的概念,例如多态性,继承和封装。 对统一建模语言(UML)有所了解是有帮助的,但不是必需的。 本教程将介绍基础知识。
本教程是关于什么的?
设计模式记录了专业软件开发人员的经验,并以系统的方式介绍了常见的重复性问题,解决方案以及这些解决方案的后果。
本教程说明:
- 为什么模式对面向对象的设计和开发有用且很重要
- 模式如何记录,分类和分类
- 什么时候应该使用模式
- 一些重要模式及其实现方式
工具类
本教程中的示例全部使用Java语言编写。 可以通过脑力劳动来阅读代码,这是可能并且足够的,但是要尝试使用该代码,则需要一个最小的Java开发环境。 您只需要一个简单的文本编辑器(例如Windows中的记事本或UNIX环境中的vi)和Java开发工具包 (1.2或更高版本)。
也可以使用许多工具来创建UML图(请参阅参考资料 )。 这些对于本教程不是必需的。
设计模式概述
设计模式简史
设计模式最初是由建筑师克里斯托弗·亚历山大(Christopher Alexander)在他的著作《模式语言:城镇,建筑物,建筑》 (牛津大学出版社,1977年)中描述的。 他引入并称为模式的概念-为反复出现的设计问题提供抽象的解决方案-引起了其他领域的研究人员的关注,尤其是在1980年代中晚期的那些开发面向对象软件的研究人员。
对软件设计模式的研究导致可能是最有影响力的关于面向对象设计的书:《 设计模式:可重用的面向对象软件的元素》 ,作者:Erich Gamma,Richard Helm,Ralph Johnson和John Vlissides(Addison-Wesley,1995年) ;请参阅参考资料 )。 这些作者通常被称为“四人帮”,而这本书被称为“四人帮”(或GoF)书。
“ 设计模式 ”的最大部分是描述23种设计模式的目录。 其他最近的目录扩展了这一功能,最重要的是,将覆盖范围扩展到了更特殊的问题类型。 Mark Grand,在Java模式:用UML演示的可重用设计模式目录中 ,添加了解决涉及并发问题的模式,例如Deepak Alur,John Crupi和Dan Malks着重讨论的核心J2EE模式:最佳实践和设计策略 。 Java 2企业技术的多层应用程序模式。
有一个活跃的模式社区,它收集新的模式,继续进行研究,并带头推广有关模式的信息。 特别是,Hillside Group赞助了许多会议,其中包括在专家的指导下向新手介绍模式的会议。 相关主题提供了有关模式和模式社区的其他信息源。
图案碎片
四人帮将模式描述为“在上下文中解决问题”。 这三件事-问题,解决方案和上下文-是模式的本质。 为了记录该模式,另外给该模式起一个名字,考虑使用该模式将产生的后果,并提供一个或多个示例也是有用的。
不同的编目人员使用不同的模板来记录其模式。 不同的编目员也为模式的不同部分使用不同的名称。 每个目录的详细程度和对每个模式的分析也有所不同。 接下来的几节描述了设计模式和Java模式中使用的模板。
设计模式模板
设计模式使用以下模板:
- 模式名称和分类:模式的概念性句柄和类别
- 目的:模式可解决什么问题?
- 也称为:模式的其他通用名称
- 动机:说明问题的方案
- 适用范围:在什么情况下可以使用该模式?
- 结构:使用对象建模技术(OMT)的图
- 参加者:设计中的类和对象
- 协作:设计中的类和对象如何协作
- 结果:该模式实现了哪些目标? 有哪些权衡?
- 实施:要考虑的实施细节,特定于语言的问题
- 示例代码:Smalltalk和C ++中的示例代码
- 已知用途:真实示例
- 相关模式:相关模式的比较和讨论
Java模板中的模式
Java中的模式使用以下模板:
- 模式名称:名称和对其最初描述位置的引用
- 内容提要:模式的简短描述
- 上下文:模式要解决的问题的描述
- 力量:导致解决方案的注意事项的描述
- 解决方案:一般解决方案的说明
- 后果:使用模式的含义
- 实施:要考虑的实施细节
- Java API的用法:如果可用,将提到Java API中的示例
- 代码示例:Java语言中的代码示例
- 相关模式:相关模式列表
学习方式
首先要学习的最重要的事情是每种模式的意图和上下文:模式要解决的问题和条件。 本教程涵盖了一些最重要的模式,但是对于勤奋的开发人员来说,建议的下一步是浏览一些目录并挑选关于每种模式的信息。 在“ 设计模式”中,要阅读的相关部分是“意图”,“动机”和“适用性”。 在Java模式中,相关部分是“概要”,“上下文”和“强制和解决方案”。
进行背景研究可以帮助您确定适合自己的模式,以解决您面临的设计问题。 然后,您可以更仔细地评估候选模式的适用性,同时详细考虑解决方案及其后果。 如果失败,则可以查看相关模式。
在某些情况下,您可能会发现多个可以有效使用的模式。 在其他情况下,可能没有适用的模式,或者就性能或复杂性而言,使用适用的模式的成本可能太高,因此临时解决方案可能是最好的选择。 (也许此解决方案可能导致尚未记录的新模式!)
利用模式获得经验
设计面向对象软件的关键步骤是发现对象。 有多种技术可以提供帮助:例如用例,协作图或类-责任-协作(CRC)卡-但是发现对象是没有经验的设计人员正确完成工作的最难步骤。
缺乏经验或指导会导致太多的对象具有太多的交互作用并因此导致依赖性,从而创建了难以维护且无法重用的整体系统。 这违背了面向对象设计的目的。
设计模式有助于克服此问题,因为它们会教从专家的经验中汲取的教训:模式记录了专业知识。 此外,模式不仅描述了软件的结构,而且更重要的是,它们还描述了类和对象的交互方式,尤其是在运行时。 明确考虑这些交互及其后果将导致软件更加灵活和可重用。
何时不使用模式
虽然正确使用模式会导致代码可重用,但其后果通常包括一些成本和收益。 可重用性通常是通过引入封装或间接获得的,这会降低性能并增加复杂性。
例如,您可以使用Facade模式将松散相关的类与单个类包装在一起,以创建易于使用的单一功能集。 一个可能的应用程序可能是为Java Internationalization API创建外观。 对于独立应用程序来说,这种方法可能是合理的,因为从资源束中获取文本,格式化日期和时间等需求分散在应用程序的各个部分中。 但这对于将表示逻辑与业务分开的多层企业应用程序可能不那么合理。 如果对国际化API的所有调用都在表示模块中隔离(可能是通过将它们包装为JSP定制标记),则添加另一层间接将是多余的。
并发模式中讨论了何时应谨慎使用模式的另一个示例,涉及单线程执行模式的后果。
随着系统的成熟,经验的增加或软件缺陷的暴露,偶尔重新考虑以前做出的选择是一个好习惯。 您可能必须重写临时代码,以便它改用模式,或者从一种模式更改为另一种模式,或者完全删除模式以消除间接层。 拥抱变更(或至少为此做好准备),因为这是不可避免的。
UML类图简介
类图
UML已成为面向对象设计的标准图表工具。 在UML定义的各种类型的图中,本教程仅使用类图。 在类图中,类被描述为带有三个隔间的盒子,如图1所示。
图1.类图
顶部隔间包含类名称; 如果该类是抽象的,则名称用斜体表示。 中间部分包含类属性(也称为属性或变量)。 底部的隔离专区包含类方法(也称为操作 )。 像类名一样,如果方法是抽象的,则其名称用斜体表示。
根据所需的详细程度,可以省略属性,仅显示类名及其方法,或者省略属性和方法,仅显示类名。 当说明总体概念关系时,此方法很常见。
类之间的关联
类之间的任何交互作用都由类之间绘制的线条来描述。 简单的线表示关联,通常是任何未指定类型的概念关联。 可以修改该行以提供有关关联的更多特定信息。 通过添加一个打开的箭头指示可导航性 。 通过添加三角形箭头来表示专业化或子类化。 基数(或未指定复数的星号)也可以添加到每一端以指示关系,例如一对一和多对一。
图2显示了这些不同类型的关联。
图2.类关联
相关主题提供有关UML和Java语言关联的进一步阅读。
创作模式
创作模式概述
创建模式规定了对象的创建方式。 当必须在实例化类时做出决定时使用这些模式。 通常,实例化的类的详细信息-它们的确切含义,创建方式和创建时间-由抽象超类封装,并从仅了解抽象类或其接口的客户端类中隐藏。实施。 具体类型的具体类型通常对于客户端类是未知的。
例如,Singleton模式用于封装对象的创建,以保持对其的控制。 这不仅确保仅创建一个,而且还允许延迟实例化。 也就是说,可以延迟对象的实例化,直到实际需要它为止。 如果构造函数需要执行昂贵的操作(例如访问远程数据库),这将特别有益。
单例模式
这段代码演示了如何使用Singleton模式来创建一个计数器以提供唯一的序号,例如用作数据库中的主键时可能需要:
// Sequence.java
public class Sequence {
private static Sequence instance;
private static int counter;
private Sequence()
{
counter = 0; // May be necessary to obtain
// starting value elsewhere...
}
public static synchronized Sequence getInstance()
{
if(instance==null) // Lazy instantiation
{
instance = new Sequence();
}
return instance;
}
public static synchronized int getNext()
{
return ++counter;
}
}
关于此实现的一些注意事项:
-
Synchronized
方法用于确保该类是线程安全的。 - 此类不能被子类化,因为构造方法是
private
。 根据受保护的资源,这可能不是好事。 要允许子类化,应将构造方法的可见性更改为protected
。 - 对象序列化可能会导致问题; 如果将Singleton序列化,然后反序列化一次以上,则将有多个对象而不是Singleton。
工厂方法模式
除了Singleton模式之外,创建模式的另一个常见示例是Factory Method。 当必须在运行时确定要实例化几个兼容类中的哪一个时,使用此模式。 整个Java API都使用此模式。 例如,抽象Collator
类的getInstance()
方法返回一个适合于默认语言环境的归类对象,该对象由java.util.Locale.getDefault()
确定:
Collator defaultCollator = getInstance();
实际上返回的具体类始终是Collator
的子类RuleBasedCollator
,但这是无关紧要的实现细节。 由抽象Collator
类定义的接口是使用它所需要的全部。
图3.工厂方法模式
结构模式
结构模式概述
结构模式规定了类和对象的组织。 这些模式与类如何彼此继承或如何由其他类组成有关。
常见的结构模式包括适配器,代理和装饰器模式。 这些模式的相似之处在于,它们在客户端类和它要使用的类之间引入了一个间接级别。 但是,它们的意图是不同的。 适配器使用间接修改类的接口,以使客户端类更容易使用它。 装饰器使用间接向类添加行为,而不会过度影响客户端类。 代理使用间接来透明地为另一个类提供替代。
适配器模式
适配器模式通常用于允许重用与客户端类希望看到的类相似但不相同的类。 通常,原始类能够支持客户端类所需的行为,但不具有客户端类期望的接口,并且更改原始类是不可能或不切实际的。 也许源代码不可用,或者在其他地方使用了源代码,并且更改接口是不合适的。
这是一个包装OldClass
的示例,以便客户端类可以使用在NewInterface
定义的方法NewMethod()
来调用它:
public class OldClassAdapter implements NewInterface {
private OldClass ref;
public OldClassAdapter(OldClass oc)
{
ref = oc;
}
public void NewMethod()
{
ref.OldMethod();
}
}
图4.适配器模式
代理和装饰器模式
代理是另一个类的直接替代,它通常具有与该类相同的接口,因为它实现了公共接口或抽象类。 客户端对象不知道它正在使用代理。 当必须以一种对客户端显而易见的方式来调解对客户端希望使用的类的访问时,将使用Proxy,例如,因为它需要受限的访问权限或是一个远程进程。
图5.代理模式
像Proxy一样,Decorator也是另一个类的替代者,并且它也具有与该类相同的接口,通常是因为它是子类。 但是,目的是不同的。 Decorator模式的目的是以对客户端类透明的方式扩展原始类的功能。
在用于处理输入和输出流的类中找到Java API中Decorator模式的示例。 例如, BufferedReader()
使从文件中读取文本方便而有效:
BufferedReader in = new BufferedReader(new FileReader("file.txt"));
复合图案
复合模式规定了复杂对象的递归组合。 目的是允许以一致的方式处理所有组件对象。 参与此模式的所有对象(简单和复杂)都来自定义通用行为的通用抽象组件类。
通过这种方式将关系强制为部分整体的层次结构,可以最大程度地减少我们的系统(或客户端子系统)需要管理的对象的类型。 例如,绘画程序的客户可以要求一条线以与要求任何其他对象(包括复合对象)相同的方式绘制自身。
图6.复合模式
行为模式
行为模式概述
行为模式规定了对象彼此交互的方式。 通过指定对象的职责以及它们之间的通信方式,它们有助于使复杂的行为易于管理。
观察者模式
观察者是一种非常常见的模式。 在使用模型/视图/控制器体系结构实现应用程序时,通常会使用此模式。 此设计的“模型/视图”部分旨在将数据表示与数据本身分离。
例如,考虑将数据保存在数据库中并且可以以多种格式显示为表格或图形的情况。 观察者模式建议显示类在负责维护数据的类中注册自己,以便可以在数据更改时得到通知,从而可以更新其显示。
Java API在其AWT / Swing类的事件模型中使用此模式。 它还提供直接支持,因此可以将这种模式用于其他目的。
Java API提供了一个Observable
类,该类可以由想要观察的对象子类化。 Observable
的方法包括:
-
Observable
对象调用addObserver(Observer o)
进行自身注册。 -
setChanged()
将Observable
对象标记为已更改。 -
hasChanged()
测试Observable
对象是否已更改。 -
notifyObservers()
通知所有的观察者,如果Observable
对象已经改变,根据hasChanged()
为此,提供了一个Observer
接口,其中包含一个由Observable
对象更改时调用的方法(当然,前提是Observer
已向Observable
类注册了自己):
public void update(Observable o, Object arg)
下面的示例演示如何使用“观察者”模式来通知传感器的显示类,例如温度已检测到变化:
import java.util.*;
class Sensor extends Observable {
private int temp = 68;
void takeReading()
{
double d;
d =Math.random();
if(d>0.75)
{
temp++;
setChanged();
}
else if (d<0.25)
{
temp--;
setChanged();
}
System.out.print("[Temp: " + temp + "]");
}
public int getReading()
{
return temp;
}
}
public class Display implements Observer {
public void update(Observable o, Object arg)
{
System.out.print("New Temp: " + ((Sensor) o).getReading());
}
public static void main(String []ac)
{
Sensor sensor = new Sensor();
Display display = new Display();
// register observer with observable class
sensor.addObserver(display);
// Simulate measuring temp over time
for(int i=0; i < 20; i++)
{
sensor.takeReading();
sensor.notifyObservers();
System.out.println();
}
}
}
策略和模板模式
策略和模板模式相似,因为它们允许针对一组固定的行为使用不同的实现。 但是,它们的意图是不同的。
策略用于允许在运行时动态选择算法或操作的不同实现。 通常,任何常见行为都是在抽象类中实现的,而具体的子类则提供了不同的行为。 客户通常了解可用的不同策略,并可以在它们之间进行选择。
例如,一个抽象类Sensor
可以定义进行测量,而具体的子类将需要实施不同的技术:一个可以提供运行平均值,另一个可以提供瞬时测量,而另一个可以保持峰值(或较低)一段时间。
图7.传感器抽象类
模板模式的目的不是要像策略中那样以不同的方式实现行为,而是要确保实现某些行为。 换句话说,在策略的重点是允许多样性的情况下,模板的重点是加强一致性。
模板模式是作为抽象类实现的,通常用于为具体子类提供蓝图或轮廓。 有时,这用于在系统(例如应用程序框架)中实现挂钩。
并发模式
并发模式概述
并发模式规定了对共享资源的访问进行协调或排序的方式。 到目前为止,最常见的并发模式是单线程执行,在这种模式下,必须确保一次只有一个线程可以访问一段代码。 这部分代码称为关键部分 ,通常是一段代码,它要么获得对必须共享的资源的访问权(例如,打开端口),要么是一系列原子操作(例如,获得)。一个值,执行计算,然后更新该值。
单线程执行模式
我们前面讨论的Singleton模式包含两个很好的单线程执行模式示例。 首先引起这种模式的问题是因为此示例使用了惰性实例化-将实例化延迟到必要时为止-从而导致两个不同的线程可以同时调用getInstance()
的可能性:
public static synchronized Sequence getInstance()
{
if(instance==null) // Lazy instantiation
{
instance = new Sequence();
}
return instance;
}
如果这个方法没有用对同时访问受保护的synchronized
,每个线程可能进入的方法,测试和发现静态实例引用为null,每个可以尝试创建一个新的实例。 最后一个完成的线程将获胜,并覆盖第一个线程的引用。 在这个特定示例中,情况可能还算不错-它仅创建一个孤立对象,垃圾回收器将最终清理该对象-但是有一个共享资源强制执行单次访问,例如打开端口或打开日志文件对于读/写访问,第二个线程创建实例的尝试将失败,因为第一个线程已经获得了对共享资源的互斥访问。
Singleton示例中代码的另一个关键部分是getNext()
方法:
public static synchronized int getNext()
{
return ++counter;
}
如果未使用synchronized
保护,则synchronized
调用它的两个线程可能会获得相同的当前值,而不是此类打算提供的唯一值。 如果将其用于获取数据库插入的主键,则第二次尝试使用相同的主键插入将失败。
如前所述,您应该始终考虑使用模式的成本。 使用synchronized
工作方式是:在一个线程输入代码段时锁定该代码段,并阻塞所有其他线程,直到第一个线程完成。 如果许多线程经常使用此代码,则可能会导致性能严重下降。
另一个危险是,如果一个线程在一个关键部分等待第二个线程而阻塞,而第二个线程在另一个关键部分等待第一个线程,则两个线程可能会死锁。
Wrapping up
摘要
由于许多重要原因,设计模式是面向对象设计的宝贵工具:
- 模式提供“ ...上下文中问题的解决方案”。 ( 设计模式 ,Gamma,Helm,Johnson和Vlissides)。
- 模式以有条不紊的方式捕获了经验丰富的设计师的专业知识,并将其用作非专家的设计工具和学习工具。
- 模式为在重要的抽象层次上讨论面向对象的设计提供了一个词汇。
- 模式目录用作成语的词汇表,有助于理解常见但复杂的设计问题解决方案。
翻译自: https://www.ibm.com/developerworks/java/tutorials/j-patterns/j-patterns.html