一、类之间常用关系的含义和UML表示
1.关联关系:
关联关系是类与类之间最常用的一种关系,它是一种结构化关系,用于表示一类对象与另一类对象之间有联系,如汽车和轮胎,师傅和徒弟、班级和学生等。
在UNL
类图中,用实线连接有关联关系的对象所对应的类,在使用java
、C#
和C++
等编程语言实现关联关系时,通常将一个类的对象作为另一个类的成员变量。
①双向关联:默认情况下,关联是双向的。如,客户购买商品并拥有商品,反之,卖出去的商品总有某个顾客与之相关联。
②单向关联:类的关联关系也可以是单向的,在UML
中单向关联用箭头的实线表示。例如,顾客拥有地址,则Customer
类与Address
类具有单向关联关系。
③自关联:在系统中可能会存在一些类的属性对象类型为该类本身,这种特殊的关联关系称为自关联。
④多重性关联:多重性关联关系又称为重数性关联关系,表示两个关联对象在数量上的对应关系。在UML
中,对象之间的多重性可以直接在关联直线上用一个数字或者一个数字范围表示。
一个界面(Form
)可以拥有零个或多个按钮(Button
),但是一个按钮只能属于一个界面。
⑤聚合关系
⑥组合关系
2.聚合关系:
聚合关系表示整体与部分的关系。在聚合关系中,成员对象是整体对象的一部分,但是成员对象可以脱离整体对象独立存在。在UML
中,聚合关系用空心菱形的直线表示。例如,汽车发动机(Engine
)是汽车(Car
)的组成部分,但是汽车发动机可以独立存在,因此,汽车和发动机是聚合关系。
在代码实现聚合关系时,成员对象通常作为构造方法、
Setter
方法或者业务方法的参数注入到整体对象中。3.组合关系:
组合关系也表示类之间整体和部分的关系,但是在组合关系中整体对象可以控制成员对象的生命周期,一旦整体对象不存在,成员对象也将不存在,成员对象与整体对象之间具有同生共死的关系。在UML
中,组合关系用带实心菱形的直线表示。例如,人的头(Head
)与嘴巴(Mouth
),嘴巴是头的组成部分之一,而且如果头没了,嘴巴也就没了,因此头和嘴巴是组合 关系。
在代码实现组合关系时,通常在整体类的构造方法中直接实例化成员类。
4.依赖关系:
依赖关系时一种 使用关系 。特定事物的改变有可能会影响到使用该事物的其他事物,在需要表示一个事物使用另一个事物是使用依赖关系。大多数情况下,依赖关系提现在某个类的方法使用另一个类对象做为参数。在UML
中,依赖关系用带箭头的虚线表示,由依赖的一方指向被依赖的一方。
例如,驾驶员开车,在Driver
类的drive()
方法中,将Car
类型的对象car
作为一个参数传递,以便在drive()
方法中能够调用Car
类的move()
方法,且驾驶员的drive()
方法依赖车的move()
方法,因此类Driver
依赖类Car
。
依赖关系通常通过3种方式来实现。第一种也是最常用的一种方式,将一个类的对象作为另一个类中方法的参数;第2种方式是在一个类的方法中将另一个类的对象作为其 局部变量;第3种方式是在一个类的方法中调用另一个类的静态方法。
5.泛化关系:
泛化关系也就是继承关系,用于描述父类与子类之间的关系,父类又称作基类或超类,子类又称作派生类。
在UML
中,泛化关系用空心三角形的直线来表示。在代码实现时,使用面向对象的继承机制来实现泛化关系,如在Java
语言中使用的是extend
是关键字。
例如,Student
类和Teacher
类都是属于Person
类的子类,Student
类和Teacher
类继承了Person
类的属性和方法,Person
类的属性包含了姓名(name
) 和年龄(age
),每一个Student
和Teacher
也都具有这两个属性,另外Student
类增加了属性学号(studentNo
),Teacher
类增加了属性教师编号(teacherNo
),Person
类的方法包括行走move()
和说话say()
,Student
类和Teacher
类继承了这两个方法,而且Student
类还新增方法study()
,Teacher
类新增方法teach()
。
6.实现关系:
UML
中用与类的表示法类似的方式表示接口。
接口之间也可以有与类之间关系类似的继承关系和依赖关系,但是接口和类之间还存在一种实现关系。在这种关系中,类实现了接口,类中的操作实现了接口中所声明的操作。在UML中,类与接口之间的实现关系用带空心三角形的虚线来表示。
例如,定义一个交通工具接口Vehicle
,包含一个抽象操作move()
,在类Ship
和类Car
中都实现了该move()
操作,但是具体实现细节将会不一样。
二、常用面向对象设计原则的定义
1.单一职责原则:
一个类只负责一个功能领域中的相应职责。或者可以定义为:就一个类而言,应该只有一个引起它变化的原因。
2.开闭原则:
一个软件实体应当对扩展开发,对修改关闭。即软件实体应尽量在不修改原有代码的情况下进行扩展。
3.里式代换原则:
所有引用基类(父类)的地方必须能够透明地使用其子类的对象。
里式代换原则表明,在软件中将一个基类对象替换成它的子类对象,程序将不会产生任何错误和异常,反过来则不成立,如果一个软件实体使用的是一个子类对象的话,那么它不一定能够使用基类对象。
例如,我喜欢动物,那么我一定喜欢狗,因为狗是动物的子类;但是我喜欢狗,不能据此断定我喜欢动物,因为我并不喜欢老鼠,虽然它也是动物。
里式代换原则是实现开闭原则的重要方式之一,由于使用基类对象的地方都可以使用子类对象,因此在程序中尽量使用基类类型来对对象进行定义,而在运行时再确定其子类类型,使用子类对象来替代父类对象。
4.依赖倒转原则:
抽象不应该依赖于细节,细节应当依赖于抽象。换言之,要针对接口编程,而不是针对实现编程。
依赖倒转原则要求在程序代码中传递参数时或在关联关系中,尽量引用层次高的抽象层类,即使用接口和抽象类进行变量类型声明、参数类型声明、方法返回类型声明,以及数据类型的转换等,而不要用具体类来做这些事情。为了确保该原则的应用,一个具体类应当只实现接口或抽象类中声明过的方法,而不要给出多余的方法,否则将无法调用到在子类中增加的新方法。
本章前面在讲依赖关系的时候有一个Driver
与Car
的例子,Driver
依赖于Car
,如果要满足依赖倒转原则的话,应该从Car
抽象出Vehicle
,在传入Driver
时使用Vehicle
,那么就是
Driver
依赖于Vehicle
,即细节依赖于抽象。
下面通过一个简单实例来加深对开闭原则、里氏代换原则和依赖倒转原则的理解。
现存在一系统,将存储在TXT 或Excel 文件中的客户信息转存到数据库中,在存储之前,需要要进行数据格式转换。
CustomeDAO
依赖TXTDataConvertor
和ExcelDataConvertor
。
由于每次转换数据时数据来源不一定相同,因此需要更换数据转换类,如有时候需要将TXTDataConvertor
改为ExcelDataConvertor
, 此时,需要修改
CustomeDAO
的源代码,而且在引入并使用新的数据转换类时也不得不修改
CustomeDAO
的源代码,系统扩展性较差,违反了开闭原则,现需要对该方案进行重构。
个人理解这段话的意思是:一开始只有TXTDataConvertor
,CustomeDAO
依赖于TXTDataConvertor
,到了后面增加了数据来源,需要增加ExcelDataConvertor
依赖,因此需要修改CustomeDAO
源代码。这样就违反了开闭原则。
可以通过引入抽象数据转换类解决该问题。在引入抽象数据转换类DataConvertor
之后,CustomeDAO
针对抽象类DataConvertor
编程,而将具体数据转换类名存储在配置文件中,符合依赖倒转原则。
在上述重构过程中,使用了开闭原则、里氏代换原则和依赖倒转原则。在大多数情况下,这3 个设计原则会同时出现,开闭原则是目标,里氏代换原则是基础,依赖倒转原则是手段,它们相辅相成,相互补充,目标一致,只是分析问题时所站角度不同而已。
ps:看到这里我才发现,原来有时候设计原则不能孤立去看,它们有时候是会一起出现的。以前总是一个个的去看,太片面,对整体把握度不够。
5.接口隔离原则:
使用多个专门的接口,而不使用单一的总接口,即客户端不应该依赖那些它不需要的接口。
下面通过一个简单实例来加深对接口隔离原则的理解。
6.合成复用原则:
尽量使用对象组合,而不是继承来达到复用的目的。
7.迪米特法则:
一个软件实体应当尽可能少地与其他实体发生相互作用。
读后感:虽然原则的定义部分是生涩难懂的,但是由于这篇文章关于面向对象设计原则的解释都给出了对应的实例去解释,而且例子通俗易懂。因此对于设计原则的知识点,虽然不能说是完全理解,但是看完这一章,还是可以学习到了一些东西的。