写点什么

02 框架设计

用户头像
escray
关注
发布于: 2020 年 09 月 26 日
02 框架设计

2.1 从编程历史看面向对象编程的本质与未来


莱布尼兹的奇思怪想


人类的第一位程序员 


雅卡尔 可编程织布机

Ada 


什么是计算机?什么是程序?


通用计算机


现代计算机与现代的程序


冯诺依曼 ENIAC


插电线


形形色色的汇编语言


每一种 CPU 都有独特的机器语言,因而需要不同的汇编语言。


形形色色的编程语言


早期 Basic 语言

结构化的 Basic 语言


Perl 语言,“伪”面向对象语言


70 年代,软件危机


C 语言,结构化语言

C++ 语言,C++ 向后兼容 C 的所有功能,并且提供了面向对象的编程机制

Java 语言,完全面向对象的语言


编程语言的实质


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


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


编程语言是一种“抽象”的机制,问题是对“谁”来抽象。


抽象的种类


机器代码和汇编语言:对基础机器进行抽象

非机构化高级语言(Basic,Fortran):对计算处理逻辑抽象

结构化的程序设计:开始对问题领域进行一定程度的抽象

面向对象程序设计:直接表达问题空间内的元素


编程方法的演进


汇编语言 → 高级语言 → 结构化程序 → 面向对象编程 OOP


编程的核心要素


  • 人(劳动者)

  • 客观业务领域(劳动对象)

  • 计算机(劳动工具)


什么是面向对象编程?


Smalltalk


  • 万物皆为对象

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

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

  • 每个对象都拥有其类型

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


什么是对象?


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

  • 行为:每个对象可以产生行为

  • 标识:表明每个对象都区别于其他的对象


面向对象编程三要素


  • 封装 Encapsulation:隐藏实现细节(访问控制),定义接口

  • 继承 Inheritance:IS-A 关系,HAS-A 关系(组合)

  • 多态 Polymorphism:后期绑定(虚函数),向上转形(Up-Casting)


封装并不是面向对象编程语言独有的,C 语言可以在头文件 .h 里面定义方法,在实现文件 .c 中定义具体的结构体和方法实现

继承也不是面向对象编程语言独有的,C 语言中 A 结构体包含 B 结构体的定义,可以理解为 A 继承了 B

多态也不是面向对象独有的,C 语言中有指向函数的指针,可以实现多态(危险)


在面向对象的编程语言中,多态非常简单。


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


面向对象就是利用多态特性进行编程


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


  • 充血模型与贫血模型

  • 领域驱动设计


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


强内聚、低耦合


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

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

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

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


设计模式 design patterns


用于解决某一种问题的通用解决方案

语言中立

贯彻设计原则


Gang of Four (Erich Gamma, Richard Helm, Ralph Johnson and John Vlissides)


  • 创建模式

  • 行为模式

  • 结构模式


框架 frameworks


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


简化应用开发者的工作

实现了多种设计模式


Web 服务器也是框架:Tomcat


框架 vs 工具


框架调用应用程序代码,应用程序代码调用工具


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


35 分钟内讲完程序语言的历史演进,概述面向对象编程要点


2.2 设计臭味:糟糕的代码有哪些特点?


UML 小练习


在面向对象分析的时候,把名词作为对象的候选


软件开发的上下文,不断需求变更 


其实我没怎么看出来那个拨号程序有什么问题,虽然从直觉上感觉似乎有点不对。


软件设计的“臭味”


“不好的”软件,“臭味”,“坏味道”


(Review 代码的时候,被恶心吐了)


一坨代码


  • 僵硬 Rigidity:不易改变,每个改动都会迫使血多对系统其他部分的改动

  • 脆弱 Fragility:只想改 A,结果 B 被意外破坏(换轮子的时候,车窗碎了),对系统的改动会导致系统中和改动地方无关的许多地方出现问题。

  • 不可移植(牢固)Immobility:不能适应环境变化,很难解开系统的纠结,组件重用

  • 导致无用的陷阱(粘滞)Viscosity:做错误的事比做正确的事更容易,引诱程序员破坏原有设计。做正确的事情比做错误的事情要困难。

  • 不必要的复杂性 Needless Complexity:设计中包含不具有任何直接好处的基础结构

  • 不必要的重复 Needless Repetition:设计中包含有重复的结构

  • 晦涩 Opacity:代码难以理解,很难阅读、理解,没有很好的表现出意图。程序是给人看的。

  • 过度设计、copy-paste 代码


代码腐化的例子


  1. 从键盘读入字符并输出到打印机


void Copy() {    int c;    while ((c=RdKbd()) != EOF)        WrtPrt(c);}
复制代码


  1. 增加读取纸带机功能


bool ptFlag = false;// remember to reset this flagvoid Copy() {    int c;    while ((c = (plflag ? Rdpt() : RdKbd())) != EOF)        WrtPrt(c);}
复制代码


flag 的含义不明确;

false 的含义不明确;

奇怪的 flag 加上奇怪的注释

只能通过读代码,才能知道 flag 的含义(flag 为真,则表示从纸带机读数据)


  1. 增加纸带机输出


bool punchFlag = false;// remember to reset these flagsvoid Copy() {    int c;    while ((c = (plflag ? Rdpt() : RdKbd())) != EOF)        punchFlag ? WrtPunch(c) : WrtPrt(c);}
复制代码


现在你已经有了“一坨代码”


一个遵循 OOD 原则的设计


public interface Reader {    public int read();}
public interface Writer { public void write();}
public class KeyboardReader extends Reader { public int read() { return readKeyboard(); }}
public class PaperTapeReader extends Reader { public int read() { return readPaperTape(); }}
public class Printer extends Writer { public void write() { writePrinter(); }}
public class PaperTapeWriter extends Writer { public void write() { writePaperTape(); }}

Reader reader = new KeyboardReader();Writer writer = new Printer();
public void copy() { int c; while ((c=reader.read()) != EOF) Writer.writer();}
Reader ptReader = new PaperTapeReader();Writer ptWriter = new PaperTapeWriter();
复制代码


看了李老师最后对于那个拨号器设计“坏味道”的分析,考虑了一下为什么自己没能够“闻”出来,其实就是没有按照那几个标准去衡量,另外一点,就是自己没有想到如何扩展拨号器,比如用于其他类型的门锁。


这一章比较大的收获,就是以前看待设计的时候用的是静态的眼光,所以看不出有什么好处;如果采用动态的角度,考虑到软件系统后续可能的变化,那么设计模式、设计原则就都显示出了威力。


按照目前的分析,我觉的 Button 按钮接口,应该有类型(数字或者命令)和数值两个属性,并且可以有 push() 续方法。


2.3 开闭原则介绍及代码分析


OCP, Open/Closed Principle


Open for extension, Closed for modification

对于扩展是开放的,对于更改是封闭的。


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


改进 Button 的第一种方法,感觉比较容易想到;而第二种方法,策略模式就稍微要费一点脑筋了。


我的理解是通过传递不同的 token,然后 Dailer 中实现的 buttonPressed(int token) 方法来处理,Dailer 成为瓶颈,而且不满足开闭原则。


然后是方法三,适配器模式


(9 分 30 秒左右,图片里面有两个命名错误 Adepter → Adapter )


方法四,观察者模式


这一段对于几个设计模式的解说,非常精彩。要是有代码配合就更好了。


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 ButtonDialerAdapter implements ButtonListener { void buttonPressed() { }}
public class SendButtonDialerAdapter implements ButtonListener { void buttonPressed() { }}
public class Dailer { public void enterDigit(int digit) { System.out.println(“enter digit: “ + digit); }
public void dial() { System.out.println(“dialing…”); }}
public class Phone { private Dialer dialer; private Button[] digitButtons; private Button sendButton;
public Phone() { dialer = new Dialer(); digitButton = 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.digitButton[9].press(); phone.digitButton[1].press(); phone.digitButton[1].press();
phone.sendButton.press(); }}
复制代码


2.4 依赖倒置原则介绍及代码案例分析


DIP, Dependency Inversion Principle


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

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


DIP 倒置了模块或包的依赖关系,开发顺序和职责。


高层决定低层:高层模块定义(依赖)一个接口,供底层模块来实现。

高层被重用:高层被重用的可能更大


面向抽象编程,关键的问题在于抽闲属于谁


好莱坞规则:Don’t Call me, I will Call you.


Tomcat 基于规范或者 Servlet 接口开发。高层模块不依赖低层模块,框架不依赖与实现代码,而是依赖于接口,框架可以调用代码。


Button 的本质:检测用户的按键指令,传递给目标对象。

用什么机制检测用户的按键,目标对象是什么,都不重要。


2.5 里氏替换原则


1988,Barbara Liskov


若对每个类型 T1 的对象 o1,都存在一个类型 T2 的对象 o2,使得在所有针对 T2 编写的程序 P 中,用 o1 替换 o2 后,程序 P 的行为功能不变,则 T1 是 T2 的子类型。


子类型 subtype 必须能够替换掉他们的基类型 base type


LSP 指导软件框架开发。


关于正方形不能继承长方形的例子,我觉的可能也需要放到场景中,可能只是在 testArea 这个测试中不合适。


按照正方形不能继承长方形的思路,那么直角三角形也不能继承三角形?


对于同一个类,所创建的不同对象,标识和状态不同,但是行为相同;因此,设计和界定一个类,应该以其行为作为区分。


有一个小的疑问,如果不符合里氏替换原则,那么会带来什么恶果呢?


使用基类的地方,一定也适用于其子类。


  • 子类一定得拥有基类的整个接口

  • 子类的访问控制不能比基类更严格


可以提取共性到基类


继承比较容易,基类的大部分功能可以通过继承进入子类;

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


组合优于继承。


一个模型,只有通过客户程序或者使用场景才能体现出来是否有效,而设计的使用者很难预测,所以从设计的角度要避免“过于复杂”或“过度设计”。


可能违反 LSP 的征兆


  • 派生类中的退化函数

  • 派生类中抛出基类不会产生的异常(如果派生类抛出异常,那么一般是基类同名方法抛出异常的子类)


其实我挺想看到一个符合 LSP 原则的代码。


2.6 单一职责接口隔离


SRP, Single Responsibility Principle


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


违反 SRP,把两个功能(变化原因)耦合在一起,会引起程序脆弱,修改其中一个功能时,另一个功能可能会议外受损;并且有可能造成代码不可移植(或者移植不需要的代码)


ISP, Interface Segregation Principle


不应该强迫客户程序依赖(看到)他们不需要的方法


实现类无法拆解的方法,可以通过不同的借口暴露给不同的客户程序。


2.7 案例:反应式编程框架 Flower 的设计


一般的并发用户请求场景,在客户端网络连接、服务器远程调用和数据库访问三个 IO 密集的位置都有可能产生阻塞,导致程序变慢,进而可能系统崩溃。


那个 Flower 反应式重构前后性能对比,不知道与 C# 和 Java 的语言特性或者部署平台有多少关系?


我觉得 Flower 的设计里面,对于消息机制的应用是最巧妙的。另外对于,异步数据库驱动有一点好奇,这个是否属于 Flower 的范畴?


2.8 第二周课后练习


作业一:


  1. 请描述什么是依赖倒置原则,为什么有时候依赖倒置原则又被称为好莱坞原则?

  2. 请用接口隔离原则优化 Cache 类的设计,画出优化后的类图。


提示:cache 实现类中有四个方法,其中 put get delete 方法是需要暴露给应用程序的,rebuild 方法是需要暴露给系统进行远程调用的。如果将 rebuild 暴露给应用程序,应用程序可能会错误调用 rebuild 方法,导致 cache 服务失效。按照接口隔离原则:不应该强迫客户程序依赖它们不需要的方法。也就是说,应该使 cache 类实现两个接口,一个接口包含 get put delete 暴露给应用程序,一个接口包含 rebuild 暴露给系统远程调用。从而实现接口隔离,使应用程序看不到 rebuild 方法。


作业二:根据当周学习情况,完成一篇学习总结

发布于: 2020 年 09 月 26 日阅读数: 34
用户头像

escray

关注

Let's Go 2017.11.19 加入

在学 Elasticsearch 的项目经理

评论

发布
暂无评论
02 框架设计