极客大学架构师训练营 框架设计、设计原则、设计模式 第四课 听课总结

说明

框架设计、设计原则、设计模式

讲师:李智慧

面向对象编程与面向对象分析

面向对象编程不是使用面向对象的编程语言进行编程,而是利用多态特性进行编程。

面向对象分析是将客观世界,即编程的业务领域进行对象分析。

  • 充血模型和贫血模型
  • 领域驱动设计DDD(Domain Driven Design)

面向对象设计的目的和原则

面向对象设计的目的

  • 强内聚、低耦合,从而使系统
    ☞ 易扩展 - 易于增加新的功能
    ☞ 更强壮 - 不容易被粗心的程序员破坏
    ☞ 可移植 - 能够在多样的环境下运行
    ☞ 更简单 - 容易理解、容易维护

面向对象设计的原则

  • 为了达到上述设计目标,最具代表性Robert C. Martin总结出了多种指导原则。
  • “原则” 是独立于编程语言的,甚至于可以用于非面向对象的编程语言中。

设计模式(Design Patterns)

设计模式是用于解决某一种问题的通用的解决方案。
设计模式也是语言中立的。
设计模式贯彻了设计原则。

Gang of Four (Erich Gamma, Richard Helm, Ralph Johnson and John Vlissides)提出了三大类23种基本的设计模式:

  • 创建模式
  • 行为模式
  • 结构模式

在更细分的领域中还可以总结出许多设计模式:

  • 并发编程模式
  • Java EE模式

框架(Frameworks)

框架是用来实现某一类应用的结构性的程序,是对某一类架构方案可复用的设计与实现

  • 如同框架架构的大厦的框架
  • 简化应用开发者的工作
  • 实现了多种设计模式,使应用开发者不需要花太大的力气,就能设计出结构良好的程序来

不同领域的框架

  • 微软公司为Windows编程开发了MFC框架。
  • Java为它的GUI(图形用户界面)开发了AWT框架。
  • 还有许多开源的框架:MyBatis,Spring等。
  • Web服务器也是框架:Tomcat

框架 VS 工具

框架调用应用程序代码(比如:Spring Boot, Spring Cloud, JUnit)
应用程序代码调用工具(比如:Log4j)

架构师用框架保证架构的落地
架构师用工具提高开发效率

软件设计的"臭味"

软件设计的最终目的,是使软件达到“强内聚、松耦合”,从而使软件:

  • 易扩展 - 易于增加新的功能
  • 更强壮 - 不容易被粗心的程序员破坏
  • 可移植 - 能够在多样的环境下运行
  • 更简单 - 容易理解、容易维护

与之相反,一个 “不好的” 软件, 会发出如下 “臭味” :

  • 僵硬 - 不易改变。
  • 脆弱 - 只想改 A,结果 B 被意外破坏。
  • 不可移植 - 不能适应环境的变化。
  • 导致误用的陷阱 - 做错误的事比做正确的事更容易,引诱程序员破坏原有设计。
  • 晦涩 - 代码难以理解。
  • 过度设计、 copy-paste 代码。

僵化性(Rigidity)

很难对系统进行改动,因为每个改动都会迫使许多对系统其它部分的改动。

  • 如果单一的改动会导致依赖关系的模块中的连锁改动,那么设计就是僵化的,必须要改动的模块越多,设计就越僵化。

脆弱性(Fragility)

对系统的改动会导致系统中和改动的地方无关的许多地方出现问题。

  • 出现新问题的地方与改动的地方没有概念上的关联。要修正这些问题又会引出更多的问题,从而使开发团队就像一只不停追逐自己尾巴的狗一样。

牢固性(Immobility)

很难解开系统的纠结,使之成为一些可在其它系统中重要的组件。

  • 设计中包含了对其它系统有用的部分,而把这部分从系统中分离出来所需要的努力和风险是巨大的。

粘滞性(Viscosity)

做正确的事情比做错误的事情要困难。

  • 面临一个改动的时候,开发人员常常会发现会有多种改动的方法。
    有的方法会保持系统原来的设计,而另外一些则会破坏设计,当那些可以保持系统设计的方法
    比那些破坏设计的方法很难应用,就表明设计具有高的粘滞性,做错误的事情就很容易。

不必要的复杂性(Needless Complexity)

设计中包含不具任何直接好处的基础结构

  • 如果设计中包含有当前没有用的组成部分,它就含有不必要的复杂性。当开发人员预测需求的变化,并在软件中放置了那些潜在的变化的代码时,常常会出现这种情况。

不必要的重复(Needless Repetition)

设计中包含有重复的结构,而该重复的结构本可以使用单一的抽象进行统一。

  • 当 copy, cut, paste 编程的时候,这种情况就会发生。

晦涩性(Opacity)

很难阅读、理解。没有很好的表现出意图。

  • 代码可以用清晰、富有变现力的方式编写,也可以用晦涩、费解的方式编写。一般说来,随着时间的推移,代码会变得越来越晦涩。

一个设计腐化过程的例子

  • 编写一个从键盘读入字符并输出到打印机的程序

Copy 程序结构图

在这里插入图片描述
Copy 程序

void Copy()
{
	int c;
	while ((c=RdKbd()) != EOF)
		WrtPrt(c);
}

几个月以后老板来找你,说有时希望 copy 程序能从纸带机中读入信息。

Copy 程序的第一次修改成果

bool ptFlag = false;
// remember to reset this flag
void Copy()
{
	int c;
	while ((c=(ptflag ? Rdpt() : RdKbd())) != EOF)
		WrtPrt(c);
}

再过几周后,你的老板告诉你,客户有时候需要输出到纸带打孔机上。

Copy 程序第二次修改成果

bool ptFlag = false;
bool punchFlag = false;
// remember to reset these flags
void Copy()
{
	int c;
	while ((c=(ptflag ? Rdpt() : RdKbd())) != EOF)
		punchFlag ? WrtPunch(c) : WrtPrt(c);
}

一个遵循 OOD 原则的设计

public interface Reader {
	public int read();
}

public class KeyboardReader extends Reader {
	public int read() {
		return readKeyboard();
	}
}

Reader reader = new KeyboardReader();
Writer writer = new Printer();
public void copy() {
	int c:
	while ((c=reader.read()) != EOF)
		Writer.write();
}

Button/Dailer 僵化例子

设计一个控制电话拨号的软件。
下面是一个 “拨打电话” 的 Use Case 描述:

  • 我们按下数字按钮,屏幕上显示号码,扬声器发出按键音。
  • 我们按下 Send 按键,系统接通无线网络,同时屏幕上显示正在拨号。

如何找出对象:大部分名词就是对象。
比如数字按钮,屏幕,扬声器,Send按键,系统,无线网络。

类图

在这里插入图片描述

合作图

在这里插入图片描述

根据 UML 想象程序代码

public class Button {
	public final static int SEND_BUTTON = -99;

	private Dialer dialer;
	private int    token;

	public Button(int token, Dialer dialer) {
		this.token = token;
		this.dialer = dialer;
	}

	public void press() {
		switch (token) {
			case 0: case 1: case 2: case 3: case 4:
			case 5: case 6: case 7: case 8: case 9:
				dialer.enterDigit(token);
				break;
			
			case SEND_BUTTON:
				dialer.dial();
				break;

			default:
				throw new UnsupportedOperationException("unknown button pressed: token=" + token);
		
		}
	}
}

public class Dialer {

	public void enterDigit(int digit) {
		screen.display(digit);
		speaker.beep(digit);
	}

	public void dial() {
		screen.display("dialing...");
		radio.connect();
	}

}

有什么臭味吗?

在这里插入图片描述

  1. 僵化 - 不易增加、修改:
    ☞ 增加一种 Button 类型,就需要对 Button 类进行修改;
    ☞ 修改 Dialer,可能会影响 Button。
  2. 脆弱 - switch case / if else 语句是相当脆弱的。
    ☞ 当我想修改 Send 按钮的功能时,有可能不小心破坏数字按钮;
    ☞ 当这种函数很多时,我很有可能会漏掉某个函数,或其中的某个条件分支。
  3. 不可移植 - 设想我们要设计密码锁的按钮,它只需要数字按键,但 Button 的设计使它必须 “附带” 一个 “Send” 类型的按钮。

OOD原则一:开/闭原则(OCP)

OCP - Open/Closed Principle

  • 对于扩展是开放的(Open for extension)
  • 对于更改是封闭的(Closed for modification)
  • 简言之:不需要修改软件实体(类、模块、函数等),就应该能实现功能的扩展。

传统的扩展模块的方式就是修改模块的源代码。如何实现不修改而扩展呢?

  • 关键是抽象!

改进 Button:方法一 继承

在这里插入图片描述

改进 Button:方法二 策略模式

在这里插入图片描述

改进 Button:方法三 适配器模式

在这里插入图片描述

改进 Button:方法四 观察者模式

拨号的同时,该按钮的灯就亮一下。
在这里插入图片描述

Button/Dailer 改进后代码实现 – 观察者模式

public interface ButtonListener {
	void buttonPressed();
}

public class Button {
	private List<ButtonListener> listeners;

	public Button() {
		this.listeners = new LinkedList<ButtonListener>();
	}
	
	public void addListener(ButtonListener listener) {
		assert listener != null;
		listeners.add(listener);
	}

	public void press() {
		for (ButtonListener listener: listeners) {
			listener.buttonPressed();
		}
	}
}

public class Phone {
	private Dialer dialer;
	private Button[] digitButtons;
	private Button sendButtons;

	public Phone() {
		dialer = new Dailer();
		digitButton = new Button[10];

		for (int i = 0; i < digitButtons.length; i++) {
			digitButton[i] = new Button();
			
			final int digit = i;

			digitButtons[i].addListener(new ButtonListener() {
				public void buttonPressed() {
					dialer.enterDigit(digit);
				}
			});
		}

		sendButton = new Button();
		sendButton.addListener(new ButtonListener() {
			public void buttonPressed() {
				dialer.dial();
			}
		});
	}

	public static void main(String[] args) {
		Phone phone = new Phone();
		
		phone.digitButton[9].press();
		phone.digitButton[1].press();
		phone.digitButton[1].press();
		
		phone.sendButton.press();
	}
}


OOD原则二:依赖倒置原则(DIP)

DIP - Dependency Inversion Principle

  • 高层模块不能依赖低层模块,而是大家都依赖于抽象;
  • 抽象不能依赖实现,而是实现依赖抽象。

在这里插入图片描述

DIP 倒置了什么?

  • 模块或包的依赖关系
  • 开发顺序和职责

软件的层次化

  • 高层决定低层
  • 高层被重用

比如Controller是高层不能依赖于低层Service,就算是依赖了IService接口,IService也是为Service服务的,这里还是高层依赖了低层。如何解决?

解决方案:
比如是个注册场景,Controller定了一个Register的接口,Service要去实现Register接口。

遵循 DIP 的层次依赖关系

在这里插入图片描述

违反 DIP 案例

在这里插入图片描述
遵循 DIP 的解决方案:
在这里插入图片描述

框架的核心

好莱坞规则:

  • Don’t call me, I’ll call you.
    倒转的层次依赖关系

在这里插入图片描述

找出 Button 背后的抽象

Button 的本质是什么?

  • 检测用户的按键指令,并传递给目标对象。

用什么机制检测用户的按键?

  • 不重要

目标对象是什么?

  • 不重要

OOD原则三:Liskov替换原则(LSP)

在 Java / C++ 这样的静态类型语言中,实现 OCP 的管线在于抽象,而抽象的威力在于多态和继承。

  • 一个正确的继承要符合什么要求?
  • 答案:Liskov 替换原则

1988年,Barbara Liskov 描述这个原则:

  • 若对每个类型 T1 的对象 O1, 都存在一个类型 T2 的对象 O2, 使得在所有针对 T2 编写的程序 P 中,用 O1 替换 O2 后,程序 P 的行为功能不变,则 T1 是 T2 的子类型。
  • 简言之:子类型(Subtype)必须能够替换掉它们的基类型(Base Type)。

举例说明

假设: Horse 是 WhiteHorse 和 BlackHorse 的基类
在使用 Horse 对象的任何场合,我们可以把 WhiteHorse 对象传进去,以取代 Horse 对象,程序仍然正确。
在这里插入图片描述

LSP 的反命题不成立

墨子曾经曰过:《墨子 小取》

  • “娣,美人也,爱娣,非爱美人也…”
    在这里插入图片描述

违反 LSP 的案例一

void drawShape(Shape shape) {
	if (shape instanceof Circle) {
		drawCircle((Circle)shape);
	} else if (shape instanceof Square) {
		drawSquare((Square)shape);
	} else {
		......
	}
}

违反 LSP 的案例二

不符合 IS-A 关系的继承,一定不符合 LSP
JDK 中的错误设计:
在这里插入图片描述

违反 LSP 的案例三

下面是一个 “长方形” 类:
public class Rectangle {
	private double width;
	private double height;

	public void setWidth(double w) { width = w; }
	public void setHeight(double h) { height = h; }
	public double getWidth() { return width; }
	public double getHeight() {return height; }
}
接着让我们创建一个 “正方形” 类。正方形 IS-A 长方形吗?

在这里插入图片描述
Rectangle 包含 width 和 height,但 Square 只需要 side 就可以了。

public class Square extends Rectangle {
	public void setWidth(double w) {
		width = height = w;
	}
	public void setHeight(double h) {
		height = width = h;
	}
}

加入有一个方法:

void testArea(Rectangle rect) {
	rect.setWidth(3);
	rect.setHeight(4);
	assert 12 == rect.calculateArea();	// 传入 Square 将失败
}

为什么正方形 IS-NOT-A 长方形呢?

IS-A 关系是关于行为的。

  • 从行为的方式来看,正方形和长方形是不同的。

从对象的属性来证明这一论点,对于同一个类,所创建的不同对象,它们的:

  • 标识 - 是不同的。
  • 状态 - 是不同的。
  • 行为 - 是不同的。
    因此,设计和界定一个类,应该以其行为作为区分。

从 “契约” 的角度来看 LSP

LSP 要求,凡是使用基类的地方,一定也适用于其子类。
从 Java 语法角度看,意味着:

  • 子类一定拥有基类的整个接口。
  • 子类的访问控制不能比基类更严格。
    ☞ 例如,Object 类中又一个方法:
    protected Object clone();
    ☞ 子类中可以覆盖(override)之并放松其访问控制:
    public Object clone();
    ☞ 但反过来是不行的,例如:
    ☞ 覆盖 public String toString() 方法,并将其访问权限缩小成 private, 编译器不可能允许这样的代码通过编译。

从更广泛的意义来看,子类的 “契约” 不能比基类更 “严格”。

  • 例如, 正方形长宽相等,这个契约比长方形要严格,因此正方形不是长方形的子类。
  • 例如,Properties 的契约比 Hashtable 更严格。

如何重构代码,以解决 LSP 问题?

方法1:最简单的办法是,提取共性到基类

在这里插入图片描述

方法2:改成组合

在这里插入图片描述

继承 vs. 组合

继承和组合 OOP 的两种扩展手段

继承的优点:

  • 比较容易,因为基类的大部分功能可以通过继承直接进入子类。

继承的缺点:

  • 继承破坏了封装,因为继承将基类更多的细节暴露给子类。因而继承被称为 “白盒复用”.
  • 当基类发生改变时,可能会层层影响其下的子类。
  • 继承是静态的,无法在运行时改变组合。
  • 类数量的爆炸。

应该优先使用组合。

合适检测 LSP?

一个模型,如果孤立地看,并不具有真正意义上的有效性。

  • 孤立地看,Rectangle 和 Square 并没有什么问题。

通过它的客户程序才能体现出来

  • 从对基类做出合理假设的客户程序的角度来看,Rectangle 和 Square 这个模型就是有问题的。

有谁知道设计的使用者会做出什么合理的假设呢?

  • 大多数这样的假设都很难预测。
  • 避免 “过于复杂” 或 “过度设计”.
  • 只预测明显的违反 LSP 的情况,而推迟其它的预测。

可能违反 LSP 的征兆

派生类中的退化函数

public class Base {
	public void func() { /* do something.    */ }
}

public class Derived extends Base {
	public void func() { }
}

派生类中抛出基类不会产生的异常。

public class Derived extends Base {
	public void func() {
		throw new UnsupportedOperationException();
	}
}

OOD 原则四:单一职责原则(SRP)

SRP - Single Responsibility Principle

  • 又被称为 “内聚性原则(Cohesion)”, 意为:
    ☞ 一个模块的组成元素之间的功能相关性。
  • 将它与引起一个模块改变的作用力相关联,就形成了如下描述:
    ☞ 一个类,只能有一个引起它的变化的原因。

什么是职责?

  • 单纯谈论职责,每个人都会得出不同的结论
  • 因此我们下一个定义:
    ☞ 一个职责是一个变化的原因。

违反 SRP 原则的后果

举例说明:

  • Rectangle 类包含了两个职责:
    draw() 在GUI上画出自己;
    area() 用来计算自身的面积。
  • 有两个应用分别依赖Rectangle:
    ☞ 计算几何应用,利用Rectangle计算面积
    ☞ 图形应用,利用Rectangle绘制长方形,也需要计算面积。
    在这里插入图片描述
    后果
  • 脆弱性 - 把绘图和计算功能耦合在一起,当修改其中一个时,另一个功能可能会意外受损。
  • 不可移植性 - 计算几何应用只需要使用 “计算面积” 的功能,却不得不包含 GUI 的依赖。

改进
在这里插入图片描述

区分类的方法: 分清职责

职责 - 变化的原因

  • 有时区分一个类包含了几个职责并不明显,例如:
interface Modem {
	void dial(String pno);
	void hangup();
	void send(char c);
	void recv();
}
  • 加入应用程序连接 Modem 的方式会发生变化,例如: dial 的参数会因此而变化,那么这个设计会导致 “僵化性” 的问题。此时,应该把连接和收发这两个职责分离:
    何时分离职责?当变化发生时。
    在这里插入图片描述

一种常见的违反 SRP 情景

Employee 包含了两个职责:

  • 业务逻辑
  • 持久化逻辑

这两个职责通常不应该混合在一起:

  • 业务变化快,持久化逻辑变化慢。
  • 变化的原因也不同
    在这里插入图片描述

OOD原则五:接口分离原则(ISP)

ISP – Interface Segregation Principle

  • 不应该强迫客户程序依赖它们不需要的方法。

ISP 和 SRP 的关系

  • ISP 和 SRP 是相关的,都和 “内聚性” 有关。
  • SRP 指出应该如何设计一个类 – 只能有一种原因才能促使该类发生变化。
  • ISP 指出改如何设计一个接口 – 从客户的需要出发,强调不要让客户看到他们不需要的方法。

以前面 Modem 为例

事实上,要完全做到 SRP 是困难的,例如在 Modem 例子中,”连接“ 环节和 ”收发数据“ 环节有内在的关系,可能必须写在一个类中。

但是我们仍然可以把接口分开,这样当 ”连接“ 的方法改变时,那些只关系 ”收发数据“ 的程序不会受到影响。
在这里插入图片描述

胖接口 – 另一个例子

这是一个可定时关闭的门。

Interface Door extends TimerClient {
	void lock();
	void unlock();
	boolean isDoorOpen();
}

class Timer {
	void register(int timeout, TimeClient client);
}

interface TimerClient {
	void timeout();
}

在这个例子中, Door 类的接口中包含了 timeout 方法,然而这个方法对不需要 timeout 机制的门是没有用的。

客户对接口的反作用

Timer 是 Door 的客户;另外还有一些不需要定时功能的 Door 客户。
当 Timer 发生改变时:

class Timer {
	public void register(int timeout, int timeoutID, TimerClient client);
}

TimerClient 也被迫改变:

interface TimerClient {
	void timeout(int timeoutID);
}

从而所有不需要定时功能的 Door 的客户程序都收到影响。

改进:分离 Door 接口和 TimerClient 接口

方法1: 适配器模式

在这里插入图片描述

方法2: 多继承

在这里插入图片描述

总结

优秀的程序员:欢迎需求变更。你的设计就是为了需求变更而设计。开闭原则,对修改关闭,对扩展开发。

推荐阅读:《敏捷软件开发原则、模式与实践》,作者Robert C. Martim
在这里插入图片描述

注意:以上信息如有侵权,请联系作者删除,谢谢。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值