一、建模语言基础
0.UML图
a.用例图
用例图(Use Case Diagram): 又称为用况图,对应于用户视图。在用例图中,使用用例来表示系统的功能需求,用例图用于表示多个外部执行者与系统用例之间以及用例与用例之间的关系。
b.类图
类图(Class Diagram):对应于结构视图。类图使用类来描述系统的静态结构,类图包含类和它们之间的关系,它描述系统内所声明的类,但它没有描述系统运行时类的行为。
c.对象图
对象图(Object Diagram):对应于结构视图。对象图是类图在某一时刻的一个实例,用于表示类的对象实例之间的关系。
d.包图
包图(Package Diagram):UML2.0新增图,对应于结构视图。包图用于描述包与包之间的关系,包是一种把元素组织到一起的通用机制,如可以将多个类组织成一个包。
e.组合结构图
组合结构图(Composite Structure Diagram):UML2.0新增图,对应于结构视图。组合结构图将每一个类放在一个整体中,从类的内部结构来审视一个类。组合结构图可用于表示一个类的内部结构,用于描述一些包含复杂成员或内部类的类结构。
f.状态图
状态图(State Diagram):对应于行为视图。状态图用来描述一个特定对象的所有可能状态及其引起状态转移的事件。一个状态图包括一系列对象的状态及状态之间的转换。
- 状态图(Statechart Diagram)用来描述一个特定对象的所有可能状态及其引起状态转移的事件。
- 我们通常用状态图来描述单个对象的行为,它确定了由事件序列引出的状态序列,但并不是所有的类都需要使用状态图来描述它的行为,只有那些具有重要交互行为的类,我们才会使用状态图来描述,一个状态图包括一系列的状态及状态之间的转移。
状态图的组成元素与绘制:
- 状态(State):又称为中间状态,用圆角矩形框表示,在一个状态图中可有多个状态,每个状态包含两格:上格放置状态名称,下格说明处于该状态时对象可以进行的活动(Action)。
- 初始状态(Initial State):又称为初态,用一个黑色的实心圆圈表示,在一个状态图中只能够有一个初始状态。
- 结束状态(Final State):又称为终止状态或终态,用一个实心圆外加一个圆圈表示,在一个状态图中可能有多个结束状态。
- 转移(Transition):用从一个状态到另一个状态之间的连线和箭头说明状态的转移情况,并用文字说明引发这个状态变化的相应事件是什么。事件有可能在特定的条件下发生,在UML中这样的条件称为守护条件(Guard Condition),发生事件时的处理也称为动作(Action)。状态之间的转移可带有标注,由三部分组成(每一部分都可省略),其语法为:事件名 [条件] / 动作名。
在一个状态图中,一个状态也可以被细分为多个子状态,包含多个子状态的状态称为复合状态。
实例说明:
某信用卡系统账户具有使用状态和冻结状态,其中使用状态又包括正常状态和透支状态两种子状态。如果账户余额小于零则进入透支状态,透支状态时既可以存款又可以取款,但是透支金额不能超过5000元;如果余额大于零则进入正常状态,正常状态时既可以存款又可以取款;如果连续透支100天,则进入冻结状态,冻结状态下既不能存款又不能取款,必须要求银行工作人员解冻。用户可以在使用状态或冻结状态下请求注销账户。根据上述要求,绘制账户类的状态图。
实例:
g. 活动图
活动图(Activity Diagram):对应于行为视图。活动图用来表示系统中各种活动的次序,它的应用非常广泛,既可用来描述用例的工作流程,也可以用来描述类中某个方法的操作行为。
开始和结束以及状态表示是和状态图是一样的,其中菱形为条件判断
h.顺序图
顺序图(Sequence Diagram):又称为时序图或序列图,对应于行为视图。顺序图用于表示对象之间的交互,重点表示对象之间发送消息的时间顺序。
顺序图(Sequence Diagram)是一种强调对象间消息传递次序的交互图,又称为时序图或序列图。
顺序图以图形化的方式描述了在一个用例或操作的执行过程中对象如何通过消息相互交互,说明了消息如何在对象之间被发送和接收以及发送的顺序。顺序图允许直观地表示出对象的生存期,在生存期内,对象可以对输入消息做出响应,还可以发送信息。
顺序图通常包括两种:
- 需求分析阶段的顺序图:主要用于描述用例中对象之间的交互,可以使用自然语言来绘制,用于细化需求,它从业务的角度进行建模,用描述性的文字叙述消息的内容。
- 系统设计阶段的顺序图:确切表示系统设计中对象之间的交互,考虑到具体的系统实现,对象之间通过方法调用传递消息。
顺序图的组成与绘制:
在UML中,顺序图将交互关系表示为一个二维图,纵向是时间轴,时间沿竖线向下延伸;横向轴表示了在交互过程中的独立对象,对象的活动用生命线表示。顺序图由执行者(Actor)、生命线(Lifeline)、对象(Object)、激活框(Activation)和消息(Message)等元素组成。
- 执行者是交互的发起人,使用与用例图一样的“小人”符号表示,在有些交互过程中无须使用执行者。
- 生命线用一条纵向虚线表示。
- 对象表示为一个矩形,其中对象名称标有下划线。
- 激活是过程的执行,包括等待过程执行的时间。在顺序图中激活部分替换生命线,使用长条的矩形表示。
- 消息是对象之间的通信,是两个对象之间的单路通信,是从发送者到接收者之间的控制信息流。消息在顺序图中由有标记的箭头表示,箭头从一个对象的生命线指向另一个对象的生命线,消息按时间顺序在图中从上到下排列。
- 一个复杂的顺序图可以划分为几个小块,每一个小块称为一个交互片段(Interaction Fragment)。每个交互片段由一个大方框包围,在方框左上角的间隔区内标注该交互片段的操作类型,该操作类型用操作符表示,常用的操作符包括:
1) alt:多条路径,条件为真时执行。
2) opt:任选,仅当条件为真时执行。
3) par:并行,每一片段都并发执行。
4) loop:循环,片段可多次执行。
实例:
- 在顺序图中,有的消息对应于激活,表示它将会激活一个对象,这种消息称为调用消息(Call Message);如果消息没有对应激活框,表示它不是一个调用消息,不会引发其他对象的活动,这种消息称为发送消息(Send Message);如果对象的一个方法调用了自己的另一个方法时,消息是由对象发送给自身,这种消息称为自身消息(Self Call Message)。
- 顺序图中的消息还包括创建消息和销毁消息,创建消息用于使用new关键字创建另一个对象,而销毁消息用于调用对象的销毁方法将一个对象从内存中销毁。
实例说明:
某基于Java EE的B/S系统需要提供登录功能,该功能简要描述如下:用户打开登录界面login.jsp输入数据,向系统提交请求,系统通过Servlet获取请求数据,将数据传递给业务对象,业务对象接收数据后再将数据传递给数据访问对象,数据访问对象对数据库进行操作,查询用户信息,再返回查询结果。
1.类之间的关系
关联关系
关联关系(Association)是类与类之间最常用的一种关系,它是一种结构化关系,用于表示一类对象与另一类对象之间有联系。
(1)单项关联
(2)双向关联
(3)自关联
在系统中可能会存在一些类的属性对象类型为该类本身,这种特殊的关联关系称为自关联。
(4)重数性关联
重数性关联关系又称为多重性关联关系(Multiplicity),表示一个类的对象与另一个类的对象连接的个数。
聚合关系
- 聚合关系(Aggregation)表示一个整体与部分的关系。通常在定义一个整体类后,再去分析这个整体类的组成结构,从而找出一些成员类,该整体类和成员类之间就形成了聚合关系。
- 在聚合关系中,成员类是整体类的一部分,即成员对象是整体对象的一部分,但是成员对象可以脱离整体对象独立存在。在UML中,聚合关系用带空心菱形的直线表示。
组合关系 - 组合关系(Composition)也表示类之间整体和部分的关系,但是组合关系中部分和整体具有统一的生存期。一旦整体对象不存在,部分对象也将不存在,部分对象与整体对象之间具有同生共死的关系。
- 在组合关系中,成员类是整体类的一部分,而且整体类可以控制成员类的生命周期,即成员类的存在依赖于整体类。在UML中,组合关系用带实心菱形的直线表示。
依赖关系 - 依赖关系(Dependency)是一种使用关系,特定事物的改变有可能会影响到使用该事物的其他事物,在需要表示一个事物使用另一个事物时使用依赖关系。大多数情况下,依赖关系体现在某个类的方法使用另一个类的对象作为参数。
- 在UML中,依赖关系用带箭头的虚线表示,由依赖的一方指向被依赖的一方。
泛化关系 - 泛化关系(Generalization)也就是继承关系,也称为“is-a-kind-of”关系,泛化关系用于描述父类与子类之间的关系,父类又称作基类或超类,子类又称作派生类。在UML中,泛化关系用带空心三角形的直线来表示。
接口与实现关系
- 接口之间也可以有与类之间关系类似的继承关系和依赖关系,但是接口和类之间还存在一种实现关系(Realization),在这种关系中,类实现了接口,类中的操作实现了接口中所声明的操作。在UML中,类与接口之间的实现关系用带空心三角形的虚线来表示。
二、面向对象设计原则
1.单一职责原则
类的职责要单一,不能将太多的职责放在一个类中。一个对象应该只包含单一的职责,并且该职责被完整地封装在一个类中。
单一职责原则是实现高内聚、低耦合的指导方针,在很多代码重构手法中都能找到它的存在,它是最简单但又最难运用的原则,需要设计人员发现类的不同职责并将其分离,而发现类的多重职责需要设计人员具有较强的分析设计能力和相关重构经验。
(1)单一职责原则实例
某基于Java的C/S系统的“登录功能”通过如下登录类(Login)实现:
现使用单一职责原则对其进行重构,得到如下结果:
我们可以看到一个类里面只有一个单一的职责(功能),其他类通过声明其对象进行调用。
2.开闭原则
软件实体对扩展是开放的,但对修改是关闭的,即在不修改一个软件实体的基础上去扩展其他功能。
(1)分析
- 抽象化是开闭原则的关键。
- 开闭原则还可以通过一个更加具体的“对可变性封装原则”来描述,对可变性封装原则(Principle of Encapsulation of Variation, EVP)要求找到系统的可变因素并将其封装起来。
(2)开闭原则实例
某图形界面系统提供了各种不同形状的按钮,客户端代码可针对这些按钮进行编程,用户可能会改变需求要求使用不同的按钮,原始设计方案如图所示:
现对该系统进行重构,使之满足开闭原则的要求。
3.里氏代换原则
在软件体系中,一个可以接受基类对象的地方必然可以接受一个子类对象。
里氏代换原则实例
某系统需要实现对重要数据(如用户密码)的加密处理,在数据操作类(DataOperator)中需要调用加密类中定义的加密算法,系统提供了两个不同的加密类,CipherA和CipherB,它们实现不同的加密方法,在DataOperator中可以选择其中的一个实现加密操作。如图所示:
如果需要更换一个加密算法类或者增加并使用一个新的加密算法类,如将CipherA改为CipherB,则需要修改客户类Client和数据操作类DataOperator的源代码,违背了开闭原则。
现使用里氏代换原则对其进行重构,使得系统可以灵活扩展,符合开闭原则。
4.依赖倒转原则
要针对抽象层编程(接口),不要针对具体类编程。
- 简单来说,依赖倒转原则就是指:代码要依赖于抽象的类,而不要依赖于具体的类;要针对接口或抽象类编程,而不是针对具体类编程。
- 实现开闭原则的关键是抽象化,并且从抽象化导出具体化实现,如果说开闭原则是面向对象设计的目标的话,那么依赖倒转原则就是面向对象设计的主要手段。
- 依赖倒转原则的常用实现方式之一是在代码中使用抽象类,而将具体类放在配置文件中。
依赖倒转原则分析
类之间的耦合
- 零耦合关系
- 具体耦合关系
- 抽象耦合关系
依赖倒转原则要求客户端依赖于抽象耦合,以抽象方式耦合是依赖倒转原则的关键。
依赖注入(DI)
- 构造注入(Constructor Injection):通过构造函数注入实例变量。
- 设值注入(Setter Injection):通过Setter方法注入实例变量。
- 接口注入(Interface Injection):通过接口方法注入实例变量。
依赖倒转实例:
某系统提供一个数据转换模块,可以将来自不同数据源的数据转换成多种格式,如可以转换来自数据库的数据(DatabaseSource)、也可以转换来自文本文件的数据(TextSource),转换后的格式可以是XML文件(XMLTransformer)、也可以是XLS文件(XLSTransformer)等。
由于需求的变化,该系统可能需要增加新的数据源或者新的文件格式,每增加一个新的类型的数据源或者新的类型的文件格式,客户类MainClass都需要修改源代码,以便使用新的类,但违背了开闭原则。现使用依赖倒转原则对其进行重构。
5.接口隔离原则
使用多个专门的接口来取代一个统一的接口,将复杂接口划分为更细的多个接口,客户只需要实现其需要的接口。
接口隔离原则的分析
接口隔离原则是指使用多个专门的接口,而不使用单一的总接口。每一个接口应该承担一种相对独立的角色,不多不少,不干不该干的事,该干的事都要干。
- (1) 一个接口就只代表一个角色,每个角色都有它特定的一个接口,此时这个原则可以叫做“角色隔离原则”。
- (2) 接口仅仅提供客户端需要的行为,即所需的方法,客户端不需要的行为则隐藏起来,应当为客户端提供尽可能小的单独的接口,而不要提供大的总接口。
使用接口隔离原则拆分接口时,首先必须满足单一职责原则,将一组相关的操作定义在一个接口中,且在满足高内聚的前提下,接口中的方法越少越好。
可以在进行系统设计时采用定制服务的方式,即为不同的客户端提供宽窄不同的接口,只提供用户需要的行为,而隐藏用户不需要的行为。
接口隔离实例
下图展示了一个拥有多个客户类的系统,在系统中定义了一个巨大的接口(胖接口)AbstractService来服务所有的客户类。可以使用接口隔离原则对其进行重构。
重构后的结果
6.合成复用原则
在系统中应该尽量多使用组合和聚合关联关系,尽量少使用甚至不使用继承关系。
合成复用原则分析
合成复用原则就是指在一个新的对象里通过关联关系(包括组合关系和聚合关系)来使用一些已有的对象,使之成为新对象的一部分;新对象通过委派调用已有对象的方法达到复用其已有功能的目的。简言之:要尽量使用组合/聚合关系,少用继承。
在面向对象设计中,可以通过两种基本方法在不同的环境中复用已有的设计和实现,即通过组合/聚合关系或通过继承。
- 继承复用:实现简单,易于扩展。破坏系统的封装性;从基类继承而来的实现是静态的,不可能在运行时发生改变,没有足够的灵活性;只能在有限的环境中使用。(“白箱”复用 )
- 组合/聚合复用:耦合度相对较低,选择性地调用成员对象的操作;可以在运行时动态进行。(“黑箱”复用 )
组合/聚合可以使系统更加灵活,类与类之间的耦合度降低,一个类的变化对其他类造成的影响相对较少,因此一般首选使用组合/聚合来实现复用;其次才考虑继承,在使用继承时,需要严格遵循里氏代换原则,有效使用继承会有助于对问题的理解,降低复杂度,而滥用继承反而会增加系统构建和维护的难度以及系统的复杂度,因此需要慎重使用继承复用。
合成复用原则实例
某教学管理系统部分数据库访问类设计如图所示:
如果需要更换数据库连接方式,如原来采用JDBC连接数据库,现在采用数据库连接池连接,则需要修改DBUtil类源代码。如果StudentDAO采用JDBC连接,但是TeacherDAO采用连接池连接,则需要增加一个新的DBUtil类,并修改StudentDAO或TeacherDAO的源代码,使之继承新的数据库连接类,这将违背开闭原则,系统扩展性较差。
现使用合成复用原则对其进行重构。
7.迪米特法则
一个软件实体对其他实体的引用越少越好,或者说如果两个类不必彼此直接通信,那么这两个类就不应该发生直接的相互作用,而是通过引入一个第三者发生间接交互。
迪米特法则分析
在迪米特法则中,对于一个对象,其朋友包括以下几类:
(1) 当前对象本身(this);
(2) 以参数形式传入到当前对象方法中的对象;
(3) 当前对象的成员对象;
(4) 如果当前对象的成员对象是一个集合,那么集合中的元素也都是朋友;
(5) 当前对象所创建的对象。
任何一个对象,如果满足上面的条件之一,就是当前对象的“朋友”,否则就是“陌生人”。
迪米特法则的主要用途在于控制信息的过载:
- 在类的划分上,应当尽量创建松耦合的类,类之间的耦合度越低,就越有利于复用,一个处在松耦合中的类一旦被修改,不会对关联的类造成太大波及;
- 在类的结构设计上,每一个类都应当尽量降低其成员变量和成员函数的访问权限;
- 在类的设计上,只要有可能,一个类型应当设计成不变类;
- 在对其他类的引用上,一个对象对其他对象的引用应当降到最低。
迪米特法则实例
某系统界面类(如Form1、Form2等类)与数据访问类(如DAO1、DAO2等类)之间的调用关系较为复杂,如图所示:
应用后