UML(Unified Modeling Language)是统一建模语言,1997年11月UML1.1版本提交给OMG并正式通过,成为建模语言的个那个也标准。2003年6月UML2.0获得正式通过。
UML结构通常包括以下4个部分:视图,图,模型元素以及通用机制。
UML视图用于从不同的角度来表示待建模的系统。视图是由许多图形组成的一个抽象集合,在建立一个系统模型时,只有通过定义多个视图,每个视图显示该系统的一个特定方面,才能构造出该系统的完整蓝图。
UML视图包括:
UML图是描述UML视图内容的图形,UML2.0提供了13种图,分别是用例图,类图,对象图,包图,组合结构图,状态图,活动图,顺序图,通信图,定时图,交互概览图,组件图和部署图。其中:
模型元素是指UML图中所使用的一些概念,对应于普通的面向对象概念,如类,对象,消息以及这些概念之间的关系,如关联关系,泛化关系等。
UML提供的通用机制为模型元素提供额外的注释,信息和语义,这些通用机制也提供了扩展机制,允许用户对UML进行扩展,如定义新的建模元素,扩展原有的语义,添加新的特殊信息来扩展模型元素的规则说明等,以便适用于特定的方法或过程,组织和用户。
类封装了数据和行为,是具有相同属性,操作,关系的对象集合的总称。类图是用出现在系统中不同类来描述系统的静态结构,主要描述不同的类以及它们之间的关系。
在UML中,类图包含类名,属性以及操作。如下面的Employee类:
类一般由三部分组成:
类名就是类的名字,一个字符串。
类的成员变量,一般的格式为
可见性 名称 : 类型 [ = 默认值]
可见性表示该属性对于类外的元素是否可见,包括:
+
-
#
~
UML规定操作的定义方式为:
可见性 名称(参数列表)[ : 返回类型]
,
分隔UML提供了四种不同的方式表示类与类之间的关系:
下面逐个看一下。
关联关系是一种结构化关系,用于表示一类对象与另一类对象之间有联系。在UML中用实线连接有关联关系的类。可以在关联线上标注角色名,关系的两端代表两种不同的角色,因此在一个关联关系中可以包含两个角色名,角色名不是必须的,但可以使类之间的关系更加明确。
例如在一个登录界面类LoginForm
包含一个JButton
:
UML中关联通常包括以下六种形式:
默认情况下关联是双向的,例如顾客购买商品并拥有商品,反之卖出的商品总是某个顾客与之相关联:
关联也可以是单向的,在UML中关联用带箭头的实线表示,比如顾客拥有地址:
系统中可能会存在一些类的属性对象类型为该类本身,这种特殊的关联关系为自关联,常见于链表:
多重性关联又称为多重性关联联系,表示两个关联对象在数量上的对应关系。在UML中,对象之间的多重性可以直接在关联直线上用一个数字或者一个数字范围来表示。常见的表示方式如下:
例如一个界面可以具有0个或多个按钮,但是一个按钮只能从属于一个界面:
聚合关系表示整体与部分的关系,使用空心菱形表示。聚合关系中部分是整体的一部分,但是部分可以脱离整体独立存在,比如引擎是汽车的一部分,但是引擎可以独立于汽车存在:
组合关系也表示整体与部分之间的关系,但是部分不能脱离整体存在。组合关系使用实心菱形表示。比如人的头和嘴巴是组合关系:
依赖关系是一种使用关系,在需要表示“一个事物使用另一个事物”时使用依赖关系。UML中依赖关系用带箭头的虚线表示,由依赖的一方指向被依赖的一方。例如驾驶员开车,开车需要车,也就是驾驶员依赖于车:
泛化关系也就是继承关系,用于描述父类与子类之间的关系,父类又叫基类或者超类,子类又称作派生类。UML中泛化关系用带空心三角形的直线表示,箭头指向基类:
很多语言比如Java,C#都有接口的概念,接口通常没有属性,所有是操作都是抽象的,只有操作的声明没有操作的实现。UML中使用<
表示接口:
类与接口之间的实现关系使用空心三角形+虚线表示:
面向对象设计的目标之一是支持可维护性复用,一方面需要实现设计方案或者源代码的重用,一方面要确保系统能够易于扩展和修改,具有较好的灵活性。面向对象设计原则由此诞生,它们蕴含于很多设计模式中,是从许多方案总结出来的指导性原则。常见的7种面向对象设计原则如下:
单一权责原则(Single Responsibility Principal):一个类只负责一个功能领域中的相应职责。
或者可以定义为:就一个类而言,应该只有一个引起它变化的原因。
单一权责原则的核心思想是:一个类不能太“累”。一个类(大到模块,小到方法)承担的职责越多,被复用的可能性越小,而且一个类承担的职责过多,就相当于将这些职责耦合在一起,当其中一个职责变化时,可能会影响其他职责的运作,因此需要将职责分离,封装在不同的类中,即将不同的变化原因封装在不同的类中。单一权责原则是实现高内聚,低耦合的指导方针。
开闭原则(Open-Closed Principle):一个软件实体应当对扩展开放,对修改关闭。即软件实体应尽量不修改原有代码的情况下进行扩展。
其中软件实体可以是一个软件模块,一个由多个类组成的局部结构或者一个独立的类。
一个软件设计符合开闭原则,则可以非常方便地对其进行扩展,而且在扩展时无须修改现有代码,使得软件系统在拥有适应性和灵活性的同时具备较好的稳定性和延续性。
为了满足开闭原则,需要对系统进行抽象化设计,抽象化是开闭原则的关键。可以通过接口,抽象类等定义抽象层,通过具体类进行扩展,修改系统的行为时无需修改抽象层,只需要增加新的具体类来实现新的业务功能即可,实现在不修改已有代码的基础上扩展系统的功能,达到开闭原则的要求。
里氏代换原则(Liskov Substitution Principle):所有引用基类/父类的地方必须能透明地使用其子类的对象。
简单地说就是父类出现的地方可以用子类代替,程序不会产生任何的错误和异常。使用里氏代换原则时,应该将父类设计为抽象类或者接口,让子类继承父类或实现父类接口,并实现父类中声明的方法,运行时,子类实例代替父类实例,可以很方便地扩展系统的功能,无须修改原有子类的代码,增加新的功能可以通过增加一个新的子类来实现。
依赖倒转原则(Dependency Inversion Principal):抽象不应该依赖具体细节,细节应当依赖于抽象,换言之要针对接口编程,而不是针对实现编程。
依赖倒转原则要求程序在源代码中传递参数时或者在关联关系中,尽量引用高层次的抽象层类,即使有接口和抽象类进行变量类型声明,参数类型声明,方法返回类型声明以及数据类型的转换等,而不是用具体类来做。一个具体类应该只实现接口或者抽象类中声明过的方法,而不要给出多余的方法,否则将无法调用在子类中新增的方法。
在实现依赖倒转原则时,需要针对抽象层进行编程,而将具体类的对象通过依赖注入(Dependency Injection)的方式注入到其他对象中。依赖注入是指当一个对象要与其他对象发生依赖关系时,通过抽象来注入所依赖的对象。常用的注入方式包括:
上面的方法在定义时使用抽象类型,在运行时传入具体类型的对象,由子类对象来覆盖父类对象 。
接口隔离原则(Interface Segregation Principal):使用多个专门的接口,而不使用单一的总接口,即客户端不应该依赖于那些它不需要的接口。
也就是说,当一个接口太大时需要划分为更小的接口,使用该接口的客户端仅需知道与之相关的方法。每一个接口应该承担一种相对独立的角色,这里的接口有两层意思:
ISP对两种不同含义的表达方式有所不同:
使用接口隔离原则时,注意控制接口的粒度:
一般而言接口中仅包含为某一类用户定制的方法即可。
合成复用原则(Composite Reuse Principal):尽量使用对象组合而不是继承来达成复用目的。
合成复用原则又叫组合/聚合复用原则(Composition/Aggregate Reuse Principal),就是在一个新对象中通过关联关系(组合/聚合)对对象进行重用而不是使用继承。
面向对象设计中,可以通过两种方法在不同环境中复用已有的设计和实现:
继承需要严格遵循里氏代换原则,有效使用继承会有助于对问题的理解,降低复杂度,而滥用继承反而会增加系统构建和维护的难度以及系统的复杂度。继承主要带来的问题是会破坏系统的封装性,因为继承会将基类实现细节暴露给子类,由于基类内部细节对子类可见,因此叫“白箱复用”。
一般而言两个类之间的关系是“Is-A”关系就可以使用继承。
尽管可以通过继承来对代码进行复用,一般来说优先考虑组合/聚合。组合/聚合可以使系统更加灵活,降低类与类之间的耦合度。由于新对象可以直接调用已有对象的功能,这样做可以使成员对象的内部实现细节对新对象不可见,所以这种复用叫“黑箱复用”。
一般而言两个类之间的关系是“Has-A”关系就可以使用组合/聚合。
迪米特法则(Law of Demeter):一个软件实体应当尽可能少地与其他实体发生相互作用。
迪米特法则又叫最少知识原则(Least Knowledge Principal,LKP),迪米特法则会对软件实体之间通信的宽度与深度进行限制,可以降低系统的耦合度,使类与类之间保持松耦合。
迪米特法则还有几种定义形式:不要和“陌生人”说话,只与直接朋友通信。对于一个对象“朋友”可以是以下几类:
满足上述条件之一即是“朋友”,否则就是“陌生人”,不能和“陌生人”发生直接交互。
迪米特法则要求设计系统时尽量减少对象之间的交互,通过引入一个合理的中间类来降低现有对象之间的耦合度。应用迪米特法则时需要注意几点:
如果觉得文章好看,欢迎点赞。
同时欢迎关注微信公众号:氷泠之路。