设计模式学习笔记(二):UML与面向对象设计原则

1 UML

1.1 UML

UML(Unified Modeling Language)是统一建模语言,1997年11月UML1.1版本提交给OMG并正式通过,成为建模语言的个那个也标准。2003年6月UML2.0获得正式通过。

1.2 UML特性

  • U(Unified):统一,UML融合了多种优秀的面向对象建模方法以及多种得到认可的软件工程方法,消除了因方法林立且相互独立而带来的种种不便,集众家之长,故名“统一”。通过统一的表示方法可以让不同知识背景的领域专家,系统分析设计人员以及开发人员可以方便地交流
  • M(Modeling):UML是一种通用的可视化建模语言,不同与编程语言,UML通过一些标准的图形符号和文字来对系统进行建模,用于对软件进行描述,可视化处理,构造系统制品的文档。UML适用于各种软件开发方法,软件生命周期的各个阶段,各种应用领域以及各种开发工具
  • L(Language):UML是一种语言,也就意味着它有属于自己的标准表达规则,不是一种类似Java,C++的编程语言,而是一种分析设计语言,一种建模语言

1.3 UML结构

UML结构通常包括以下4个部分:视图,图,模型元素以及通用机制。

1.3.1 视图

UML视图用于从不同的角度来表示待建模的系统。视图是由许多图形组成的一个抽象集合,在建立一个系统模型时,只有通过定义多个视图,每个视图显示该系统的一个特定方面,才能构造出该系统的完整蓝图。

UML视图包括:

  • 用户视图:以用户的观点表示系统的目标,是所有视图的核心,用于描述系统的需求
  • 结构视图:系统的静态行为,描述系统的静态元素,比如包,类,对象以及它们之间的关系
  • 行为视图:系统的动态行为,描述系统的组成元素在系统运行时的交互关系
  • 实现视图:系统中逻辑元素的分布,描述系统中物理文件以及它们之间的关系
  • 环境视图:系统中物理元素的分布,描述系统中硬件设备以及他们之间的关系

1.3.2 图

UML图是描述UML视图内容的图形,UML2.0提供了13种图,分别是用例图,类图,对象图,包图,组合结构图,状态图,活动图,顺序图,通信图,定时图,交互概览图,组件图和部署图。其中:

  • 用例图对应用户视图
  • 类图,对象图,包图和组合结构图对应结构视图
  • 状态图,活动图,顺序图,通信图,定时图和交互概览图对应行为视图
  • 组件图对应实现视图
  • 部署图对应环境视图

1.3.3 模型元素

模型元素是指UML图中所使用的一些概念,对应于普通的面向对象概念,如类,对象,消息以及这些概念之间的关系,如关联关系,泛化关系等。

1.3.4 通用机制

UML提供的通用机制为模型元素提供额外的注释,信息和语义,这些通用机制也提供了扩展机制,允许用户对UML进行扩展,如定义新的建模元素,扩展原有的语义,添加新的特殊信息来扩展模型元素的规则说明等,以便适用于特定的方法或过程,组织和用户。

2 UML类图

2.1 类图

类封装了数据和行为,是具有相同属性,操作,关系的对象集合的总称。类图是用出现在系统中不同类来描述系统的静态结构,主要描述不同的类以及它们之间的关系。

在UML中,类图包含类名,属性以及操作。如下面的Employee类:

在这里插入图片描述

类一般由三部分组成:

  • 类名
  • 属性
  • 操作

2.1.1 类名

类名就是类的名字,一个字符串。

2.1.2 属性

类的成员变量,一般的格式为

可见性 名称 : 类型 [ = 默认值]

可见性表示该属性对于类外的元素是否可见,包括:

  • 公有:+
  • 私有:-
  • 受保护:#
  • 包:~

2.1.3 操作

UML规定操作的定义方式为:

可见性 名称(参数列表)[ : 返回类型]
  • 可见性与属性可见性的定义一致
  • 参数列表表示方法的参数,语法与属性定义类似,用,分隔

2.2 类之间的关系

UML提供了四种不同的方式表示类与类之间的关系:

  • 关联关系
  • 依赖关系
  • 泛化关系
  • 接口与实现关系

下面逐个看一下。

3 关联关系

关联关系是一种结构化关系,用于表示一类对象与另一类对象之间有联系。在UML中用实线连接有关联关系的类。可以在关联线上标注角色名,关系的两端代表两种不同的角色,因此在一个关联关系中可以包含两个角色名,角色名不是必须的,但可以使类之间的关系更加明确。

例如在一个登录界面类LoginForm包含一个JButton

在这里插入图片描述

UML中关联通常包括以下六种形式:

  • 双向关联
  • 单向关联
  • 自关联
  • 多重性关联
  • 聚合关系
  • 组合关系

3.1 双向关联

默认情况下关联是双向的,例如顾客购买商品并拥有商品,反之卖出的商品总是某个顾客与之相关联:

在这里插入图片描述

3.2 单向关联

关联也可以是单向的,在UML中关联用带箭头的实线表示,比如顾客拥有地址:

在这里插入图片描述

3.3 自关联

系统中可能会存在一些类的属性对象类型为该类本身,这种特殊的关联关系为自关联,常见于链表:

在这里插入图片描述

3.4 多重性关联

多重性关联又称为多重性关联联系,表示两个关联对象在数量上的对应关系。在UML中,对象之间的多重性可以直接在关联直线上用一个数字或者一个数字范围来表示。常见的表示方式如下:

在这里插入图片描述

例如一个界面可以具有0个或多个按钮,但是一个按钮只能从属于一个界面:

在这里插入图片描述

3.5 聚合关系

聚合关系表示整体与部分的关系,使用空心菱形表示。聚合关系中部分是整体的一部分,但是部分可以脱离整体独立存在,比如引擎是汽车的一部分,但是引擎可以独立于汽车存在:

在这里插入图片描述

3.6 组合关系

组合关系也表示整体与部分之间的关系,但是部分不能脱离整体存在。组合关系使用实心菱形表示。比如人的头和嘴巴是组合关系:

在这里插入图片描述

4 依赖关系

依赖关系是一种使用关系,在需要表示“一个事物使用另一个事物”时使用依赖关系。UML中依赖关系用带箭头的虚线表示,由依赖的一方指向被依赖的一方。例如驾驶员开车,开车需要车,也就是驾驶员依赖于车:

在这里插入图片描述

5 泛化关系

泛化关系也就是继承关系,用于描述父类与子类之间的关系,父类又叫基类或者超类,子类又称作派生类。UML中泛化关系用带空心三角形的直线表示,箭头指向基类:

在这里插入图片描述

6 接口与实现关系

很多语言比如Java,C#都有接口的概念,接口通常没有属性,所有是操作都是抽象的,只有操作的声明没有操作的实现。UML中使用<<Interface>>表示接口:

在这里插入图片描述

类与接口之间的实现关系使用空心三角形+虚线表示:

在这里插入图片描述

7 面向对象设计原则

7.1 概述

面向对象设计的目标之一是支持可维护性复用,一方面需要实现设计方案或者源代码的重用,一方面要确保系统能够易于扩展和修改,具有较好的灵活性。面向对象设计原则由此诞生,它们蕴含于很多设计模式中,是从许多方案总结出来的指导性原则。常见的7种面向对象设计原则如下:

  • 单一权责原则
  • 开闭原则
  • 里氏代换原则
  • 依赖倒转原则
  • 接口隔离原则
  • 合成复用原则
  • 迪米特法则

7.2 单一职责原则SRP

单一权责原则(Single Responsibility Principal):一个类只负责一个功能领域中的相应职责。

或者可以定义为:就一个类而言,应该只有一个引起它变化的原因。

单一权责原则的核心思想是:一个类不能太“累”。一个类(大到模块,小到方法)承担的职责越多,被复用的可能性越小,而且一个类承担的职责过多,就相当于将这些职责耦合在一起,当其中一个职责变化时,可能会影响其他职责的运作,因此需要将职责分离,封装在不同的类中,即将不同的变化原因封装在不同的类中。单一权责原则是实现高内聚,低耦合的指导方针。

7.3 开闭原则OCP

开闭原则(Open-Closed Principle):一个软件实体应当对扩展开放,对修改关闭。即软件实体应尽量不修改原有代码的情况下进行扩展。

其中软件实体可以是一个软件模块,一个由多个类组成的局部结构或者一个独立的类。

一个软件设计符合开闭原则,则可以非常方便地对其进行扩展,而且在扩展时无须修改现有代码,使得软件系统在拥有适应性和灵活性的同时具备较好的稳定性和延续性。

为了满足开闭原则,需要对系统进行抽象化设计,抽象化是开闭原则的关键。可以通过接口,抽象类等定义抽象层,通过具体类进行扩展,修改系统的行为时无需修改抽象层,只需要增加新的具体类来实现新的业务功能即可,实现在不修改已有代码的基础上扩展系统的功能,达到开闭原则的要求。

7.4 里氏代换原则LSP

里氏代换原则(Liskov Substitution Principle):所有引用基类/父类的地方必须能透明地使用其子类的对象。

简单地说就是父类出现的地方可以用子类代替,程序不会产生任何的错误和异常。使用里氏代换原则时,应该将父类设计为抽象类或者接口,让子类继承父类或实现父类接口,并实现父类中声明的方法,运行时,子类实例代替父类实例,可以很方便地扩展系统的功能,无须修改原有子类的代码,增加新的功能可以通过增加一个新的子类来实现。

7.5 依赖倒转原则DIP

依赖倒转原则(Dependency Inversion Principal):抽象不应该依赖具体细节,细节应当依赖于抽象,换言之要针对接口编程,而不是针对实现编程。

依赖倒转原则要求程序在源代码中传递参数时或者在关联关系中,尽量引用高层次的抽象层类,即使有接口和抽象类进行变量类型声明,参数类型声明,方法返回类型声明以及数据类型的转换等,而不是用具体类来做。一个具体类应该只实现接口或者抽象类中声明过的方法,而不要给出多余的方法,否则将无法调用在子类中新增的方法。

在实现依赖倒转原则时,需要针对抽象层进行编程,而将具体类的对象通过依赖注入(Dependency Injection)的方式注入到其他对象中。依赖注入是指当一个对象要与其他对象发生依赖关系时,通过抽象来注入所依赖的对象。常用的注入方式包括:

  • 构造注入:通过构造函数来传入具体类的对象
  • 设值注入(setter注入):通过setter来传入具体类对象
  • 接口注入:通过实现在接口中声明的方法来传入具体类对象

上面的方法在定义时使用抽象类型,在运行时传入具体类型的对象,由子类对象来覆盖父类对象 。

7.6 接口隔离原则ISP

接口隔离原则(Interface Segregation Principal):使用多个专门的接口,而不使用单一的总接口,即客户端不应该依赖于那些它不需要的接口。

也就是说,当一个接口太大时需要划分为更小的接口,使用该接口的客户端仅需知道与之相关的方法。每一个接口应该承担一种相对独立的角色,这里的接口有两层意思:

  • 一种是指一个类型所具有的方法特征的集合,仅仅是一种逻辑上的抽象
  • 另一种是指某个语言具体接口的定义,有严格的定义和结构,比如Java中的interface

ISP对两种不同含义的表达方式有所不同:

  • 当接口理解成一个类型所提供的所有方法特征的集合时,这就是一种逻辑上的概念,接口的划分将直接带来类型的划分,可以把接口理解成角色,一个接口只能代表一个角色,每个角色都有它特定的一个接口,此时这个原则可以叫“角色隔离原则”
  • 把接口理解成狭义的特定语言的接口,ISP表达的意思是接口仅仅提供客户端需要的行为,客户端不需要的行为则隐藏起来,应当为客户端提供尽可能小的接口,而不提供大的总接口。接口应尽量细化,同时接口中的方法应该尽量少,每个接口中只包含一个客户端(如子模块或者业务逻辑类)所需的方法即可,这种机制也叫“定制服务”

使用接口隔离原则时,注意控制接口的粒度:

  • 接口太小导致接口泛滥,不利于维护
  • 接口太大将违背ISP,灵活性差,使用不方便

一般而言接口中仅包含为某一类用户定制的方法即可。

7.7 合成复用原则CRP

合成复用原则(Composite Reuse Principal):尽量使用对象组合而不是继承来达成复用目的。

合成复用原则又叫组合/聚合复用原则(Composition/Aggregate Reuse Principal),就是在一个新对象中通过关联关系(组合/聚合)对对象进行重用而不是使用继承。

面向对象设计中,可以通过两种方法在不同环境中复用已有的设计和实现:

  • 继承
  • 组合/聚合

7.7.1 继承

继承需要严格遵循里氏代换原则,有效使用继承会有助于对问题的理解,降低复杂度,而滥用继承反而会增加系统构建和维护的难度以及系统的复杂度。继承主要带来的问题是会破坏系统的封装性,因为继承会将基类实现细节暴露给子类,由于基类内部细节对子类可见,因此叫“白箱复用”。

一般而言两个类之间的关系是“Is-A”关系就可以使用继承。

7.7.2 组合/聚合

尽管可以通过继承来对代码进行复用,一般来说优先考虑组合/聚合。组合/聚合可以使系统更加灵活,降低类与类之间的耦合度。由于新对象可以直接调用已有对象的功能,这样做可以使成员对象的内部实现细节对新对象不可见,所以这种复用叫“黑箱复用”。

一般而言两个类之间的关系是“Has-A”关系就可以使用组合/聚合。

7.8 迪米特法则LoD

迪米特法则(Law of Demeter):一个软件实体应当尽可能少地与其他实体发生相互作用。

迪米特法则又叫最少知识原则(Least Knowledge Principal,LKP),迪米特法则会对软件实体之间通信的宽度与深度进行限制,可以降低系统的耦合度,使类与类之间保持松耦合。

迪米特法则还有几种定义形式:不要和“陌生人”说话,只与直接朋友通信。对于一个对象“朋友”可以是以下几类:

  • 对象本身(this)
  • 以参数形式传入的对象
  • 成员对象
  • 如果成员对象是一个集合,那么集合中的元素也是“朋友”
  • 当前对象所创建的对象

满足上述条件之一即是“朋友”,否则就是“陌生人”,不能和“陌生人”发生直接交互。

迪米特法则要求设计系统时尽量减少对象之间的交互,通过引入一个合理的中间类来降低现有对象之间的耦合度。应用迪米特法则时需要注意几点:

  • 优先将类设计为不变类
  • 类划分上尽量创建松耦合的类
  • 类结构设计上尽量降低成员变量和成员函数的访问权限
  • 在对其他类的引用上,一个对象对其他对象的引用应当降到最低

8 总结

在这里插入图片描述

在这里插入图片描述

posted @ 2020-07-11 11:34  氷泠  阅读(839)  评论(0编辑  收藏  举报