《代码整洁之道》总结与演绎(下)

程序员可以分为三个层次:普通程序员、工程师 、架构师

单元测试

1、TDD三定律

定律一、在编写不能通过的单元测试前,不可编写生产代码。

定律二、只可编写刚好无法通过的单元测试,不能编译也算不过。

定律三、只可编写刚好足以通过当前失败测试的生产代码。

(1)测试与生产代码一起编写

2、保持测试整洁

(1)测试必须随生产代码的演进而修改,测试越脏就越难修改

(2)失去了测试,每次修改生产代码都有可能带来缺陷

3、整洁的测试

(1)整洁测试有三个要素:可读性、可读性和可读性

(2)在测试中,要尽可能少的文字表达大量的内容

(3)每个测试都清晰的拆分为三个环节:构造-操作-检验

第一环节构造测试数据;第二环节操作测试数据;第三环节检验操作是否得到期望的结果

(4)测试保证和增强了生产代码的可扩展性、可维护性和可复用性

4、每个测试一个断言

(1)可以利用模版方法模式消除重复代码

(2)单个测试中的断言数量应该最小化

5、F.I.R.S.T.

(1)快速(Fast) :测试应该够快。测试应该能快速运行。测试运行缓慢,你就不回想要频繁地运行它。如果你不频繁运行测试,就不能尽早发现问题,也无法轻易修正,从而也不能轻而易举地清理代码。最终,代码就会腐败。

(2)独立(Independent): 测试应该相互独立。某个测试不应该为下一个测试设定条件。你应该可以单独运行每个测试,以及任何顺序运行测试。当测试互相依赖时,头一个没通过就会导致一连串的测试失败,使问题诊断变得困难,隐藏了下级错粗。

(3)可重复(Repeatable) :测试应当可在任何环境中重复通过。你应该能够生产环境、质检环境中运行测试,也能够在无网络的列车上用笔记本电脑运行测试。如果测试不能在任何环境中重复,你就总会有个解释其失败的接口。当环境条件不具备时,你也会无法运行测试。

(4)自足验证(Self-Validating): 测试应该有布尔值输出。无论是通过或失败,你不应该查看日志文件来确认测试是否通过。你不应该手工对比两个不同文本文件来确认测试是否通过。如果测试不能自足验证,对失败的判断就会变得依赖主观,而运行测试也需要更长的手工操作时间。

(5)及时(Timely) :测试应及时编写。单元测试应该恰好在使其通过的生成代码之前编写。如果在编写生成代码之后编写测试,你会发现生成代码难以测试。你可能会认为某些生产代码本身难以测试。你可能不会去设计可测试的代码。

1、类的组织

(1)类应该从一组变量列表开始:公共静态常量---> 私有静态变量---> 私有实体变量---> 公共变量--->公共函数--->私有工具函数

(2)为了便于测试访问,有时会用到受保护(protected)变量或工具函数,除此之外的大部分情况保持变量和工具函数的私有性。

2、类应该短小

(1)类的名称应当描述其权责,类名越含糊,该类越有可能拥有过多的权责

(2)单一职责原则,类或模块有且只有一条加以修改的理由

(3)系统应该由许多短小的类而不是少量巨大的类组成

(4)类应该只有少量实体变量,方法操作的变量越多,就越粘聚到类上

(5)如果一个类中的每个变量都被每个方法所使用,则该类具有最大的内聚性

(6)创建极大化内聚类是既不可取也不可能的,保持函数和参数列表短小的策略

3、为了修改而组织

(1)具体类包含实现细节,抽象类只呈现概念

(2)依赖倒置原则:类应该依赖于抽象而不是依赖于具体细节

系统

1、如何建造一个城市(系统层级上保持整洁)

2、将系统的构造与使用分开

(1)软件系统应该将起始过程和起始之后的运行时逻辑分离开,在起始过程中构建对象,也会存在相互缠结的依赖关系

(2)延迟初始化/赋值:真正用到对象之前,无需操心这种构造,启动时间也会更短

(3)分解main:将构造与使用分开,将全部构造过程搬迁至main或称之为main模块中,设计系统的其余部分时,假设所有对象都已经正确构造和设置。

(4)可以使用抽象工厂模式让应用自行控制何时创建对象,但构造的细节隔离于应用程序代码之外。

(5)依赖注入(DI)可以实现分离构造和使用,控制反转(IOC)是依赖管理中的一种手段。

(6)面向切面编程(AOP)是一种恢复横贯式关注面模块化的普适手段,常被用来处理持久化、安全和事务

3、JAVA代理适用于简单的情况,例如在单独的对象或类中包装方法调用。

(1)JDK提供的动态代理仅能与接口协同工作;

(2)对于代理类,得使用字节码操作库,比如CGLIB、ASM或Javassist

4、纯Java AOP 框架

(1)在数个Java框架中,代理都是内嵌的,如Spring AOP 和 JBoss AOP等,从而能够以纯Java代码(表示不使用AspectJ)实现面向编程。

(2)使用描述性配置文件或API,你把需要的应用程序架构组合起来,包括持久化、事务、安全、缓存、恢复等横贯性问题。

(3)annotation中有些或者全部持久化信息可以转移到XML部署描述中,只留下真正的纯POJO。

5、AspectJ的方面

(1)在80%~90%用到方面特性的情况下,SpringAOP 和JBoos AOP 提供的纯Java实现手段足够使用

(2)AspectJ的弱势在于,需要采用几种新工具,学习新语言构造和使用方式

6、测试驱动系统架构

(1)在代码层面与架构关注面分离开,就有可能真正用测试来驱动架构

(2)最佳的系统架构由模块化的关注面领域组成,每个关注面均用纯Java(或其他语言)对象实现

(3)不同的领域之间用最不具有侵害性的方面或类方面工具整合起来,这种架构能测试驱动

7、优化决策

(1)模块化和关注面切分成就了分散化管理和决策

(2)在巨大的系统中,最好是授权给最有资格的人

(3)延迟决策至最后一刻是一个好手段,让我们能够基于最有可能的信息作出选择

8、明智使用添加了可论证价值的标准

9、系统需要领域特定语言

(1)领域特定语言(Domain-Specific Language, DSL),是一种单独的小型脚本语言或以标准语言写就的API

(2)DSL在有效使用时能提升代码惯用性和设计模式之上的抽象层次

(3)DSL允许所有抽象层级和应用程序中的所有领域,从高级策略到底层细节,使用POJO来表达

迭进

1、通过迭进设计达到整洁目的

(1)Kent Beck 关于简单设计的规则:

  • 运行所有测试
  • 不可重复
  • 表达了程序员的意图
  • 尽可能减少类和方法的数量

以上规则按其重要程度排列

2、简单设计规则(运行所有测试)

(1)不可验证的系统,绝不应该部署

(2)只要系统可测试,就会导向保持类短小且目的单一的设计方案

(3)编写测试越多,就越会遵循DIP之类的规则,使用依赖注入、接口和抽象等工具尽可能减少耦合

3、简单设计规则(重构)

(1)测试消除了对清理代码就会破坏代码的恐惧

(2)优秀软件设计的体现:提升内聚性、降低耦合度、切分关注面、模块化系统性关注面、缩短函数和类的尺寸、选用更好的名称

4、不可重复

(1)重复意味着额外的工作和额外的风险和额外且不必要的复杂度

(2)重复有多种表现,极其雷同的代码或者实现上的重复等形态

int size() {} / boolean isEmpty() {} 这两个方法可以分别实现,为了减少重复,可以在isEmpty中使用size方法

(3)做共性抽取,小规模复用可大量降低系统复杂性

(4)要想实现大规模复用,必须理解如何实现小规模复用

(5)模版方法模式是一种移除高层级重复的通用技巧

abstract public class VacationPolicy{
    public void accrueVacation(){
        calculateBaseVacationHours();
        alterForLegalMinimums();
        applyToPayroll();
    }
    
    private void calculateBaseVacationHours(){ //implement logic... }
    abstract protected void alterForLegalMinimums();
    private void applyToPayroll(){ //implement logic... }
}

// 下面子类按照不同各自不同场景填充accrueVacation算法中的“空洞”(abstract)
// 重复信息直接继承,仅提供不重复信息
public class USVacationPolicy extends VacationPolicy{
    @Override
    protected void alterForLegalMinimums(){
        // US Specific logic
    }
}

public class EUVacationPolicy extends VacationPolicy{
    @Override
    protected void alterForLegalMinimums(){
        // EU Specific logic
    }
}

5、表达力

(1)软件项目的主要成本在于长期维护

(2)通过采用标准命名法来表达

(3)编写良好的单元测试,测试的主要目的之一就是通过实例起到文档的作用

6、尽可能少的类和方法

(1)不能为了保持类和函数短小,造出太多细小的类和方法,也要注意函数和类的数量

(2)在保持函数和类短小的同时,保持整个系统短小精悍

(3)尽管使类和函数的数量尽量少是很重要,但更重要的是测试、消除重复和表达力

并发编程

1、为什么要并发

(1)并发是一种解耦策略,有利于把做什么(目的)和何时(时机)做分解开

(2)解耦目的与时机能明显地改进应用程序的吞吐量和结构

(3)若系统对响应时间和吞吐量有要求,需要并发解决

(4)单线程在Web套接字I/O上面消耗较大,通过同时访问多个站点的多线程算法能改进性能

(5)常见的迷思与误解

  • 并发总能改进性能 (错误)
  • 编写并发程序无需修改设计 (错误)
  • 在采用web或EJB容器的时候,理解并发问题并不重要 (错误)

2、挑战

(1)由于多个线程在执行Java代码时有许多可能路径可行,有些路径会产生错误的结果

3、并发防御性原则

(1)单一职责原则:分离并发相关代码与其他生产代码

(2)限制数据作用域:严格限制对可能被共享的数据访问

问题:两个线程修改共享对象的同一字段时,导致未预期的行为

解决方案:采用同步机制,例如Synchronized关键字等锁机制

(3)使用数据复本:例如ThreadLocal实现机制

(4)线程应尽可能地独立:例如HttpServlet的子类接收所有以参数形式传递给doGet和doPost方法的信息

4、了解Java库

(1)线程安全群集:java.util.concurrent包中的群集对于多线程解决方案是安全的

掌握java.util.concurrent、java.util.concurrent.atomic 、java.util.concurrent.locks

(2)使用Excutor框架执行无关任务

(3)尽可能使用非锁定的解决方案

(4)有几个类并不是线程安全的

5、了解执行模型

(1)并发应用中的基础定义:限定资源、互斥、线程饥饿、死锁、活锁

(2)生产者-消费者模型

  • 生产者创建某些工作,放置于缓存队列中
  • 消费者从队列中获取并完成工作
  • 缓存队列是一种限定资源

(3)读者-作者模型

  • 当一个主要为读者线程提供信息源,只偶尔被作者线程更新的共享资源,吞吐量是个问题
  • 增加吞吐量会导致线程饥饿和过时信息积累,更新会影响吞吐量
  • 协调读者线程,不去读作者线程正在更新的信息

(4)宴席哲学家:容易出现死锁、活锁、吞吐量和效率降低等问题

6、警惕同步方法之间的依赖:避免使用同一个共享对象的多个方法

7、保持同步区域微小:尽可能减小同步区域,降低锁带来的延迟和额外开销

8、很难编写正确的关闭代码:存在的问题与死锁有关,线程会等待永远不会到来的信号

9、测试线程代码

(1)将伪失败看作可能的线程问题:偶发事件被忽略的越久,代码就越可能搭建在不完善的基础上

(2)先使非线程代码可工作:不要同时追踪非线程缺陷和线程缺陷

(3)编写可插拔的线程代码,这样就能在不同的配置环境下运行

(4)编写可调整的线程代码:允许线程依据吞吐量和系统使用率自我调整

(5)运行多于处理器数量的线程,促使任务交换的发生,任务交换越频繁,越有可能找到错过临界区或导致死锁的代码

(6)在不同平台上运行:在不同环境中多线程代码的行为也不一样,应该在所有可能部署的环境中运行测试

(7)调整代码并强迫错误发生

  • 手工向代码中插入wait、sleep、yield和priority的调用,改变代码执行顺序,测试缺陷代码
  • 不要在产品环境中留下这类代码,将拖慢代码执行速度

(8)自动化:可以使用Aspect-Oriented Framework 、 CGLIB或ASM之类的工具

------------------------------------------------------------------------- end ------------------------------------------------------------------------------- 

《代码整洁之道》总结与演绎(下)_第1张图片 欢迎关注微信公众号“JAVA万维猿圈”,记录程序员修炼之路,共同学习和成长!

 

 

 

 

 

 

你可能感兴趣的:(读书笔记)