一、常用UML类图示例
UML关系详解:http://justsee.iteye.com/blog/808799
常见的基数及含义(聚合关系的使用举例):
0..1:0 或1 个实例.
0..*: 对实例的数目没有限制.
1: 只能有一个实例.
1..*: 至少有一个实例.
1. 泛化(Generalization)
【泛化关系】:是一种继承关系,表示一般与特殊的关系,它指定了子类如何特化父类的所有特征和行为。例如:老虎是动物的一种,即有老虎的特性也有动物的共性。
【箭头指向】:带三角箭头的实线,箭头指向父类
2. 实现(Realization)
【实现关系】:是一种类与接口的关系,表示类是接口所有特征和行为的实现.
【箭头指向】:带三角箭头的虚线,箭头指向接口
3. 关联(Association)
【关联关系】:是一种拥有的关系,它使一个类知道另一个类的属性和方法;如:老师与学生,丈夫与妻子关联可以是双向的,也可以是单向的。双向的关联可以有两个箭头或者没有箭头,单向的关联有一个箭头
【代码体现】:成员变量
【箭头及指向】:带普通箭头的实心线,指向被拥有者
上图中,老师与学生是双向关联,老师有多名学生,学生也可能有多名老师。但学生与某课程间的关系为单向关联,一名学生可能要上多门课程,课程是个抽象的东西他不拥有学生
4. 聚合(Aggregation)
【聚合关系】:是整体与部分的关系,且部分可以离开整体而单独存在。如车和轮胎是整体和部分的关系,轮胎离开车仍然可以存在。
聚合关系是关联关系的一种,是强的关联关系;关联和聚合在语法上无法区分,必须考察具体的逻辑关系。
【代码体现】:成员变量
【箭头及指向】:带空心菱形的实心线,菱形指向整体
5. 组合(Composition)
【组合关系】:是整体与部分的关系,但部分不能离开整体而单独存在。如公司和部门是整体和部分的关系,没有公司就不存在部门。
组合关系是关联关系的一种,是比聚合关系还要强的关系,它要求普通的聚合关系中代表整体的对象负责代表部分的对象的生命周期。
【代码体现】:成员变量
【箭头及指向】:带实心菱形的实线,菱形指向整体
6. 依赖(Dependency)
【依赖关系】:是一种使用的关系,即一个类的实现需要另一个类的协助,所以要尽量不使用双向的互相依赖.
【代码表现】:局部变量、方法的参数或者对静态方法的调用
【箭头及指向】:带箭头的虚线,指向被使用者(即被依赖的对象)
各种关系的强弱顺序:
泛化 = 实现 > 组合 > 聚合 > 关联 > 依赖
注意:关联和依赖的一个简单判断原则:某个类以成员变量的形式出现在另一个类中,二者是关联关系;某个类以局部变量的形式出现在另一个类中,二者是依赖关系。
二、C++中隐藏、重载、覆盖(重写)区别
三、6大设计原则
1、单一职责原则(SRP)Single Responsibility Principle
定义:就一个类而言,应该仅有一个引起它变化的原因。
问题由来:类T负责两个不同的职责:职责P1,职责P2。当由于职责P1需求发生改变而需要修改类T时,有可能会导致原本运行正常的职责P2功能发生故障。
解决方案:遵循单一职责原则。分别建立两个类T1、T2,使T1完成职责P1功能,T2完成职责P2功能。这样,当修改类T1时,不会使职责P2发生故障风险;同理,当修改T2时,也不会使职责P1发生故障风险。(若类T是的单一职责难以做到,可以做成方法级别上的单一职责)
方法优点:
l 可以降低类的复杂度,一个类只负责一项职责,其逻辑肯定要比负责多项职责简单的多;
l 提高类的可读性,提高系统的可维护性;
l 变更引起的风险降低,变更是必然的,如果单一职责原则遵守的好,当修改一个功能时,可以显著降低对其他功能的影响。
需要说明的一点是单一职责原则不只是面向对象编程思想所特有的,只要是模块化的程序设计,都需要遵循这一重要原则。
【观点】:
A、接口一定要做到单一职责;
B、类的设计尽量做到只有一个原因引起变更
2、里氏替换原则(LSP)Liskov Substitution Principle
定义1:子类必须可以替换掉它们的父类型
定义2:如果对每一个类型为 T1的对象 o1,都有类型为 T2 的对象o2,使得以 T1定义的所有程序 P 在所有的对象 o1 都代换成 o2 时,程序 P 的行为没有发生变化,那么类型 T2 是类型 T1 的子类型。
问题由来:有一功能P1,由类A完成。现需要将功能P1进行扩展,扩展后的功能为P,其中P由原有功能P1与新功能P2组成。新功能P由类A的子类B来完成,则子类B在完成新功能P2的同时,有可能会导致原有功能P1发生故障。
解决方案:当使用继承时,遵循里氏替换原则。类B继承类A时,除添加新的方法完成新增功能P2外,尽量不要重写父类A的方法,也尽量不要重载父类A的方法(若重写,需要符合里氏替换原则)。在实际编程中,我们常常会通过重写父类的方法来完成新的功能,这样写起来虽然简单,但是整个继承体系的可复用性会比较差,特别是运用多态比较频繁时,程序运行出错的几率非常大。如果非要重写父类的方法,比较通用的做法是:原来的父类和子类都继承一个更通俗的基类,原有的继承关系去掉,采用依赖、聚合,组合等关系代替。
【4层含义】
子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法(理论场景,但是实际实现都有覆盖存在)。
子类中可以增加自己特有的方法。
当子类的方法重写父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。
当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。
【里氏替换原则关键点】
1)、里氏替换原则针对的是C++中的覆盖(重写)(即多态实现),若函数实现有“隐藏关系”,切记甄别与修改,隐藏关系会带来很多隐式的问题;
2)、当子类重写父类方法时,一般调用原则是先直接调用父类的方法,然后根据新增场景进行扩展。(该点符合 里氏替换原则 中关于“当子类的方法重写父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松”这样就可以进行参数判断,进行场景扩展。
3)、子类重写方法后,函数的返回值要比父类更严格,最好是父类包含情况的一种,这样写出的函数接口 无论是父类调用,还是子类调用,都不会违反里氏替换原则了。
4)、里氏转换原则要求子类从抽象继承而不是从具体继承,如果从抽象继承,子类必然要重写父类方法。因此里氏转换原则和多态是相辅相成的!
注意:“里氏替换原则”并不是要阻止多态的实现,而是指导更好的实现多态,避免发生更多意想不到的问题。
3、依赖倒置原则( Dependence Inversion Principle, DIP)
定义:定义的理解可以分为两部分
1)、高层模块不应该依赖低层模块,二者都应该依赖抽象;
2)、抽象不应该依赖细节,细节应该依赖抽象。
定义理解后的深层次含义为“面向接口编程”
问题由来:类A直接依赖类B,假如要将类A改为依赖类C,则必须通过修改类A的代码来达成。这种场景下,类A一般是高层模块,负责复杂的业务逻辑;类B和类C是低层模块,负责基本的原子操作;假如修改类A,会给程序带来不必要的风险。
解决方案:将类A修改为依赖接口I,类B和类C各自实现接口I,类A通过接口I间接与类B或者类C发生联系,则会大大降低修改类A的几率
【传递依赖关系的三种方式】
A、直接通过调用的接口传递;
B、构造方法传递;
C、Setter方法传递(即提供一个接口用来设置不同的依赖对象给当前类对象使用)
4、接口隔离原则
定义:客户端不应该依赖它不需要的接口;一个类对另一个类的依赖应该建立在最小的接口上。
问题由来:类A通过接口I依赖类B,类C通过接口I依赖类D,如果接口I对于类A和类B来说不是最小接口,则类B和类D必须去实现他们不需要的方法。
解决方案:
将臃肿的接口I拆分为独立的几个接口,类A和类C分别与他们需要的接口建立依赖关系。也就是采用接口隔离原则
《————————》
5、迪米特原则(最少知识原则)
定义:一个对象应该对其他对象保持最少的了解。
问题由来:类与类之间的关系越密切,耦合度越大,当一个类发生改变时,对另一个类的影响也越大。
解决方案:尽量降低类与类之间的耦合。
注意 迪米特法则要求类“羞涩”一点, 尽量不要对外公布太多的public方法和非静态的public变量, 尽量内敛, 多使用private、 package-private、 protected等访问权限。
6、开闭原则
定义:一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。
问题由来:在软件的生命周期内,因为变化、升级和维护等原因需要对软件原有代码进行修改时,可能会给旧代码中引入错误,也可能会使我们不得不对整个功能进行重构,并且需要原有代码经过重新测试。
含义:用抽象构建框架,用实现扩展细节
总结:
1、单一职责原则告诉我们实现类要职责单一;(类的设计尽量做到只有一个原因引起变更,接口实现一定要满足单一职责);
2、里氏替换原则告诉我们不要破坏继承体系;(子类面向抽象继承,而不是具体继承,若要实现具体方法,则要宽入严出,子能替父的效果)
3、依赖倒置原则告诉我们要面向接口编程;(依赖的对象为接口类,这样才能在不改变主类接口的前提下,扩充客类(被依赖的类),实现功能多样化)
4、接口隔离原则告诉我们在设计接口的时候要精简单一;(接口类方法合理划分,子类继承时可以避免实现不需要的方法)
5、迪米特法则告诉我们要降低耦合;(类的设计尽量内敛,提供的接口越少,其它类使用时耦合度才越小。能用组合、聚合替代时,尽量不使用继承)
6、开闭原则是总纲,他告诉我们要对扩展开放,对修改关闭。(已有的代码尽量不修改,新增的功能使用扩展类或扩展方法的方式,综合能力的考验在此体现)