写点什么

【架构笔记之框架设计】架构师训练营第 1 期第 2 周

用户头像
业哥
关注
发布于: 2020 年 09 月 23 日
【架构笔记之框架设计】架构师训练营第1期第2周

1. 面向对象编程的本质


编程的目的:用计算机来解决现实的问题;

编程的过程即:在计算机所能理解的“模型”(解空间)和现实世界(问题域空间)之家,建立一种联系;

编程语言:是一种“抽象的机制”,问题是对“谁”来抽象 (如下图所示);


编程核心 3 要素:人,客观业务领域,计算机 (如下图所示):


何为面向对象编程:第一个成功的面向对象的语言 Smalltalk 描述:

  • 万物皆对象。

  • 程序是对象的集合,他们通过发送消息来告知彼此所要做的事情。

  • 每个对象都有自己的由其他对象所构成的存储。

  • 每个对象都拥有其他类型。

  • 某一特定类型的所有对象都可以接收同样的消息。


什么是对象:Booch 对于对象的描述:对象具有状态、行为和标识。

  • 状态:表明每个对象可以有自己的数据。

  • 行为:表明每个对象都可以产生。

  • 标识:表明每个对象都区别于其他的对象。(唯一的地址)


面向对象编程的三要素:描述特征

  1. 封装性(Encapsulation)

  • 隐藏实现细节(访问控制)

  • 定义接口


  1. 继承性(Inheritance)

  • is-A 关系

  • HAS-A 关系(组合)


  1. 多态性(Polymorphism)

  • 后期绑定(虚函数)

  • 向上转形


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

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

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

  • 充血模型与贫血模型

  • 领域驱动设计 DDD


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

  • 面向对象设计的目的:

强内聚,低耦合,从而使系统:

  • 易扩展 -- 增加新的功能

  • 更强壮 -- 不容易被粗心的程序员破坏

  • 可移植 -- 能够在多样的环境下运行

  • 更简单 -- 容易理解,容易维护


  • 面向对象设计的原则:

  • 为了达到上述设计目标,有人总结出了多种指导原则

  • 原则”是独立于编程语言的,甚至也可以用于非面向对象的编程语言中


设计模式(design patterns):

  • 设计模式是用于解决某一种问题的通用的解决方案。

  • 设计模式也是语言中立的。

  • 设计模式贯彻了设计原则。


Gang of Four 提出 23 种基本的设计模式 (三大类):

  • 创建模式(5 种)

单例模式、抽象工厂模式、建造者模式、工厂模式、原型模式。

  • 行为模式(7 种)

适配器模式、桥接模式、装饰模式、组合模式、外观模式、享元模式、代理模式。

  • 结构模式(11 种)

模版方法模式、命令模式、迭代器模式、观察者模式、中介者模式、备忘录模式、

解释器模式、状态模式、策略模式、职责链模式(责任链模式)、访问者模式


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

  • 并发编程模式

  • Java EE 模式

  • etc


框架(frameworks):

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

  • 如同框架结构的大厦的框架

  • 简化应用开发者的工作

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


不同领域的框架

  • 微软公司为 Windows 编程开发了 MFC 框架。

  • Java 为它的 GUI(图形用户界面)开发了 AWT 框架。

  • 还有许多开源的框架:MyBatis,Spring 等。

  • Web 服务器也是框架:Tomcat


框架 VS 工具 :

  • 框架调用应用程序代码

  • 应用程序代码调用工具

  • 架构师用框架保证架构的落地

  • 架构师用工具提高开发效率

2. 糟糕的代码的特点


软件设计的“臭味:

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

  • 易扩展 - 易于增加新的功能

  • 更强壮 - 不容易被粗心的程序员破坏

  • 可移植 - 能够在多样的环境下运行

  • 更简单 - 容易理解、容易维护


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

  • 僵硬 - 不易改变。

  • 脆弱 - 只想改 A,结果 B 被意外破坏。

  • 不可移植 - 不能适应环境的变化。

  • 导致误用的陷阱 - 做错误的事比做正确的事更容易,引诱程序员破坏原有的设计。

  • 晦涩 - 代码难以理解。

  • 过度设计、copy-paste 代码。


【案例分析】

一个设计腐化过程的例子:

原始需求:编一个从键盘读入字符并输出到打印机的程序,代码如下:


需求第 1 次变更:几个月以后老板来找你,说有时希望 copy 程序能从纸带机中读入信息,代码如下:


需求第 2 次变更:几周后,你的老板告诉你,客户有时候需要输出到纸带打孔机上,代码如下:


基于 OOD 原则对案例进行重新设计:

public interface Reader {	public int read();}
public class KeyboardReader extends Reader { public int read(){ return readKeyboard(); }}
public class MainApplication { public static void main(String[] args){ Reader reader = new KeyboardReader(); Writer writer = new Printer(); int c; while((c=reader.read())!=EOF) Writer.write(); }}
复制代码


3. OOD 设计的 5 个原则


OOD 是面对对象的设计的英文缩写。遵循 OOD 设计一共有 5 个原则。

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


OCP - Open/Closed Principle

  • 对于扩展是开放的(Open for extension)

  • 对于更改是封闭的(Closed for modification)

  • 简言之:不需要修改软件实体(类、模块、函数等),就应该能实现功能的扩展。


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

  • 关键是抽象!


【案例分析】

需求:设计一个控制电话拨号的软件。

下面是一个“拨打电话”的 Use Case 描述:

  • 我们按下数字按钮,屏幕上显示号码,扬声器发出按键音。

  • 我们按下 Send 按钮,系统接通无线网络,同时屏幕上显示正在拨号。


设计类图如下:


设计活动图如下:


设计代码如下:

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(); }}
复制代码


以上的设计没有遵循 OCP 的设计原则,有如下缺点:

  • 僵硬 - 不易增加、修改:

  • 增加一种 Button 类型,就需要对 Button 类进行修改;

  • 修改 Dialer,可能会影响 Button。

  • 脆弱 - switch case/if else 语句是相当脆弱的。

  • 当我想修改 Send 按钮的功能时,有可能不小心破坏数字按钮;

  • 当这种函数很多时,我很有可能会漏掉某个函数,或其中的某个条件分支。

  • 不可移植 - 设想我们要设计密码锁的按钮,它只需要数字按键,但 Button 的设计使它必须

“附带”一个“Send”类型的按钮。


基于 OCP 原则改进本案例的设计:

  • 改进方法一:

以上改进把 button 抽象为接口,然后拨号按钮和发送按钮分别实现接口,这 2 个实现类都依赖拨号类,调用拨号类对应的方法。


  • 进一步改进方法二:使用策略模式


  • 进一步改进方法三:使用适配器模式


  • 进一步改进方法四:使用观察者模式

最终改进后的代码如下:

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 sendButton; public Phone() { dialer = new Dialer(); digitButtons = new Button[10]; for (int i = 0; i < digitButtons.length; i++) { digitButtons[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.digitButtons[9].press(); phone.digitButtons[1].press(); phone.digitButtons[1].press(); phone.sendButton.press(); }}
复制代码


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


DIP - Dependency Inversion Principle

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

  • 抽象不能依赖实现,而是实现依赖抽象。


DIP 倒置了什么?

  • 模块或包的依赖关系

  • 开发顺序和职责


软件的层次化

  • 高层决定低层

  • 高层被重用


框架的核心:

  • 好莱坞规则:

  • Don't call me, I'll call you.

  • 倒转的层次依赖关系


遵循 DIP 的层次依赖关系(例子):


违反 DIP 案例:


基于以上违反 DIP 案例的改造后的设计:


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


在 Java/C++ 这样的静态类型语言中,实现 OCP 的关键在于抽象,而抽象的威力在于多

态和继承。

  • 一个正确的继承要符合什么要求呢?

  • 答案:Liskov 替换原则


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

  • 若对每个类型 T1 的对象 o1,都存在一个类型 T2 的对象 o2,使得在所有针对 T2 编写的程

序 P 中,用 o1 替换 o2 后,程序 P 的行为功能不变,则 T1 是 T2 的子类型。

  • 简言之:子类型(subtype)必须能够替换掉它们的基类型(base type)


【案例分析 1:符合 LSP】

假设:Horse 是 WhiteHorse 和 BlackHorse 的基类

在使用 Horse 对象的任何场合,我们可以把 WhiteHorse 对象传进去,以取代 Horse 对

象,程序仍然正确


【案例分析 2:违反 LSP】

不符合 IS-A 关系的继承,一定不符合 LSP

JDK 中的错误设计:


【案例分析 3:违反 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 = w;  }}
复制代码

假如有一个方法:

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 更严格。


【上面的案例分析 3:重构以解决 LSP 问题】

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


  • 方法 2:改成组合:


继承 vs. 组合

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


继承的优点:

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


继承的缺点:

  • 继承破坏了封装,因为继承将基类更多的细节暴露给子类。因而继承被称为“白盒复用”。

  • 当基类发生改变时,可能会层层影响其下的子类。

  • 继承是静态的,无法在运行时改变组合。

  • 类数量的爆炸。


应该优先使用组合


何时检测 LSP?

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

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

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

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

的。

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

  • 大多数这样的假设都很难预测。

  • 避免“过于复杂”或“过度设计”。

  • 只预测明显的违反 LSP 的情况,而推迟其它的预测。


可能违反 LSP 的征兆


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


SRP - Single Responsibility Principle

  • 又被称为“内聚性原则(Cohesion)”,意为:

  • 一个模块的组成元素之间的功能相关性。

  • 将它与引起一个模块改变的作用力相联,就形成了如下描述:

  • 一个类,只能有一个引起它的变化的原因。


什么是职责?

  • 单纯谈论职责,每个人都会得出不同的结论

  • 因此我们下一个定义 :

  • 一个职责是一个变化的原因。


【案例分析】


如上图 Rectangle 类包含了两个职责:

  • draw()在 GUI 上画出自己;

  • area()用来计算自身的面积。


有两个应用分别依赖 Rectangle:

  • 计算几何应用,利用 Rectangle 计算面积。

  • 图形应用,利用 Rectangle 绘制长方形,也需要计算面积。


这个案例设计违反了 SRP 原则,导致的后果:

  • 脆弱性 - 把绘图和计算功能耦合在一起,当修改其中一个时,另一个功能可能会意外受损。

  • 不可移植性 - 计算几何应用只需要使用“计算面积”的功能,却不得不包含 GUI 的依赖。


基于 SRP 原则改进后的类图设计,如下图:


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


ISP - Interface Segregation Principle

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


ISP 和 SRP 的关系

  • ISP 和 SRP 是相关的,都和“内聚性”有关。

  • SRP 指出应该如何设计一个类 —— 只能有一种原因才能促使类发生改变。

  • ISP 指出应该如何设计一个接口 —— 从客户的需要出发,强调不要让客户看到他们不需要

的方法。


【案例分析】

需求:设计一个可定时关闭的们

设计类图如下:


代码如下:

Interface Door extends TimerClient {  void lock();  void unlock();  boolean isDoorOpen();}class Timer {	void register(int timeout, TimerClient client);}interface TimerClient {	void timeout();}
复制代码


以上案例的设计违反了 ISP 原则。

在这个案例中,Door 类的接口中包含了 timeout 方法,然而这个方法对不需要

timeout 机制的门是没有用的。


基于 ISP 原则改进一:使用适配器设计模式解决


基于 ISP 原则改进二: 使用多继承设计模式解决。

以上经过改进后,不需要定时功能的 Door 的客户程序就只用没有定时功能的 Door 接口,这样程序就不受影响了。


用户头像

业哥

关注

架构即未来! 2018.02.19 加入

还未添加个人简介

评论

发布
暂无评论
【架构笔记之框架设计】架构师训练营第1期第2周