写点什么

极客大学 - 架构师训练营 第二周

用户头像
9527
关注
发布于: 2020 年 09 月 24 日

第二周 框架设计学习总结

灵魂拷问:如何进行优雅的程序设计,从而使软件的架构更加的富有弹性和灵活,更加的易于扩展和易于维护?

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

1. 为什么最终进化到面向对象编程?

首先来看看编程语言的时间线

可以看到,随着时间线的从左到右,之前的各种编程语言,由最早期的电线开关式编程,到打孔纸带输入编程,到汇编语言,到非结构化编程语言,再到结构化面呈语言,再到面向过程的编程语言,最后进化到现在的面向对象编程语言。这其实是编程语言进化的必然规律。从最开始的和机器接近,到现在的和人类世界更加接近,其背后是一个自然发展,进化的规律。因为现实世界是由对象组成的,不同的对象进行交互。而我们在开发软件的时候,能不能模拟/映射现实世界的逻辑以及对象的交互,从而使得软件编程也如同各个不同的对象一样来进行交互呢?答案很明显,就是面向对象编程,因为其可以更好的映射事物之间的交互方式或者交互规律。



2. 面向对象编程的特点和目的:

  • 面向对象编程从对象本身出发,对象之间进行合理的交互,从而完成系统的开发和设计

  • 设计的对象是不是真的有意义?贴近于现实生活中的对象?

  • 目的:

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

  • 易扩展 - 增加新功能

  • 更强壮 - 不容易被破坏

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

  • 更简单 - 易于理解和维护



3. 编程的实质,你领悟到了吗?

为什么要编程?是为了利用计算机来解决现实世界的问题。

什么是编程的过程?在计算机所能理解的“模型”和现实世界之间,建立起一种联系

编程的三部曲是什么?分析领域问题并抽象成模型,然后设计和开发软件系统



4. 编程的核心要素

其实每一种编程语言的进化和归类,都离不开以下三要素:



5. 学习新的技术灵魂拷问

这门你要学习的技术,你知道它发明的初衷吗?它是为了解决什么问题?它是如何发展到了现在的地步?为什么别的技术不能比这门技术做的更好?不要仅仅只抓住皮毛,要有深度,要真是能学到皮毛就能做好事情,那人人都是架构师了。不要仅仅只学手段,要看到本质。



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

1.为啥要写优秀的代码

老规矩,上图

优秀代码/设计和功能的关系图

糟糕代码/设计和功能的关系图(想起了指数函数吗??)

2. 何为不好的软件设计?

  • 僵硬性(Rigidity)- 很难改动,任何一个改动都会迫使许多对系统其它部分的改动

  • 脆弱性(Fragility) - 对系统某一个地方的改动会导致系统中和改动无关的地方出现问题

  • 牢固性(Immobility) - 设计中的有用代码很难被抽离出来被其他系统使用

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

  • 粘滞性(Viscosity) - 做错误的事情比做正确的事情容易,引诱程序员破坏原有设计

  • 晦涩性(Opacity) - 代码难以理解

  • 过度设计,copy-paste代码(Needless Complexity/repetition) - 设计包含没有用的部分以及重复结构



开闭原则 - Open/Closed Principle

1. 何为开闭原则?
  • 对于扩展是开放的 - Open for extension

  • 对于更改是关闭的 - Closed for modification

  • 不需要修改软件的实体,就可以实现功能的扩展

  • 而实现以上最最关键的是抽象!

2. 类图实例



  • 客户端程序不直接依赖我们要依赖的目标对象

  • 定义一个策略接口 (ButtonServer), 从而去依赖策略接口

  • Button类依赖ButtonServer接口

  • 将Button按下的token传给buttonPressed的方法

  • Dailer类去实现这个ButtonServer接口

  • 依赖的目标对象去实现这个接口

  • 在Button类里,当press()方法调用的时候,其只需要调用ButtonServer接口的buttonPressed()就可以了

  • Button.press()方法里不需要做任何具体的操作

  • Button类不直接依赖Dialer类

  • 增加适配器,将ButtonServer的接口调用,转化成对Dialer的调用

  • DigitButtonDailerAdepter调用Dailer里enterDigit()的方法

  • SendButtonDailerAdepter调用Dailer里dail()的方法

  • 这样Dailer类里无需再有switch case的判断



依赖倒置原则 - Dependency Inversion Principle

1. 何为依赖倒置?

常规来说,高层模块一般是依赖于低层模块的,低层模块定义和实现一个功能之后,高层模块再来根据实现进行抽象和调用,这样导致高层模块难以被复用

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

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

  • 依赖倒置,倒置的是类的依赖关系,倒置的是开发的顺序和职责

  • 根据依赖倒置,我们先定义一个抽象,一个接口,一个规范,然后高层模块去调用这个接口 (接口属于高层模块),而低层模块去实现这个接口,从而使得高层模块不再依赖低层模块。

2. 传统依赖 vs 依赖倒置





里氏替换原则 - Liskov substitution principle

为什么会有“里氏替换原则”?它解决了什么问题?

1. 里氏替换原则的描述
  • 若对每个类型T1的对象O1, 都存在一个类型T2的对象O2,使得在所有针对T2编写的程序P中,用O1替换O2后,程序P的行为功能不变,则T1是T2的子类型

  • subtype必须能够替换它们的base type,才算遵循里氏替换原则

  • 很重要的一点是要看类的使用场景,而不是单纯从定义上去判断继承的合理性

  • 凡事使用基类的地方,一定也适用与其子类

2. 违反LSP的示例

为什么以上代码不符合里氏替换原则?(现有的子类替换进来没有问题,但是如果有一个新的子类传进来,而不在 else if 或者 else 的条件里,那么就不再工作)

3. 为什么要用行为来设计和界定一个类

从对象的属性来说,对于同一个类,所创建的不同对象,它们的

  • 标示 - 不相同

  • 状态 - 不同

  • 行为 - 相同

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



单一职责接口隔离

All pieces of software must have only a single responsibility.

1. 单一职责原则(SRP) - Single Responsibility Principle
  • 又被称为“内聚性原则(Cohesion)", 意为:

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

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

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

  • 如果一个类,引起它变化的原因有很多,那么它的职责就是不单一的



2. 什么是职责?
  • 一个职责是一个变化的原因



3. 违反SRP原则引起的后果
  • 脆弱性 - 把两个不想关的功能耦合在一起,当修改其中一个的时候,另外一个功能可能会意外受损

  • 不可移植性 - 当只需要使用其中一个功能的时候,却在引用的时候不得不包含一个不必要的依赖



4. 违反SRP的代码示例



def calculate_price(products: List[Product]) -> Decimal:
"""Returns the final price of all selected products (in rubles)."""
price = 0
for product in products:
price += product.price
logger.log('Final price is: {0}', price)
return price

在以上代码中,突兀插入的 logger.log 破坏了 calculate_price 的单一职责性。在调用函数的时候,我们完全不知道函数中还有一个 logger 的调用,如果是这样的话,那么函数的命名应该为 calculate_and_log_price。 这样一来就很明显,如果函数名字里有 and, or 或者 then 的话,那么应该考虑重构。



接口分离原则 (ISP) - Interface Segregation Principle

When we define an interface that provides multiple methods, it is better to instead break it down into multiple ones, each one containing fewer methods (preferably just one), with a very specific and accurate scope.

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

1. ISP和SRP的关系
  • 是相互关联的,都和“内聚性”有关

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

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

2. ISP设计示例

假设我们有一个 EventParser 从不同的data source(假设xml和json) 得到数据,然后进行分析,一般的做法是建立一个 abstract base class 然后在其中定义 from_xml() 以及 from_json()两个方法,然后继承类去实现。



但是对于不需要json格式的类,我们强迫其要实现 xml 类,这就破坏了ISP的原则。正确的做法是把这个 EventParser 接口再细化成以下:





第二周 作业一

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

答案:

依赖倒置原则定义

依赖倒置原则 (Dependency Inversion Principle), 简称DIP,是 Object Mentor 公司总裁罗伯特·马丁(Robert C.Martin)于 1996 年在一份 C++ Report 上发表的文章。其最原始的定义为:

  • 高层模块不应该依赖低层模块,两者都应该依赖其抽象;High level modules should not depend upon low level modules. Both should depend upon abstractions.

  • 抽象不应该依赖细节/实现,细节/实现应该依赖抽象; Abstractions should not depend upon details. Details should depend upon abstractions。

其核心思想是:要面向接口编程,不要面向实现编程。依赖倒置原则是实现开闭原则的重要途径之一,它降低了客户与实现模块之间的耦合。DIP倒置的模块和包之间的依赖关系,也倒置了开发的顺序和职责。

例子:

一般来说,当一个高层模块依赖于低层模块的时候,那么被依赖的模块(低层) 总是要先被开发出来,然后高层模块才能够调用。而如果基于依赖倒置原则,高层模块不再依赖于低层模块,而是依赖于一个抽象,该抽象可能是一个接口 (接口属于高层模块),规范,配置。大家都依赖于这个接口进行编程。于是开发顺序就变成了先开发接口,然后基于这个接口的抽象,高层模块来调用这个接口,而低层模块来实现这个接口。这种开发模式使得高层模块与低层模块之间的依赖关系被取消。



依赖倒置原则的好处

  • 高层模块可以根据自己的使用场景进行模块接口的设计

  • 低层模块按照高层的设计和抽象进行实现,而不是低层模块实现完了之后,再抽象出一个属于低层的接口,让高层来调用。

  • 高层决定低层,高层被重用/复用



依赖倒置原则的例子

传统依赖

Policy Layer (高层)依赖于 Mechanism Layer (低层),而Mechanism Layer (低层) 依赖于更低层的Utility Layer。在这个图里,Utility layer可以被其它高层复用,而Policy Layer就很难被复用,因为其每一次的复用都要依赖于低层模块。



遵循DIP的层次依赖关系

Policy依赖Policy Service Interface这个接口。而Mechanism层去实现接口



依赖倒置原则为什么被称为好莱坞规则?

由于依赖倒置原则也是框架设计的核心,框架的重要特点是由框架去调用我们的应用程序,而不是由我们的应用程序来调用框架。同时框架也不依赖于我们的应用程序。比如Tomcat这样的框架,不依赖于任何Java的应用程序,只负责调用。"Don't call 框架,框架 will call you", 这点跟好莱坞原则很相似,好莱坞原则指的是 "Don't call me, I'll call you", 所以依赖倒置原则也被称为好莱坞规则。



依赖倒置原则的作用

  • 降低类间的耦合性

  • 提高系统的稳定性

  • 减少并行开发引起的风险

  • 提高代码的可读性和可维护性



依赖倒置原则的实现方法

依赖倒置原则的目的是通过要面向接口的编程来降低类间的耦合性,所以我们在实际编程中只要遵循以下4点,就能在项目中满足这个规则。

  1. 每个类尽量提供接口或抽象类,或者两者都具备。

  2. 变量的声明类型尽量是接口或者是抽象类。

  3. 任何类都不应该从具体类派生。

  4. 使用继承时尽量遵循里氏替换原则。



第二周 作业二

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

  • 通过接口隔离不同的实现类里的方法

  • 不强迫应用程序依赖它们不需要的方法





类说明

Cache类有四个方法,其中get, put以及delete都是普通的cache的操作,而reBuild则是根据具体应用场景衍生出的方法,这四个方法都有着一定联系 (在特定的应用场景中)

  • get

  • put

  • delete

  • reBuild - Cache可以在程序的运行期重建自己的配置,在程序的运行期间,要是配置发生了变化,该方法可以把新的配置文件热推送到应用程序正在运行的服务器



说明:

  1. 对于只需要进行一般Cache操作的应用程序,那么只需要调用CacheData Operation 接口就可以。并不需要知道有reBuild方法

  2. 对于需要去热更新Cache里的配置的应用程序,那么只需要知道reBuild的方法就可以。

  3. 定义最基本的一个Cache类,包含三个方法: get/put/delete

  4. 一般的应用程序可以直接调用Cache基本类

  5. 创建ServiceCache继承Cache,添加reBuild方法

  6. 创建CacheManager接口,实现接口隔离原则



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

9527

关注

还未添加个人签名 2020.04.22 加入

还未添加个人简介

评论

发布
暂无评论
极客大学 - 架构师训练营 第二周