架构师训练营学习总结——框架设计【第二周】
上周内容回顾
需求分析、概要设计、详细设计阶段分别需要画哪些图?
1 编程的本质和未来
1.1 编程语言的实质
编程的目的是:用计算机来解决现实世界的问题。
编程的过程即:在计算机所能理解的“模型”(解空间)和现实世界(问题空间)之间,建立一种联系。
编程语言是一种“抽象”的机制,问题是对“谁”来抽象:
1.2 问题领域(Problem Domain)
包含与系统所要解决的问题相关的实物和概念的空间。
1.3 抽象的种类
机器代码和汇编语言——对基础机器进行抽象
非结构化的高级语言(如 Basic,Fortran 等)——对计算处理逻辑抽象
结构化的程序设计——开始对问题领域进行一定程度的抽象
面向对象的程序设计——直接表达问题空间内的元素
1.4 什么是面向对象编程
第一个成功的面向对象的语言 Smalltalk 描述:
万物皆为对象
程序是对象的集合,它们通过发送消息来告知彼此所要做的
每个对象都有自己的由其他对象构成所构成的存储
每个对象都拥有起类型
每一种特定类型的所有对象都可以接收同样的消息
1.4.1 什么是对象
Booch 对于对象的描述:对象具有状态、行为和标识。
状态:表明每个对象可以有自己的数据
行为:表明每个对象可以产生行为
标识:表明每个对象都区别于其他的对象。(唯一的抽象)
1.4.2 面向对象编程的三要素(特征)
封装性(Encapsulation):隐藏实现细节(访问控制);定义接口
继承性(Inheritance): IS-A 关系 (注:HAS-A 组合关系)
多态性(Polymorphism):后期绑定(虚函数)、向上转型(Up Casting)
1.4.2.1 封装性——隐藏实现
封装并不是面向对象编程语言独有的。
面向过程的编程语言,比如 C 语言,也可以实现封装特性,在头文件.h 里定义方法,而在实现文件.c 文件里定义具体的结构体和方法实现,从而使依赖.h 文件的外部程序只能够访问头文件里定义过的方法,这样同样实现了变量和函数的封装,以及访问权限的控制。
1.4.2.2 继承性——接口的重用
继承也不是面向对象编程语言独有的。
C 语言也可以实现继承。如果 A 结构体包含 B 结构体的定义,那么就可以理解成 A 继承了 B,定义在 B 结构上的方法可以直接(通过强制类型转换)执行 A 结构体的数据。
1.4.2.3 多态性——对象互换的魔法
多态也不是面向对象编程语言独有的。因为有指向函数的指针,多态事实上在 C 语言中也可以实现。
但是使用指向函数的指针实现多态是非常危险的,因为这种多态没有语法呵呵编译方面的约束,只能靠程序员之间约定,一旦出现 BUG,调试非常痛苦。因此在面向过程语言的开发中,这种多态并不能频繁使用。而在面向对象的编程语言中,多态非常简单,子类实现父类或者接口的接口方法,程序使用抽象父类或者接口编程,运行期注入不同的子类,程序就表现出不通过的行为,是为多态。
1.4.3 面向对象编程与面向对象分析
面向对象编程不是使用面向对象的编程语言进行编程,而是利用多态性进行编程。
面向对象分析是将客观世界,即编程的业务领域进行对象分析。
充血模型与贫血模型
领域驱动设计 DDD
1.4.4 面向对象设计的目的和原则
面向对象设计的目的:
强内聚、低耦合、从而是系统
易扩展——易于增加新的功能
更强壮——不容易被粗心的程序员破坏
可移植——能够在多样的环境下运行
更简单——容易理解、容易维护
面向对象设计的原则:
为了达到上述设计目标,有人总结出了多种指导原则
“原则”是独立于编程语言的,甚至也可以用于非面向对象的编程语言中。
设计模式(design patterns)
设计模式是用于解决某一种问题的通用的解决方案。
设计模式也是语言中立的。
设计模式贯彻了设计原则。
三大类 23 种基本的设计模式:创建模式、行为模式、结构模式。
在更细分的领域当中还可以总结出许多设计模式:并发编程模式、JavaEE 模式等
框架(frameworks)
框架是用来实现某一类应用的结构性的程序,是对每一类架构方案可复用的设计与实现
如同框架结构的大厦的框架
简化了开发这的工作
实现了多种设计模式,是应用开发者不需要花太大的力气,就能设计结构良好的程序来
不同领域的框架
微软公司为 Windows 编程开发了 MFC 框架
Java 为它的 GUI 开发了 AWT 框架
开源框架:MyBatis,Spring 等
Web 服务器也是框架:Tomcat
框架 VS 工具
框架调用应用程序代码
应用代码调用工具
架构师用框架保证架构的落地
架构师用工具提高开发效率
2 面向对象设计的基本原则
2.1 软件设计的“臭味”
软件设计的最终目的,是使软件达到“强内聚、松耦合”,从而使软件:
易扩展 —— 易于增加新的功能
更强壮 —— 不容易被粗心的程序员破坏
可移植 —— 能够在多样的环境下运行
更简单 —— 容易理解、容易维护
与之相反,一个“不好的”软件,会发出以下“臭味”:
僵硬 —— 不易改变
脆弱 —— 只想改 A,结果 B 被意外破坏
不可移植 —— 不能适应环境的变化
导致误用的陷阱 —— 做错的事比做正确的事更容易,引诱程序员破坏原有的设计
晦涩 —— 代码难以理解
过度设计、copy-paste 代码
“不好的”设计,有以下几种特性
僵化性(Rigidity)
很难对系统进行改动,因为每个改动都会迫使许多对系统其他部分的改动过。如果单一的改动会导致依赖关系的模块中的连锁改动,那么设计就是僵化的,必须要改动的模块越多,设计越僵化。
脆弱性
对系统的改动会导致系统中和改动的地方无关的很多地方出现问题。出现新问题的地方与改动的地方没有概念上的关联。要修正这些问题又会引出更多的问题,从而使开发团队就像是一只不停追逐自己尾巴的狗一样。
牢固性
很难解开系统的纠结,使之成为一些可在其他系统中重用的组件。设计中包含了对其他系统有用的部分,而把这些部分从系统中分离出来所需要的努力和风险是巨大的。
粘滞性
做正确的事情比做错误的事情要困难。面临一个改动的时候,开发人员常常会发现会有多种改动方法。有的方法会保持系统原来的设计,而另外一些则会破坏设计,当那些可以保持系统设计的方法比那些破坏设计的方法更难应用时,就表明设计具有很高的粘滞性,作错误的事情就很容易。
不必要的复杂性
设计中包含有不具任何直接好处的基础结构。如果设计中包含有当前没有用的组成部分,它就含有不必要的复杂性。当开发人员预测需求的变化,并在软件中放置了处理那些潜在变化的代码时,常常会出现这种情况。
不必要的重复
设计中包含有重复的结构,而该重复的结构本可以使用单一的抽象进行统一。当 copy,cut,paste 编程的时候,这种情况就会发生。
晦涩性
代码可以用清晰、富有表现力的方式编写,也可以用晦涩、费解的方式编写。一般说来,随着时间的退役,代码会变得越来越晦涩。
2.2 单一职责原则(Single Responsibility Principle,SRP)
定义:应该有且仅有一个原因引起类的变更。
单一职责原则的优点:
类的复杂性降低,实现什么职责都有清晰明确的定义
可读性提高,复杂性降低
可维护性提高
变更引起的风险降低。变更是必不可少的,如果类的单一职责做得好,一个接口修改只对相应的实现类有影响,对其他的接口无影响,这对系统的扩展性、维护性都有非常大的帮助。
2.3 里氏替换原则(Liskov Substitution Principle)
在面向对象的语言中,继承是必不可少的、非常优秀的语言机制,它有如下优点:
代码共享,减少创建类的工作量,每个子类都拥有父类的方法和属性
提高代码的重用性
子类可以形似父类,但又异于父类
提高代码的可扩展性,实现父类的方法就可以为所欲为了,很多开源框架的扩展接口都是通过继承父类来完成的
提高产品或项目的开放性。
自然界的所有事物都是优点和缺点并存的,继承的缺点如下:
继承是侵入性的,只要继承,就必须拥有父类的所有属性和方法
降低代码的灵活性。子类必须拥有父类的所有属性和方法,使子类多了些约束
增强了耦合性。当父类的常量、变量和方法被修改时,需要考虑子类的修改,而且在缺乏规范的环境下,这种修改可能带来非常糟糕的结果——大段的代码需要重构。
里氏替换原则的两种定义:
如果对于每一个类型为 S 的对象 o1,都有类型为 T 的对象 o2,使得以 T 定义的所有程序 P 在所有的对象 o1 都代换成 o2 时,程序 P 的行为没有发生变化,那么类型 S 就是类型 T 的子类型。
所有引用基类的地方必须能透明地使用其子类的对象。
里氏替换原则为良好的继承定义了一个规范,包含以下四层含义:
子类必须完全父类的方法
子类可以有自己的个性
覆盖或实现父类的方法时输入参数可以被放大
复写或实现父类的方法时输出结果可以被缩小
2.4 依赖倒置原则(Dependence Inversion Principle,DIP)
依赖倒置的三层含义:
高层模块不应该依赖低层模块,两者都应该依赖其抽象
抽象不应该依赖细节
细节应该依赖抽象
每一个逻辑的实现都是有原子逻辑组成个的,不可分割的原子逻辑就是低层模块,原子逻辑的再组装就是高层模块。抽象是指接口和抽象类,两者都是不能给直接被实例化的;细节就是实现类,实现接口或继承抽象类而产生的类就是细节,其特点就是可以直接被实例化。
高层模块定义接口,低层模块实现接口。
更加精简的定义就是“面向接口编程”——OOD(Object-Oriented Design,面向对象设计的精髓之一)
采用依赖倒置原则的优点:可以减少类间的耦合性,提高系统的稳定性,降低并行开发引起的风险,提高代码的可读性和可维护性。
依赖倒置原则的本质是通过抽象(接口或抽象类)使各个类或模块的实现彼此独立,不互相影响,实现模块间的松耦合。
我们怎么在项目中使用这个规则呢?只要遵循以下的几个规则就可以:
每个类尽量都有接口和抽象类,或者接口和抽象类两者都具备
变量的表面类型尽量是接口或者是抽象类
任何类都不应该从具体类派生
尽量不要覆写基类的方法
结合里氏替换原则使用
注意:我们在实际的项目中使用依赖倒置原则需要审时度势,不要抓住一个原则不放,每一个原则的优点都是有限度的,并不是放之四海而皆准的真理,所以别为了遵循一个原则而放弃了一个项目的终极目标:投产上线和盈利。作为一个项目经理或者架构师,应该懂得技术只是实现目的的工具,设计做得再漂亮,项目做得再符合标准,一旦项目亏本,产品投入大于产出,那整体就是扯淡!
2.5 接口隔离原则
接口——实例接口,类接口,抽象类
隔离:客户端不应依赖于它不需要的接口,类间的依赖关系应该建立在最小的接口上。
通俗定义:建立单一接口,不要建立臃肿庞大的接口。接口尽量细化,同时接口中的方法尽量少。
接口隔离原则是对接口进行规范约束,其包含以下四层含义:
接口要尽量小
拆分接口原则:根据接口隔离原则拆分接口时,首先必须满足单一职责原则
接口要高内聚
在接口中尽量少公布 public 方法,接口是对外的承诺,承诺越少对系统的开发越有利,变更的风险也就越小,同时有利于降低成本。
定制服务
一个系统或系统内的模块必然会有耦合,有耦合就要有相互访问的接口,我们设计时就需要为各个访问者定制服务。定制服务就是单独为一个个体提供优良服务。我们在做系统设计时也需要考虑对系统之间和模块之间的接口采用定制服务。采用定制服务就必然有一个要求:只提供给访问者需要的方法。
接口设计是有限度的
接口设计的粒度越小,系统越灵活,这是不争的事实。但是,灵活的同时也带来了结构的复杂性,开发难度增加,可维护性降低。所以,接口设计一定要注意适度。这个“度”如何来判断呢?根据经验和常识判断,没有一个固定或可测量的标准。
2.6 迪米特法则(Low of Demeter,LoD)
迪米特法则 = 最小知识原则
一个对象应该对其他对象有最少的理解。
迪米特法则对类的低耦合提出了明确的要求,其包含以下四层含义:
只和朋友交流
只与最直接的朋友交流
朋友间也是有距离的
是自己的就是自己的
如果一个方法放在本类中,既不增加类间关系,对本类不产生负面影响,那就放置在本类中。
谨慎使用 Serializable
最佳实践
迪米特法则的核心观念就是类间解耦,弱耦合,只有弱耦合了以后,类的复用率才可以提高。其要求的结果就是产生了大量的中转和跳转类,导致系统的复杂性提高,同时也为维护带来了难度,所以,在采用迪米特法则时需要反复权衡,既做到让结构清晰,又做到高内聚低耦合。
迪米特法则需要类间解耦,但解耦是有限度的。在实际的项目中,需要适度地考虑这个原则,别为了套用原则而做项目。原则只是参考,如果违背了这个原则,项目也未必会失败。这就需要大家在采用原则时反复度量,不遵循是不对的,严格执行就是“过犹不及”。
2.7 开闭原则
一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。
一个软件实体应该通过扩展来实现变化,而不是通过修改已有的代码来实现代码。
软件实体包括以下几个部分:
项目或软件产品中按照一定的逻辑规则划分的模块
抽象和类
方法
变化有以下三种类型:
逻辑变化
子模块变化
可见试图变化
开闭原则是最基础的一个原则,前几个原则都是开闭原则的具体形态,也就是说前几个原则就是指导设计的工具和方法,而开闭原则才是其精神领袖。
版权声明: 本文为 InfoQ 作者【王海】的原创文章。
原文链接:【http://xie.infoq.cn/article/b20e5cb370f405fe6eae62b67】。未经作者许可,禁止转载。
评论