写点什么

架构师训练营 1 期第 2 周:框架设计 - 总结

用户头像
piercebn
关注
发布于: 2020 年 09 月 25 日

如何进行优雅的程序设计,使软件的架构更加的富有弹性,更加灵活,易于扩展和易于维护,是软件架构师的一个重要工作职责。本文分析了编程语言的实质,梳理软件编程方法的演变过程,总结面向对象编程的特征,目的,指导原则,以及如何遵循这些原则来进行框架设计。

一、编程语言的实质及演变过程

编程的本质

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

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

  • 问题领域(问题空间):包含与系统所要解决的问题相关的实物和概念的空间。比如电灯的开关。

  • 编程语言是一种“抽象”的机制,其编程方法的演进如下

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

  • 非结构化的高级语言:对计算处理逻辑的抽象

  • 结构化的程序设计:开始对问题领域进行一定程度的抽象。比如开关函数

  • 面向对象程序设计:直接表达问题空间内的元素。比如电灯对象及其开关方法



编程的核心要素和发展脉络

  • 编程的核心要素包括:人,计算机,客观业务领域

  • 编程方法的演进方向也是围绕这三个要素发展的,从面相机器的思维方式,到面向人的思维方式,最后到面向业务领域问题的编程,也就是我们现在使用的面向对象的编程。展现了事物发展的必然规律,面向对象编程更符合我们对问题的理解,也更符合我们对编程的认知。

  • 面向业务领域也可以进行细分,业务领域对象可以是实体对象,在大数据时代,业务领域对象也可以是数据,我们需要面向数据进行编程,这时一些新的编程语言或特性会出现,使面向对象编程的关注点关注到了数据里,如函数式编程,反应式编程等等,抓住了这个特点,有利于我们把握编程语言的发展脉络,当未来出现新的编程语言特性出现的时候,我们也可以判断它们是面向业务领域细分了,还是编程方法倒退了。

二、面向对象编程的特征,目的,指导原则

面向对象编程的定义(第一个成功的面向对象的语言Smalltalk描述):

  • 万物皆为对象

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

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

  • 每个对象都拥有其类型

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



面向对象编程的三要素(关键特征)

  • 封装性 - 隐藏实现

  • 继承性 - 接口的重用

  • 多态性 - 对象互换的魔法



正式有了多态,面向对象编程才表现出各种纷繁复杂的特性

  • 在程序开发时使用父类抽象类接口类进行编程,这时我们并不知道具体的方法表现出什么样的行为,运行期使用具体的子类来替代接口,表现出不同的特性,整个面向对象的各种编程技巧,设计模式,设计原则其实都是围绕多态特征展开的



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

  • 充血模型与贫血模型

  • 领域驱动设计DDD



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

  • 目的:实现系统的强内聚、松耦合的特性,从而实现系统的易扩展,更强壮,可移植,更简单的特性

  • 原则:为实现上述目标,人们总结出来的一些指导原则,来指导程序设计开发。指导原则主要包括一些设计原则,贯彻设计原则的设计模式,以及基于这些设计原则、设计模式构建出来的一些框架、工具。

  • 设计原则,设计模式

  • 基于设计原则开发一个框架

  • 基于设计模式对代码进行重构

  • 框架,工具

  • 框架是用来实现某一类应用的结构性的程序,是对某一类架构方案可复用的设计与实现,程序必须要依附在框架上面进行进一步的开发和实现,框架决定了程序的整体结构和主体流程,程序由框架调用完成程序的整体结构

  • 作为架构师,能够理解框架背后的规律和框架设计的方法,要开发出自己的框架,或对现有框架进行自己的改进,基于框架的特点,使框架更加符合你要开发的系统的应用场景

  • 框架调用代码,代码调用工具,架构师用框架(如Tomcat,Spring)保证架构落地,保证整体的程序运行流程和整体的结构,用工具(如Log4j)来提高开发效率,可复用的一些操作可通过提供一些工具来复用

三、设计原则与框架设计

软件设计敏捷和软件过程敏捷

  • 对于一个软件开发而言,要想实现敏捷的软件开发,并不只在于敏捷的软件过程保证,更重要的是设计本身是敏捷的(灵活强大)。当我们有需求变更的时候,当我们要快速迭代的时候,这些设计能够快速的支撑我们进行变更和迭代;而不是耦合在一起,僵硬的编程一团,脆弱的,不可移植的,难以使用的,让我们无从下手,再好的软件过程保证,也难以实现一个敏捷的软件开发。



设计的好坏如何评断?软件设计如何应对不断的需求变更(上下文环境)?

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

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

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

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

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

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

  • 僵硬 - 不易改变

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

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

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

  • 晦涩 - 代码难以理解

  • 过度设计、copy-paste 代码

设计原则1:开闭原则(OCP)

开闭原则(OCP,Open/Closed Principle):对扩展功能开放,对修改代码封闭,不修改代码实现功能的扩展,靠的是抽象,对抽象进行编程

  • 方法一:继承或实现接口

  • 方法二:使用策略模式:不直接依赖目标对象,而是依赖策略接口,目标对象实现策略接口,但是是目标对象脆弱了

  • 方法三:适配器模式:目标对象通过适配器实现策略接口,变化的需求通过不同的适配器来实现

  • 方法四:观察者模式:优化策略接口,将策略接口改成Listener接口,可通过内部类的方式实现适配器进行事件监听



案例:设计一个控制电话拨号的软件,用例如下:

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

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



原始设计



设计问题

  • 僵硬 - 不易增加、修改

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

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

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

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

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

  • 不可移植 - 设想我们要设计密码锁的按钮,它只需要数字按键,但 Button 的设计使它必须 “附带”一个“Send”类型的按钮。



改进设计

设计原则2:依赖倒置原则(DIP)

依赖倒置原则(DIP,Dependency Inversion Principle):高层模块不能依赖低层模块,而是大家都依赖于抽象,抽象不能依赖实现,而是实现依赖抽象。倒置开发顺序和职责,先开发接口,接口属于高层模块,低层模块实现接口。高层模块根据自己使用场景进行接口设计。软件层次化,高层决定低层,高层被重用

  • 框架设计的核心,框架不依赖于代码,但是却可以调用代码,高层(框架)定义一组接口,我们(代码程序)实现接口。

  • 好莱坞原则:Don’t call me,I’ll call you.倒转了层次依赖关系。

  • 如Tomcat基于J2EE规范(Servlet,web.xml)进行编程,Tomcat实现了Http接口的监听,获得Http的数据包,将Http的数据包解析成Request和Response的对象,然后根据URL和web.xml配置获得对应的Servlet接口,然后将请求参数传递给Servlet的doGet和doPost的方法调用就可以了。我们的应用程序实现Servlet接口即可在框架下运行

  • 如开发异步服务,反应式编程框架实现

设计原则3:里氏替换原则(LSP)

里氏替换原则(LSP,Liskov Substitution principle):用来解决继承合理性的问题,如果在编写程序中,子类可以替换父类,则继承是合理的,针对父类编写的程序可以用子类替换。在应用场景中看子类能否替换父类,替换后程序能否正常运行,能正常运行就是合理的继承,不能正常运行就是不合理的继承。同时也可以判断抽象接口使用是否合理。

  • LSP反例:

  • JDK中Properities和HashTable

  • Stack和Veckor

  • 正方形和长方形

  • 不看状态,看行为

  • 设计和界定一个类,应该以其行为作为区分

从“契约”角度看:遵循可替换原则

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

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

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

  • 提取共性到基类

  • 将继承改为组合:将父类变成子类的成员变量

  • 继承与组合:软件设计中优先使用组合来代替继承

  • 继承破坏了封装

  • 基类发生变化层层影响子类

  • 继承是静态的无法在运行时改变,同时使类数量增长爆炸

  • 何时检测LSP?

  • 一个继承孤立的看,并不能看出其是否符合LSP原则,重要是需要放到场景中,只有放到应用程序中,放到执行代码的上下文汇总才能看出继承是有问题的。

  • 违反LSP的征兆:

  • 派生类中出现退化函数

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

设计原则4:单一职责原则(SRP)

单一职责原则(SRP,Single Responsibility Principle):内聚性原则,一个类只有一个引起它变化的原因,如果原因很多,说明类是不单一的,导致类的职责很多,设计复杂,难以维护。

  • 违反SPR原则的后果

  • 脆弱性:功能耦合

  • 不可移植性:包含不必要的依赖

  • 解决办法:

  • 分清职责,可以通过类进行拆分,通过类组合功能实现复杂的类

  • 代码少,职责单一,易于维护



举例说明:

  • Rectangle类包含了两个职责:

  • draw()在GUI上画出自己

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

  • 有两个应用分别依赖Rectangle:

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

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



设计问题:

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

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



设计原则5:接口隔离原则(ISP)

接口隔离原则(ISP,Interface Segregation Principle):职责不好分清时,如何优化?不应该强迫客户程序依赖它们不需要的方法

  • 通过拆分接口进行隔离,从而保证应用程序不依赖它们不需要的方法

  • 隔离方法:

  • 通过适配器隔离

  • 通过多继承隔离

举例说明:

  • 实现一个可定时关闭的门。 

设计问题

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

  • 如果Timer发生改变,导致timeout方法改变,所有不需要定时功能的 Door 的客户程序都受到影响。

四、框架设计案例

反应式编程框架 Flower 的设计

  • 实现基于普通编程方式的反应式编程,降低反应式编程框架的使用门槛,开发工程师不用学函数式编程即可开发反应式系统

  • 基于函数式编程的反应式编程框架:Reactor,WebFlux,RxJava

  • 基于普通编程方式的反应式编程框架设计:Flower

  • 反应式编程框架解决什么问题?

  • 由于大量用户并发访问,导致服务端的有限资源被占用(工作线程数量,数据库连接数量),正在处理的请求占用资源阻塞等待IO操作(网络IO操作,文件IO操作),无法释放资源,新用户的请求由于系统资源耗尽无法被响应,导致用户请求超时,服务不可用

  • 解决方法:通过异步操作改善系统性能

  • 网络异步IO编程多路复用:用有限线程(容器线程)处理多个并发请求,交给Flower运行环境处理,Flower负责创建工作线程(通过Akka Actor实现),处理服务消息,线程不会被阻塞,所以可以用来处理其他消息

  • Flower框架设计:遵循依赖倒置原则(DIP)

  • Flower框架里定义的抽象接口,供开发者来实现,开发步骤如下

  • 实现Service<String, String>接口,定义两个泛型,第一个是输入消息的类型,第二个是返回消息的类型,process接口方法接收输入消息,返回消息作为下一个Service的输入消息

  • 编排Service流程:ServiceA -> ServiceB -> ServiceC

  • 调用异步服务流程

  • Flower核心模块设计



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

piercebn

关注

还未添加个人签名 2019.07.24 加入

还未添加个人简介

评论

发布
暂无评论
架构师训练营 1 期第 2 周:框架设计 - 总结