《代码整洁之道》:细节之中自有天地,整洁成就卓越代码
第6章 对象和数据结构
数据结构指的就是数据的载体,暴露数据,而几乎没有有意义的行为的贫血类。最常见的应用在分布式服务,以wcf,webservice,reset之类的分布式服务中不可或缺的数据传输对象(DTO)模式,DTO(Request/Response)就是一个很典型的数据载体,只存在简单的get,set属性,并且更倾向于作为值对象存在。而对象则刚好相反作为面向对象的产物,必须封装隐藏数据,而暴露出行为接口,DDD中领域模型倾向于对象不仅在数据更多暴露行为操作自己或者关联状态。
数据结构和对象之间看是细微的差别却导致了不同的本质区别:使用数据结构的代码便于在不改动现在数据结构的前提下添加新的行为(函数),面向对象代码则便于不改动现 有函数的前提下添加新的类。换句话说就是数据结构难以添加新的的数据类型,因为需要改动所有函数,面向对象的代码则难以添加新的函数,因为需要修改所有的类。在任何一个复杂的系统都会同时存在数据结构和对象,我们需要判断的是我们需要的是需要添加的新的数据类型还是新的行为函数。
隐藏作为面向对象主要特性中的最重要特性,封装隐藏是面向对象中最重要的特性,一个好的面向对象代码肯定是对对象的内部细节做到很好的隐藏封装,封装过后才有是多态,委派之类的。一个好的面向对象的代码一定是具有很好的隐藏封装,易于测试,不稳定因素往往集中在一处很小或者固定的位置,不稳定因素的变更不会导致更大面积的修改扩散。
德墨忒尔律:模块不应该了解它所操作对象内部情形。比如C的方法f只能调用以下对象的方法。
· C
· 由f创建的对象
· 作为参数传递给f的对象
· C的实体变量持有的变量
对象的隐藏要求:方法不应和任何调用方法返回的对象操作,换句话之和朋友说话,不和陌生人说话(迪米特法则,或被译为最小知识原则),比如:ctxt.getOptions().getSearchDir().getAbsolutePath(),就是迪米特法则的反例模式。
过程式代码(函数编程)便于在不改动既有数据结构的前提下添加新函数[增加新功能],面向对象代码便于在不改动既有函数的前提下添加新类[增加新需求、新数据结构]。反过来讲也说的通,过程式代码难以添加新的数据结构,因为必须修改所有函数,面向对象代码难以添加新函数,因为必须修改所有类。所以在设计的时候要分析好是以后是要添加新函数还是要添加新的数据结构。
第7章 异常处理
每个软件系统都避不开异常处理,需要防止它搞乱我们的逻辑。
1. 利用异常处理代替返回异常编码,返回异常编码会是的代码中充满了if/else,switch/case扰乱我的代码流转。对于特定异常扑捉,可以面向异常编程,编写特定的异常类,使得对异常封装转化,更容易捕善后获处理。对错误分类的方式有多种,可以依据来源,是组件还是其他地方,或者依据类型,是设备错误还是网络错误。不过在我们定义异常类的时候,最重要的考虑是如何捕获它们。
2. 避免返回null,在软件系统中最常见头疼的就是NullReferenceException。在非特定场景下,我们应该极力的避免返回null。面对这种场景我们可以采用null object Pattern(空对象模式)返回特例对象,如c#类库中的Guid.Empty,string.Empty;对于集合类型我们可以返回长度0的空集合而非null。返回Null,作者认为这种代码很糟糕。建议抛出异常 或者返回特定对象(默认值)。更早的发现问题。同理,也应该避免传递Null值给其他的方法。
3. try代码就像是事务,catch代码块将程序维持在一种持续状态。在编写可能抛出异常的代码时,最好先写出try-catch-finally 语句。
第8章 边界
有时候我们在使用第三方程序包或者开源代码的时候,或者依靠公司其他团队的代码,我们都得干净利落的的整合进自己的代码中。这一章就是介绍保持保持软件边界整洁的实践手段和技巧。
1.对第三方进行学习性测试,当第三方程序包发布了新的版本,我们可以允许学习性测试,看看程序包的行为有没有发生改变。
2.使用尚不存在的代码,有时候我们的第三方,还没有开发好API,但又不能影响到我们的开发进度,所以我们先可以定义好自己想要的接口。如果第三方ok了,我们再对接起来。
3.通过接口管理第三方边界,可以使用Adapter模式将我的接口转换为第三方提供的接口。这个是要注意,第三方的代码和自己的代码混合太多,这样不便管理。
在系统开发中不可能一切都得从零开始,自己写所有的代码,更好的方案是需要整合一些开源或者第三方的项目,为我所用。但是不能让这些非自己的代码渗侵中我们的代码各处,有一些所以功能很强大的第三方产品,但不一定具有很好的抽象。很多时候我更宁愿花些时间抽象出我们自己所需要的接口在第三方类库上外覆一层自己的抽象,这样不仅便于TDD,因为我们能够很好的创建伪对象,使的测试独立不依赖外部资源,得到快速反馈;而且在设计上得到很好的扩展,当由于某些原因如第三方类库不再能满足业务需求,或者权益收费等等,我们可以很好的切换底层而使得修改不会扩散到系统各处。外覆类也是处理遗留代码带入测试容器的一种很好实践。
第9章 单元测试
敏捷和TDD[测试驱动开发(Test-Driven Development)]运动鼓舞了许多程序员编写自动化单元测试,单元测试是确保代码中的每个犄角旮旯都如我们所愿的工作。
TDD三定律
1. 除非这能让失败的单元测试通过,否则不允许去编写任何的生产代码。
2. 只允许编写刚好能够导致失败的单元测试。 (编译失败也属于一种失败)
3. 只允许编写刚好能够导致一个失败的单元测试通过的产品代码。
TDD中测试代码在往往和产品代码差不多,在系统中占据一半的代码量,不好的测试代码也可能拖累项目的开发。整洁的测试代码应该是遵循first原则的,测试还应遵守以下5条原则:
1. 快速(Fast):测试应该快速,因为需要不断的运行测试得到反馈,我们需要的快速反馈,错误的快速定位。所以你的测试就不能依赖太多的外部资源,数据库,硬件环境等等,对于这些外部资源应该采用伪对象模式来隔离。
2. 独立(Independent):测试应该是独立的,独立于测试用例之间,独立于特定的环境,独立于测试的运行顺利。数据的独立通常采用两种独立方式,每个测试环境的独立,很多时候我们希望每个测试运行完成后环境(如数据库)和运行前保持一致,如数据库高层次测试我们更希望在每次测试完成后不会带来多余或者改变数据。再则就是数据的隔离,我们的行为测试(BDD,集成高角度的测试)都会依赖一些固定的信息,通常是登陆系统的人员,我们可以采用么个测试建立一个不同的登陆人员来使的每个测试之间的s数据隔离。
3. 可重复(Repeatable):测试应该可以在任何环境下可重复,可运行,因为测试独立于环*部资源。
4. 自足验证(Self-Validation):测试应该有通过失败的标示,从每一个测试上能得到一处代码逻辑的通过失败。每个测试都有对同一件事物的一种行为的断言,也之断言一件事,从而能够很好的错误定位,避免高技巧性的测试。
5. 及时(Timely):测试应该是及时编写的,TDD要求测试必须在实现代码之前,提前以使用者的角度定义使用接口方式。如果你是在编码后补测试,你的测试覆盖很可能不够,而且容易定式于实现的逻辑写测试,很多时候对于较低层次的测试也不是那么容易写的。一个设计良好的代码必须也是可测试的。
测试代码和生产代码一样重要,它可不是二等公民,它需要被思考、被设计和北照料。它该像生产代码一样保持整洁。单元测试让你的代码可扩展,可维护,可复用。原因很简单,有了测试,你就不担心对代码的修改,没有单元测试,每次修改可能带来缺陷,一个测试,一个断言。一个测试,对应一个概念。我们不想要超长的测试函数。
第10章 类
面向对象的相似行为的抽象,函数代码块的组织形式,在面向对象中我们的软件系统是由众多的类和类之间的交互协作完成了。面向对象特征:封装,继承,多态度,委派。一个设计良好的类该是具有良好的封装,站在使用者的调度考虑那些是使用接口,那些是内部细节;这是面向对象最主要的特征,但是有时会与测试冲突,可以适当的放开并仅限于于测试调用。继承和多态在面向对象中可以实现重用,但我更倾向于继承不是为了重用,而是隔离变化;大量的滥用继承不干净的继承体系将会导致庞大的继承体系,继承体系中众多职责重复在各个同级派生类,理想的继承应该是满足里氏替换原则(LSP:每个父类出现的地方都应该可以被派生类所替换,并且能正确的工作);面oo第二原则组合优先。而委派则是一个类把部分功能委派给其他类来完成,体现类之间的协作,类似组合。
类--小结:
1. 类第一原则应是是小并足够的小。但与函数不同的是函数以代码行数统计,而类以权责统计。
2. 单一原则(SRP),体现了类只应该做一件事,并且做好它,这样变化修改的理由只有他所做的事。良好的软件设计中系统是由一组大量的短小的类和他们之间功能协作完成的,而不是几个上帝类。
3. 内聚:高内聚低耦合:提出与结构化编程,内聚表述模块内部功能不同操作逻辑之间的距离,如果一个类的每个变量都被每个方法所使用为最大的内聚;耦合描述模块之间的依赖程度;高内聚低耦合以简单的方式表述就是功能完备(高内聚)对象之间是通过稳定的接口(低耦合)交互的。
4. 依赖倒置(DIP):描述组件之间高层组件不应该依赖于底层组件。依赖倒置是指实现和接口倒置,采用自顶向下的方式关注所需的底层组件接口,而不是其实现。DI模式很好的就是应用IOC(控制反转)框架,构造方式分为分构造注入,函数注入,属性注入;.net平台流行的IOC框架有Unity,Castle windsor,Ninject,Autofac等框架支持。
5. 为了修改而组织。开放闭合原则(OCP):类应当对扩展开放,对修改封闭。我们可以借助接口和抽象类来隔离这些细节带来的影响。
第11章 系统
1 将系统的构造和使用分开:构造和使用是不一样的过程。
就好比:修建一栋大楼的时候,起重机和升降机在外面,工人们穿着安全服在忙碌。当大楼建设完成,建筑物变得整洁,覆盖着玻璃幕墙和漂亮的漆色。在其中工作的人,看完完全不同的景象。软件也是如此,将关注的方面分离。
public Service getService() {
if (service ==null) service =new MyServiceImpl(...);// Good enough default for most cases? return service;
}
这就是所谓延迟初始化/赋值。
好处:在真正使用到对象之前,无需操心这种架空构造,起始时间更短,而且永远不会返回null值。
坏处:我们也得到了MyServiceImpl及其构造器所需一切的硬编码依赖,不分解这些依赖关系就无法编译,即便在运行时永不使用这种类型的对象!
2 解决方法
1.工厂,有时候应用程序需要确定何时创建对象,我们可以使用抽象工厂模式。将构造的细节隔离于应用程序之外。
2.依赖注入(DI/IOC)。在依赖管理情景中,对象不应该负责实例化对自身的依赖,反之,它应该将这份权责移交给其他有权利的机制,从而实现控制的反转。
3.扩容:“一开始就做对的系统”纯属神话,反之,我们应该只实现今天的用户的需求。然后重构,明天再扩容系统,实现新用户的需求。这就是迭代和增量敏捷的精髓所在。 就像城市不断的再拆掉,再建设。
4.面向切面编程。AOP中,被称为方面(aspect)的模块构造指明了系统中哪些点的行为会以某种一致的方式被修改,从而支持某种特定的场景。这种说明是用某种简洁的声明(Attribute)或编程机制来实现的。