UML类图和设计模式的七大设计原则总结(全方面详细总结)

856 阅读12分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第30天,点击查看活动详情

一.UML类图图示样例

在这里插入图片描述


二.对上述图片中的内容具体分析

1.类的表示

在这里插入图片描述

  • 首先来看动物矩形框,它代表一个类。
  • 类图分三层:
    • 第一层显示类的名称,如果是抽象类,则用斜体显示;
    • 第二层是类的特性,也就是属性;
    • 第三层是类的操作,也就是方法。
  • 属性、方法前面的 '+'表示public ; '-' 表示private; '#' protected。

2.接口的表示

在这里插入图片描述

  • 接口的表示与类有些区别,接口的顶端有<<interface>>显示。
  • 第一行是接口的名称。
  • 第二行是接口的方法。

接口还有一种表示方法,俗称棒棒糖表示法,如下: 在这里插入图片描述

也就是说唐老鸭这个类实现了讲话人这个接口,接口中的讲话()方法在唐老鸭这个实现类中得到实现。

3.依赖(Dependency)关系

依赖关系用虚线箭头来表示,如下: 在这里插入图片描述

只要是在类中用到了对方,那么他们之间就存在依赖关系。具体如下:

  • 类的成员属性
  • 方法的返回类型
  • 方法接收的参数类型
  • 方法中使用到

举例:在类A的某个方法中,其方法的参数类型为类B、类C,则类A与类B、类C是依赖关系,代码如下:

public abstract class Animal {
    public void metabolism(Oxygen oxygen,Water water){
    }
}

4.继承关系(泛化关系)

继承关系用空心三角形+实线来表示,如下:

在这里插入图片描述

  • 泛化关系实际上就是继承关系,他是依赖关系的特例。
  • 如果A类继承了B类,我们就说A和B存在泛化关系。

5.实现关系

实现接口用空心三角形+虚线来表示,如下:

在这里插入图片描述

  • 实现关系实际上就是A类实现B接口,他是依赖关系的特例。

6.关联(association)关系

关联关系用实线箭头来表示,如下:

在这里插入图片描述

  • 关联关系实际上就是类与类之间的联系,他是依赖关系的特例
  • 关联具有导航性:即双向关系或单向关系
  • 关系具有多重性:如“1”(表示有且仅有一个),“0...”(表示0个或者多个), “0,1”(表示0个或者一个),“n...m”(表示n到 m个都可以),“m...*”(表示至少m 个)。
  • 单向一对一关系 在这里插入图片描述
public class Person { 
	private IDCard card;
}
public class IDCard{}
  • 双向一对一关系 在这里插入图片描述
public class Person { 
	private IDCard card;
}
public class IDCard{
	private Person person 
}

举例:上述中的企鹅需要知道气候的规律,才能够进行一些活动,如下蛋,捕食等等,当一个类中引用到另一个类时,这个时候就是关联关系,如下面的代码:

Class Penguin extends Bird {
	//在企鹅Penguin类中引用了气候Climate类
	private Climate climate;
}

7.聚合(Aggregation)关系

聚合关系用空心的菱形+实线箭头来表示,如下:

在这里插入图片描述

  • 聚合关系是一种弱的“拥有”关系。
  • 聚合关系(Aggregation)表示的是整体和部分的关系,整体与部分可以分开。
  • 聚合关系是关联关系的特例,所以他具有关联的导航性与多重性。
  • 比如一台电脑由键盘、显示器,鼠标等组成;组成电脑的各个配件是可以从电脑上分离出来的 在这里插入图片描述

例子:比如雁群和大雁,每只大雁都是属于一个雁群,一个雁群可以有多只大雁,所以它们之间就满足聚合关系,如下代码:

public class WideGooseAggregate {
	//在雁群WideGooseAggregate类中,有大雁数组对象arrayWideGoose
    private WideGoose[] arrayWideGoose;
}

8.组合(Composition)关系(也可翻译为合成关系)

合成关系用实心的菱形+实线箭头来表示,如下: 在这里插入图片描述

  • 合成关系是一种强的“拥有”关系。
  • 组合关系:也是整体与部分的关系,但是整体与部分不可以分开,体现了严格的部分和整体的关系,部分和整体的生命周期一样。
  • 合成关系连线的两端有一个数字1和2,这被称为基数,表明这一端的类可以有几个实例:
    • 在该例子中1和2表示一个鸟有两只翅膀;
    • 如果一个类可能有无数个实例,则就用n来表示;
    • 另外关联关系,聚合关系也可以有基数。
  • 简单来说,在A类中的构造器中实例化B类(创建B类对象),它们之间同时生成。 例子:比如鸟和翅膀,因为它们是部分和整体的关系,并且翅膀和鸟的生命周期是相同的,如下代码:
public class Bird {
    private Wing wing;
    public Bird(){
    	//在Bird类的构造器中创建Wing类对象
        wing = new Wing();
    }
}

二.设计模式的七大设计原则总结

1.单一职责原则

  • 对类来说,一个类应该只负责一项职责(并不是一个类只有一个方法,可以有多个方法,这些方法共同完成一项职责)。
  • 如果一个类承担的职责过多,就等于把这些职责耦合在一起,一个职责的变化可能会削弱或者抑制这个类完成其他职责的能力;这种耦合会导致脆弱的设计,当发生变化时,设计会遭受到意想不到的破坏。
  • 软件设计真正要做的许多内容,就是要发现职责并把那些职责相互分离。
  • 如果你能够想到多于一个的动机去改变一个类,那么这个类就具有多于一个的职责。
  • 通常情况下,我们应当遵守单一职责原则,只有当逻辑足够简单时,才可能在代码级违反单一职责原则;类中方法数量足够少时,可以在方法级别保持单一职责原则。
  • 单一职责原则的作用:
    • 降低类的复杂度,一个类只负责一项职责。
    • 提高类的可读性,可维护性。
    • 降低变更引起的风险。

2.里氏替换原则

  • 问题的提出:
    • 继承包含这样一层含义:父类中凡是已经实现好的方法,实际上是在设定规范和契约,虽然它不强制要求所有的子类必须遵循这些契约,但是如果子类对这些已经实现的方法任意修改,就会对整个继承体系造成破坏。
    • 继承在给程序设计带来便利的同时,也带来了弊端。比如使用继承会给程序带来侵入性,程序的可移植性降低,增加对象间的耦合性,如果一个类被其他的类所继承, 则当这个类需要修改时,必须考虑到所有的子类,并且父类修改后,所有涉及到子类的功能都有可能产生故障。
    • 在编程中,如何正确的使用继承? => 由此提出里氏替换原则
  • 里氏替换原则(Liskov Substitution Principle)在1988年,由麻省理工学院的Barbara Liskov女士提出的。
  • 它的内容白话翻译就是一个软件实体如果使用的是一个父类的话,那么一定适用于其子类,而且它察觉不出父类对象和子类对象的区别;也就是说,在软件里面,把父类都替换成它的子类,程序的行为没有变化。
  • 简单来说,子类型必须能够替换掉它们的父类型。
  • 只有当子类可以替换掉父类,软件单位的功能不受到影响时,父类才能真正被复用,而子类也能够在父类的基础上增加新的行为。
  • 由于子类型的可替换性才使得使用父类类型的模块在无需修改的情况下就可以扩展。
  • 里氏替换原则注意点:
    • 在使用继承时,遵循里氏替换原则,在子类中尽量不要重写父类的方法。
    • 里氏替换原则告诉我们,继承实际上让两个类耦合性增强了,在适当的情况下,可以让原来的父类和子类都继承一个更通俗的基类,原有的继承关系去掉, 采用依赖,聚合,组合等关系代替。

3.依赖倒转原则

  • 高层模块不应该依赖底层模块,二者都应该依赖抽象。也就是说我们可以去依赖接口,可以去依赖抽象类,但是我们不要去依赖一个具体的子类。
  • 抽象不应该依赖细节,细节应该依赖抽象。也就是说我们要针对接口编程,不要对实现编程。
  • 依赖倒转原则是基于这样的设计理念:相对于细节的多变性,抽象的东西要稳定的多。以抽象为基础搭建的架构比以细节为基础的架构要稳定的多,在Java中,抽象指的是接口或抽象类,细节就是具体的实现类。
  • 使用接口或抽象类的目的是制定好规范,而不涉及任何具体的操作,把展现细节的任务交给它们的实现类去完成。
  • 使用依赖倒转原则的细节:
    • 底层模块尽量都要有抽象类或接口,或者两者都有,程序稳定性好。
    • 变量的声明类型尽量是抽象类或接口,这样我们的变量引用和实际对象间就存在一个缓冲层,利于程序的扩展和优化。
    • 继承时遵循里氏替换原则。

4.接口隔离原则

  • 客户端不应该依赖它不需要的接口,即一个类对另一个类的依赖应该建立在最小的接口上。
  • 图示讲解: 在这里插入图片描述
    • 类A通过接口Interface1依赖类B,类C通过接口Interface1依赖类D,如果接口Interface1对于类A和类C来说不是最小接口, 那么类B和类D必须去实现他们不需要的方法。
    • 按隔离原则应当这样处理: 将接口Interface1拆分为独立的几个接口, 类A和类C分别与他们需要的接口建立依赖 关系。也就是采用接口隔离原则,如下:

在这里插入图片描述

5.开闭原则

  • 开闭原则(Open Closed Principle)是编程中最基础、最重要的设计原则。
  • 开闭原则是说软件实体(类、模块、函数等等)应该可以扩展,但是不可修改。
  • 也就是说对于扩展是开放的,对于修改是封闭的。
  • 编程中遵循其它原则,以及使用设计模式的目的就是遵循开闭原则。
  • 无论模块是多么的“封闭”,都会存在一些无法对之封闭的变化。既然不可能完全封闭,设计人员必须对于他设计的模块应该对哪种变化封闭做出选择。他必须先猜出最有可能发生的变化种类,然后构造抽象来隔离那些变化。
  • 面对需求,对程序对改动是通过增加新代码进行对,而不是更改现有的代码。
  • 我们希望的是在开发工作展开不久就知道可能发生的变化,查明可能发生的变化所等待的时间越长,要创建正确的抽象就越困难。
  • 开闭原则是面向对象设计的核心所在。遵循这个原则可以带来面向对象技术所声称的巨大好处,也就是可维护、可扩展、可复用、灵活性好。开发人员应该仅对程序中呈现出频繁变化的那些部分做出抽象;然而,对于应用程序中的每个部分都刻意地进行抽象同样不是一个好注意。拒绝不成熟的抽象和抽象本身一样重要。

6. 迪米特法则

  • 一个对象应该对其他对象保持最少的了解。
  • 类与类关系越密切,耦合度越大。
  • 迪米特法则(Demeter Principle)又叫最少知道原则,即一个类对自己依赖的类知道的越少越好;如果两个类不必彼此直接通信,那么这两个类就不应当发生直接的相互作用。如果其中一个类需要调用另一个类的某一个方法的话,可以通过第三者转发这个调用。
  • 迪米特法则首先强调的前提是在类的结构设计上,每一个类都应当尽量降低成员的访问权限,也就是说,一个类包装好自己的private状态,不需要让别的类知道的字段或行为就不要公开。
  • 迪米特法则其根本思想是强调了类之间的松耦合。
  • 类之间的耦合越弱,越有利于复用,一个处在弱耦合的类被修改,不会对有关系的类造成波及。也就是说,信息的隐藏促进了软件的复用。
  • 迪米特法则还有个更简单的定义:只与直接的朋友通信。何为直接的朋友:
    • 直接的朋友:每个对象都会与其他对象有耦合关系,只要两个对象之间有耦合关系, 我们就说这两个对象之间是朋友关系。耦合的方式很多,依赖,关联,组合,聚合等。其中,我们称出现成员变量,方法参数,方法返回值中的类为直接的朋友,而出现在局部变量中的类不是直接的朋友。也就是说,陌生的类最好不要以局部变量的形式出现在类的内部。

7.合成复用原则

  • 原则是尽量使用合成/聚合的方式,而不是使用继承。
  • 举个例子,如果A类中有两个方法,我们想让B类中能够用到A类中的两个方法,我们有以下几种方法:
    • 让B类继承A类,这样B类就可以使用A类中的方法了,但是这种方式要尽量少用,因为它提高了类与类之间的耦合。
    • 让B类中某个方法的形参类型为A类
    • 让B类中某个属性的类型为A类

8.设计原则核心思想总结

  • 找出应用中可能需要变化之处,把它们独立出来,不要和那些不需要变化的代码混在一起。
  • 针对接口编程,而不是针对实现编程。
  • 为了交互对象之间的松耦合设计而努力。