本文已参与「新人创作礼」活动,一起开启掘金创作之路。
@TOC
前言
设计原则
单一职责原则(Single Responsibility Principle)
定义
不要存在多于一个导致类变更的原因。通俗的说,即一个类只负责一项职责。
问题由来
类 T 负责两个不同的职责:职责 P1,职责 P2。 当由于职责 P1 需求发生改变而需 要修改类 T 时,有可能会导致原本运行正常的职责 P2 功能发生故障。
解决方案
将类 T 分成两个不同的类来实现。比如 C 语言中会将头文件分类 string.h stdio.h, Qt 中 QLable QButton 等。C++中会有 string fstream 类,就是单一原则的体现。
开闭原则(Open Closed Principle)
定义
一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。
问题由来
在软件的生命周期内,因为变化、升级和维护等原因需要对软件原有代码进行修改 时,可能会给旧代码中引入错误,也可能会使我们不得不对整个功能进行重构,并且需 要原有代码经过重新测试。
解决方案
当软件需要变化时,尽量通过扩展软件实体的行为来实现变化,而不是通过修改已 有的代码来实现变化。
例如: man 可以做doAction, 不考虑开闭,就是man添加 jump,run,read方法。直接使用 但是这样每次都会修改 影响代码的维护。
class Man{
public:
void jump();
void read();
void run();
}
void main(){
Man *aman = new Man;
man.jump();
man.read();
man.run();
}
遵循开闭原则的话,man就通过调用Ido的do()来实现动作 也不需要每个man把三个对象都调用一遍
class Ido{
public:
virtual void do() = 0;
}
class Read :public Ido{
void do(){
//read();
}
}
class Run:public Ido{
void do(){
//run();
}
}
class Jump:public Ido{
void do(){
//jump();
}
}
class Man{
public:
void doAction(Ido *doptr){ doptr.do()}
}
void main(){
Man *aman = new Man;
Ido* it= new Read();
man.doAction(it);
Ido* it2= new Run();
man.doAction(it2);
Ido* it3= new Jump();
man.doAction(it3);
}
依赖倒置原则(Dependence Inversion Principle)
定义
高层模块不应该依赖低层模块,二者都应该依赖其抽象;抽象不应该依赖细节;细 节应该依赖抽象。
问题由来
类 A 直接依赖类 B,假如要将类 A 改为依赖类 C,则必须通过修改类 A 的代码来达成。这种场景下,类 A 一般是高层模块,负责复杂的业务逻辑;类 B 和类 C 是低层模 块,负责基本的原子操作;假如修改类 A,会给程序带来不必要的风险。
解决办法
接口分离原则(Interface Segregation Principle)
定义
客户端不应该依赖它不需要的接口;一个类对另一个类的依赖应该建立在最小的接 口上。
问题由来
类 A 通过接口 I 依赖类 B,类 C 通过接口 I 依赖类 D,如果接口 I 对于类 A 和类 B 来说不是最小接口,则类 B 和类 D 必须去实现他们不需要的方法。
解决办法
将臃肿的接口 I 拆分为独立的几个接口,类 A 和类 C 分别与他们需要的接口建立依 赖关系。也就是采用接口隔离原则。
迪米特法则(Law of Demeter)
定义
面向对象有一个概念是迪米特法则,其规则如下: 每个对象对其他对象的认识必须限制,只能与自己最近的对象 每个对象应当只和它的朋友联系,而不是陌生人 每个对象应当只和他的直接朋友联系。
问题由来
在于降低类之间的耦合。由于每个类尽量减少对其他类的依赖,因此,很容易使得系统的功能模块功能独立,相互之间不存在(或很少有)依赖关系。
解决方案
迪米特法则不希望类之间建立直接的联系。如果真的有需要建立联系,也希望能通过它的友元类来转达。因此,应用迪米特法则有可能造成的一个后果就是:系统中存在大量的中介类,这些类之所以存在完全是为了传递类之间的相互调用关系——这在一定程度上增加了系统的复杂度。
设计模式的门面模式(Facade)和中介模式(Mediator),都是迪米特法则应用的例子。
里氏替换原则(Liskov Substitution Principle)
定义
如果对每一个类型为 T1 的对象 o1,都有类型为 T2 的对象 o2,使得以 T1 定义的所有程序 P 在所有的对象 o1 都代换成 o2 时,程序 P 的行为没有发生变化,那 么类型 T2 是类型 T1 的子类型。 所有引用基类的地方必须能透明地使用其子类的对象
问题由来
当使用继承时,遵循里氏替换原则。类 B 继承类 A 时,除添加新的方法完成新增 功能 P2 外,尽量不要 shadow(遮盖)父类 A 的方法。 继承作为面向对象三大特性之一,在给程序设计带来巨大便利的同时,也带来了弊 端。
比如使用继承会给程序带来侵入性,程序的可移植性降低,增加了对象间的耦合性, 如果一个类被其他的类所继承,则当这个类需要修改时,必须考虑到所有的子类,并且 父类修改后,所有涉及到子类的功能都有可能会产生故障。
优点: 1. 提高代码的重用性,子类拥有父类的方法和属性; 2. 提高代码的可扩展性,子类可形似于父类,但异于父类,保留自我的特性; 缺点:侵入性、不够灵活、高耦合 1. 继承是侵入性的,只要继承就必须拥有父类的所有方法和属性,在一定程度上约束了子类,降低了代码的灵活性; 2. 增加了耦合,当父类的常量、变量或者方法被修改了,需要考虑子类的修改,所以一旦父类有了变动,很可能会造成非常糟糕的结果,要重构大量的代码。
解决方案
里氏替换原则通俗的来讲就是: 子类可以扩展父类的功能,但不能改变父类原有的 功能。 它包含以下 4层含义: 1.子类必须实现父类的抽象方法,但不得重写(覆盖)父类的非抽象(已实现)方法。 2.子类中可以增加自己特有的方法。 3.当子类覆盖或实现父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。 4.当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。
用我自己的话讲就是: 子类可以完全替换父类,而不影响程序编译;尽量不要重写父类。
UML图
设计视图
类的关系
类图
泛化
定义 是一种继承关系, 表示一般与特殊的关系, 它指定了子类如何特化父类的所有特征 和行为。
箭头指向 带三角箭头的实线,箭头指向父类。
类图关系:
实现
定义 是一种类与接口的关系,表示类是接口所有特征和行为的实现
箭头指向 带三角箭头的虚线,箭头指向接口。
类图关系
关联
组合关联
定义 组合也是关联关系的一种特例,他体现的是一种 contains-a 的关系,这种关系比聚 合更强,也称为强聚合;他同样体现整体与部分间的关系,但此时整体与部分是不可分 的,它要求代表整体的对象负责代表部分的对象的生命周期整体的生命周期结束也就意 味着部分的生命周期结束。比如你和你的大脑。 表现在代码层面,和关联关系是一致的,只能从语义级别来区分; 组件组成个体。
箭头及指向 带实心菱形的实线,菱形指向整体
类图关系
聚合关联
定义 聚合是关联关系的一种特例,他体现的是整体与部分、拥有的关系,即 has-a 的关 系,此时整体与部分之间是可分离的,他们可以具有各自的生命周期,部分可以属于多 个整体对象,也可以为多个整体对象共享。比如公司与员工的关系等。 表现在代码层面,和关联关系是一致的,只能从语义级别来区分。
个体组成整体
箭头及指向 带空心菱形的实心线,菱形指向整体。
类图关系
组合和聚合
普通关联
是一种拥有的关系, 它使一个类知道另一个类的属性和方法,强调的是一种 A-A 的 关系;关联可以是双向的,也可以是单向的。双向的关联可以有两个箭头或者没有箭头, 单向的关联有一个箭头。
在代码层面,通常体现为成员变量的关系。
箭头及指向 带普通箭头的实心线,指向被拥有者
类图关系
依赖关联
定义 是一种使用的关系,即一个类的实现需要另一个类的协助,所以要尽量不使用双向 的互相依赖。 比如某人要过河,需要借用一条船,此时人与船之间的关系就是依赖;表现在代码 层面,为类 B 作为参数被类 A 在某个 method 方法中使用。 局部变量、方法的参数或者对静态方法的调用
箭头指向 带箭头的虚线,指向被使用者
类图关系
对象图
进程视图
序列图(时序图,顺序图)
定义
捕捉一段时间内多个对象间的交互信息,强调消息交互的时间顺序
图示
协作图
状态图
活动图
定义
活动图则强调的是从活动到活动的控制流。 活动图是一种表述过程基理、业务过程以及工作流的技术。它可以用来对业务过程、工作流建模,也可以对用例实现甚至是程序实现来建模。 活动图在本质上是一种流程图。
图示
实现视图
构件图
用例视图
用例图
定义
用例是在系统中执行的一系列动作,这些动作将生成特定执行者可见的价值结果。一个用例定义一组用例实例。
简单而言,就是用户的基本操作。
图示
UML实战
UML在需求分析与系统设计中之实战讲解(完整UML图形演示)