讨伐设计模式——基本原则篇

本文是学习设计模式时做的笔记,原视频链接:

https://www.bilibili.com/video/BV1G4411c7N4

目录

设计模式

UML

UML类图

类的组成

类之间的关系

依赖(Dependence)

泛化(Generalization)

实现(Implementation)

扫描二维码关注公众号,回复: 12757004 查看本文章

关联(Association)

聚合(Aggregation)和组合(Composition)

设计模式七大原则

单一职责原则

接口隔离原则

依赖倒转原则

里氏替换原则

开闭原则

迪米特法则

合成复用原则


设计模式

1.什么是设计模式?

设计模式是对软件设计中普遍存在(反复出现)的各种问题,所提出的解决方案。

2.为什么要使用设计模式?

使用设计模式,程序可以具有更好的:

  • 代码重用性:实现相同功能的代码,不需要重复编写;
  • 可读性:增强编程的规范性,便于其他程序员阅读;
  • 可扩展性:在原程序上添加新的功能非常方便;
  • 可靠性:添加新的功能后,对原来的功能没有影响;
  • 内聚性、耦合性:即“高内聚、低耦合”。

设计模式包含了面向对象的精髓。

视频采用图解方式讲解设计模式,在学习之前应对UML图有一定了解。

如果已经掌握了UML图,可以跳过此部分。

UML

UML(Unified Modeling Language,统一建模语言),是一种用于软件分析和设计的语言工具。

UML本质上是一套符号规定,这些符号用于描述 软件模型中的各个元素(如类、接口)和它们之间的关系(如泛化、实现)。

UML图分类:

静态结构图:类图、对象图、包图、组件图、部署图。

动态行为图:交互图、状态图、活动图。

其中,类图用来描述类与类之间的关系,在讲解设计模式时,主要是用到类图。

UML类图

UML类图用于描述 系统中的类 本身的组成类之间的各种静态关系

类的组成

我们将下面这个类画成UML图:

public class Person{
	private Integer id;
	private String name;

	public void setName(String name) {
		this.name = name;
	}

	public String getName() {
		return name;
	}
}

UML类图:

可以看到,UML类图中的类包含的信息有:类名(Person),成员变量(id、name),成员方法(setName、getName)。

其中,成员变量信息包括成员变量名和成员变量类型,两者之间用":"隔开;成员方法信息包括成员方法名、参数列表、返回值类型。

类之间的关系

类之间的关系包括:依赖、泛化、实现、关联、聚合、组合。

依赖:

泛化:

实现:

关联:

聚合:

组合:

依赖(Dependence)

只要在一个类中用到了另一个类,两个类之间就存在依赖关系。

在一个类A中使用另一个类B的方式有:

  • B作为A的成员属性
  • B作为A中方法接收的参数
  • B作为A中方法的返回类型
  • A在方法中用到了B

以下面的PersonServiceBean类为例:

public class PersonServiceBean {
	private PersonDao personDao;// PersonDao类作为成员属性

	public void save(Person person) {// Person类作为方法的接收参数
	}

	public IDCard getIDCard(Integer personid) {// IDCard类作为方法的返回类型
		return null;
	}

	public void modify() {
		Department department = new Department();// Department类在方法中被用到
	}
}

class PersonDao {
}

class IDCard {
}

class Person {
}

class Department {
}

以上四种形式都构成依赖关系。

用UML图来表示就是:

依赖关系传递的三种方式:接口、构造器、setter

通过接口:

interface IOpenAndClose {
	public void open(ITV tv);
}

interface ITV {
	public void play();
}

class OpenAndClose implements IOpenAndClose {
	public void open(ITV tv) {
		tv.play();
	}
}

class ChangHong implements ITV {
	@Override
	public void play() {
		System.out.println("打开长虹电视机");
	}
}

public class Test {
	public static void main(String[] args) {
		ChangHong changHong = new ChangHong();
		OpenAndClose openAndClose = new OpenAndClose();
		openAndClose.open(changHong);
	}
}

通过构造方法:

interface IOpenAndClose {
	public void open();
}

interface ITV {
	public void play();
}

class OpenAndClose implements IOpenAndClose {
	public ITV tv;

	public OpenAndClose(ITV tv) {
		this.tv = tv;
	}

	public void open() {
		this.tv.play();
	}
}

class ChangHong implements ITV {
	@Override
	public void play() {
		System.out.println("打开长虹电视机");
	}
}

public class Test {
	public static void main(String[] args) {
		ChangHong changHong = new ChangHong();
		OpenAndClose openAndClose = new OpenAndClose(changHong);
		openAndClose.open();
	}
}

通过setter方法:

interface IOpenAndClose {
	public void open();

	public void setTv(ITV tv);
}

interface ITV {
	public void play();
}

class OpenAndClose implements IOpenAndClose {
	public ITV tv;

	public void setTv(ITV tv) {
		this.tv = tv;
	}

	public void open() {
		this.tv.play();
	}
}

class ChangHong implements ITV {
	@Override
	public void play() {
		System.out.println("打开长虹电视机");
	}
}

public class Test {
	public static void main(String[] args) {
		ChangHong changHong = new ChangHong();
		OpenAndClose openAndClose = new OpenAndClose();
		openAndClose.setTv(changHong);
		openAndClose.open();
	}
}

输出:

打开长虹电视机

泛化(Generalization)

泛化关系实际上就是面向对象中的继承,它本质上是依赖关系的特例。

如果A类继承了B类,我们就说A类和B类之间存在泛化关系。

以下面的DaoSupport类和PersonServiceBean类为例:

public abstract class DaoSupport {
	public void save(Object entity) {
	}

	public void delete(Object id) {
	}
}

class PersonServiceBean extends DaoSupport {
}

用UML图来表示就是:(子类指向父类)

实现(Implementation)

实现关系实际上就是面向对象中的实现,它也是依赖关系的特例。

如果A类实现了B接口,我们就说A类和B接口之间存在实现关系。

以下面的PersonService接口和PersonServiceBean类为例:

public interface PersonService {
	public void delete(Integer id);
}

class PersonServiceBean implements PersonService {
	public void delete(Integer id) {
		System.out.println("delete");
	}
}

用UML图来表示就是:(类指向接口)

关联(Association)

关联关系是类与类、或者类与接口之间的一种强依赖关系。

关联关系具有导航性,可以是双向关系,也可以是单向关系。

关系可以是一对一、一对多、多对多。

以单向一对一关系和双向一对一关系为例:

单向一对一关系:

public class Person {
	private IDCard card;
}

class IDCard {
}

UML图:

双向一对一关系:

public class Person {
	private IDCard card;
}

class IDCard {
	private Person person;
}

UML图:

聚合(Aggregation)和组合(Composition)

聚合关系:

聚合关系表示的是整体和部分之间的关系,聚合关系认为部分可以从整体中分离出来。

如果部分不可以从整体中分离出来,则是组合关系。

聚合关系是关联关系的特例,所以它具有关联关系的导航性与多重性。

以一台电脑(Computer)为例,电脑由键盘(keyboard)、显示器(monitor)、鼠标(mouse)等部分组成,如果我们认为组成电脑的各个部分都是可以从电脑中分离出来的,用代码表示就是:

public class Computer {
	private Monitor monitor;
	private Mouse mouse;
}

class Monitor {
}

class Mouse {
}

聚合关系用UML图来表示就是:

组合关系:

组合关系也表示整体和部分之间的关系,组合关系认为部分不可以从整体中分离出来。

如果我们认为Monitor、Monse和Computer是不可分离的,则上面的聚合关系可以升级为组合关系。

public class Computer {
    private Monitor monitor = new Monitor();
    private Mouse mouse = new Mouse();
}

class Monitor {
}

class Mouse {
}

这样,只要创建了Computer对象,就会创建monitor和mouse;同样的,只要销毁了Computer对象,与之对应的monitor和mouse会一并销毁。

组合关系用UML图来表示就是:

鉴于“显示器、鼠标等能不能从电脑中分离出来”是一个哲学问题,下面用一个 人(Person)-脑袋(Head)-身份证(IDCard) 的例子来比较聚合关系和组合关系。

public class Person {
	private IDCard card;// 聚合关系,可以分离
	private Head head = new Head();// 组合关系,不可分离
}

class IDCard {
}

class Head {
}

用UML图来表示就是:

设计模式七大原则

设计模式原则是程序员在使用设计模式编程时应当遵守的规则。

提问:xx模式为什么要这么设计?

答:因为xx设计原则。

设计模式原则包括:

  • 单一职责原则
  • 接口隔离原则
  • 依赖倒转原则
  • 里氏替换原则
  • 开闭原则
  • 迪米特法则
  • 合成复用原则

单一职责原则

单一职责原则:一个类应该只负责一项职责。

如果类A负责两个不同的职责:职责1、职责2,当职责1需求变更并要求改变A时,可能会造成职责2执行错误。可将类A分解为A1、A2,分别负责职责1、职责2。

遵循单一职责原则主要的目的是降低类的复杂度,提高类的可读性、可维护性,降低业务变更带来的风险。

接口隔离原则

接口隔离原则:客户端不应该依赖它不需要的接口,即一个类对另一个类的依赖应该建立在最小的接口上。

假设有接口Interface1定义了5个抽象方法,类B、类D实现了Interface1,类A通过接口Interface1依赖于类B,但只会用到接口中的1、2、3三个方法,类C通过接口Interface1依赖于类D,但只会用到接口中的1、4、5三个方法。

如果按照下图来设计:

这样就违反了接口隔离原则,因为Interface1对于类A和类C来说不是最小接口,类B和类D必须实现它们不需要的方法。

为遵循接口隔离原则,可以将接口Interface1拆分为几个独立的接口,类A和类C分别与它们需要的接口建立依赖关系。如下图所示:

依赖倒转原则

依赖倒转原则:

  • 高层模块不应该依赖底层模块,二者都应该依赖其抽象。
  • 抽象不应该依赖细节,细节应该依赖抽象。
  • 依赖倒转的中心思想是面向接口编程。
  • 依赖倒转原则的设计理念:相对于细节的多变性,抽象的东西相对稳定。以抽象为基础搭建的架构比以细节为基础的架构要稳定的多。在Java中,抽象就是指接口或抽象类,细节就是具体的实现类。
  • 使用接口或抽象类的目的是制定规范,而不涉及任何具体的实现,展现细节的任务交给它们的实现类去完成。

依赖倒转原则的注意事项有:

  • 底层模块尽量都要有抽象类或接口。
  • 变量的声明类型尽量是抽象类或接口,这样变量引用和实际对象间就存在一个缓冲层,利于程序扩展和优化。
  • 继承时遵循里氏替换原则。

我们先不遵循依赖倒转原则写一个接收email信息的Person类:

public class Test {
	public static void main(String[] args) {
		Person person = new Person();
		person.receive(new Email());
	}
}

class Email {
	public String getInfo() {
		return "Email: Hello World!";
	}
}

class Person {
	public void receive(Email email) {
		System.out.println(email.getInfo());
	}
}

输出结果:

Email: Hello World!

这样做比较容易理解。

现在我们要新增一个接收微信消息的功能,需要新增一个WeChat类,并在Person类中添加接收WeChat消息的方法:

public class Test {
	public static void main(String[] args) {
		Person person = new Person();
		person.receive(new Email());
		person.receive(new WeChat());
	}
}

class Email {
	public String getInfo() {
		return "Email: Hello World!";
	}
}

class WeChat {//新增WeChat类
	public String getInfo() {
		return "WeChat: Hello World!";
	}
}

class Person {
	public void receive(Email email) {
		System.out.println(email.getInfo());
	}

	public void receive(WeChat weChat) {//新增接收WeChat消息的方法
		System.out.println(weChat.getInfo());
	}
}

输出结果:

Email: Hello World!
WeChat: Hello World!

这么做当然也可以完成需求,但每次增加接收新的消息类型的功能时,都需要改动Person类(违反了开闭原则)。遵循依赖倒转原则,我们就可以在不改动Person类的情况下增加新的消息类型。

创建接口IReceiver,并使每种消息类型都实现这个接口:

public class Test {
	public static void main(String[] args) {
		Person person = new Person();
		person.receive(new Email());
		person.receive(new WeChat());
	}
}

interface IReceiver {//创建接口IReceiver
	String getInfo();
}

class Email implements IReceiver {//Email类实现IReceiver接口
	public String getInfo() {
		return "Email: Hello World!";
	}
}

class WeChat implements IReceiver {//新增的WeChat类实现IReceiver接口
	public String getInfo() {
		return "WeChat: Hello World!";
	}
}

class Person {
	public void receive(IReceiver receiver) {//参数改为IReceiver接口对象
		System.out.println(receiver.getInfo());
	}
}

输出结果:

Email: Hello World!
WeChat: Hello World!

之后每次增加接收新的消息类型的功能时,都创建新增消息类型的类,并实现IReceive接口,不需要再对Person类进行改动。

里氏替换原则

里氏替换原则:如果对每个类型为T1的对象o1,都有类型为T2的对象o2,使得以T1定义的所有程序P在所有的对象o1都代换成o2时,程序P的行为没有发生变化,那么类型T2是类型T1的子类型。换言之,所有引用基类的地方必须能透明地使用其子类的对象。

更直白地来讲,在子类中尽量不要重写父类的方法。

继承实际上让两个类的耦合性增强了,在适当的情况下,可以使用依赖、聚合、组合的方式来代替继承解决问题(合成复用原则)。

通常的做法是:把原有的继承关系去掉,原来的父类和子类都继承一个更通俗的基类,采用依赖、聚合、组合等关系代替。

例如,类B继承类A:

class A {
	public int func1(int a, int b) {
		return a + b;
	}
}

class B extends A {
	public int func1(int a, int b) {
		return a - b;
	}
}

类A中的func1方法被重写。

可以改为:

class Base {

}

class A extends Base {
	public int func1(int a, int b) {
		return a + b;
	}
}

class B extends Base {
	private A a = new A();

	public int func1(int a, int b) {
		return a - b;
	}

	public int func2(int a, int b) {
		return this.a.func1(a, b);
	}
}

调用类B中的func2,相当于调用类A中的func1。

开闭原则

开闭原则:一个软件实体(如类、模块、函数)应该对扩展开放,对修改关闭。用抽象构建框架,用实现扩展细节。

对提供方开放,对使用方关闭。

当软件需要变化时,尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有的代码来实现变化。

编程中遵循其它原则,以及使用设计模式的目的就是遵循开闭原则。

我们回到讲依赖倒转原则时举的例子,其中People类是使用方,应该避免在增加新功能时对People类进行修改,而应该通过扩展现有的软件实体完成需求。

public class Test {
	public static void main(String[] args) {
		Person person = new Person();
		person.receive(new Email());
		person.receive(new WeChat());
	}
}

interface IReceiver {
	String getInfo();
}

class Email implements IReceiver {
	public String getInfo() {
		return "Email: Hello World!";
	}
}

class WeChat implements IReceiver {
	public String getInfo() {
		return "WeChat: Hello World!";
	}
}

class Person {
	public void receive(IReceiver receiver) {
		System.out.println(receiver.getInfo());
	}
}

迪米特法则

迪米特法则:一个对象应该对其它对象保持最少的了解。

迪米特法则又叫最少知道原则,即一个类对自己依赖的类了解的越少越好,被依赖的类不管多么复杂,都尽量将逻辑封装在类的内部,除了提供的public方法,不对外泄漏任何信息。

遵循迪米特法则是为了降低类之间的耦合。

关于迪米特法则我们只需要记住一点:陌生的类不要以局部变量的形式出现在类的内部。

所谓陌生的类,即没有在成员变量、方法参数、方法返回值中出现过的类。

合成复用原则

合成复用原则:尽量使用合成/聚合的方式,而不是使用继承。

如果我们想让类B使用类A中的方法,使用继承会使A和B的耦合性增强:

可以将A和B改进成依赖关系:

或者改成聚合关系:

或者改成组合关系:

加油!

猜你喜欢

转载自blog.csdn.net/qq_42082161/article/details/111299870