Code Clean读书笔记

代码整洁之道读书笔记

  • by fangpcjava

    序言部分

  • "神在细节之中" — 建筑师路德维希
  • 5S哲学(精益)
  • 整理(Seiri):搞清楚事物之所在——经过恰当地命名之类的手段——相当重要
  • 整顿(Seiton):每段代码都应该在你但愿它所在的地方——若是不在那里,就须要重构了
  • 清楚(Seiso):或谓清洁,清理工做地拉线、油污和边角废料
  • 清洁(Seiketsu):或谓标准化,开发组内使用统一的代码风格和实践手段
  • 身美(Shisuke):或谓纪律(自律),在实践中贯彻规程,并时时体现于我的工做上,并且要乐于改进程序员

    代码猴子和童子军军规

  • 沉迷测试(Test Obsessed)
  • 不要作代码猴子(Do not be a code monkey)编程

    第一章——整洁代码

    一、代码永不消失
    代码就是衔接人脑理解需求的含糊性和机器指令的精确性的桥梁。哪怕将来会有对如今高级编程语言的再一次抽象——但这个抽象规范自身仍旧是代码。因此既然代码会一直存在下去,且本身都干了程序员这一行了,就好好的对待它吧。json

二、读远比写多
当你录下你平时的编码的过程,回放时,你会发现读代码所花的时间远比写的多,甚至超过 10:1。因此整洁的代码会增长可读性。网络

三、稍后等于永不
糟糕的代码会致使项目的难以维护。而当你正在写糟糕的代码的时候,内心却圣洁的想:“有朝一日再回来整理”。但现实是残酷的,正如勒布朗(LeBlanc)法则:稍后等于永不(Later equals never)数据结构

四、精益求精
写代码就跟写文章同样,先自由发挥,再细节打磨。追求完美。架构

第二章——有意义的命名

1.名副其实而且也命名有意义。 提及来很简单。选个好名字须要花时间,但省下的时间比花掉的多。注意命名,一旦有好的命名,就换掉旧的。app

int d;// 消失的时间,以日计。
int elapsedTimeInDays;框架

2.避免误导。好比不是List类型,就不要用个accountList来命名,这样造成误导。编程语言

3.作有意的区分。

Public static void copyChars(char a1[],char a2[]){
    for(int i=0;i<a1.length;i++){
        a2[i]=a1[i];
    } 
}

若是参数名称改成source和destination ,这个函数就会像样不少。废话都是冗余的,Variable一词 永远不该当出如今变量名中。Table一词永远不该当出如今表名中。NameString 会比 Name好吗,难道Name 会是一个浮点数不成?若有一个Customer的类,有又一个CustomerObject的类。是否是就凌乱了。

4.使用便于搜索的的名称
单个字母或者数字常量是很难在一大堆文章中找出来。好比字母e,它是英文中最经常使用的字母。长名胜于短名称,搜获得的名称胜于自编的名称。 窃觉得单字母的名称仅用于短方法中的本地变量。名称长短应与其做用域大小相对应。

5.类名应该是名词或短语,像Customer,Account,避免使用Manager,Processor,Data或者Info这样的类名。类名不该当是动词。方法名应该是动词或动词短语,如postPayment ,deletePage或Save,属性访问、修改和断言应该根据其值来命名,并加上get,set,is这些前缀。

6.别扮可爱,耍宝,好比谁知道HolyHandGrenada 函数是干什么的,没错这个名字挺伶俐,可是不过DeleteItems或许是更好的名字。

7.每一个概念对应一个词。而且一以贯之。
在一堆代码中有Controller,又有manager,driver。就会使人困惑。好比DeviceManager和Protal-Controller之间又什么本质区别?

第三章——函数

1.短小:函数要短小,更短小。极长的函数逻辑是不清晰的,读很长的函数每每会被不少琐碎的细节分神,好的函数应该是模块化的。
2.只作一件事:一个函数应该尽可能只作并作好一件事。
3.向下规则:代码的阅读顺序应该是自顶向下的,要让每一个函数后面都跟着位于下一抽象层级的函数。Logic -> Service -> Dao。
4.函数命名:
    - 函数越短、功能越集中,就越便于取个好名字。
    - 别惧怕长名称。长而具备描述性的名称,要比短而使人费解的名称好。长而具备描述性的名称,要比描述性的长注释要好。
    - 命名方式要保持一致。
5.函数参数:零参数函数 > 单参数函数 > 双参数参数 > 三参数参数(避免),参数越少越好
    - 测试友好
    - 函数封闭性更好
    - 标识参数丑陋不堪,骇人听闻
    - 无反作用:若是必定要时序性耦合,就该在函数名称中说明。
    - 分隔指令与询问:一个函数作一件事,表明一个动做,不易引发歧义。
    - 抛异常代替返回错误码:把try-catch代码块抽出来造成检测函数
    - DRY:杜绝重复代码,凡是须要用至少两次的代码,给它单独写成一个类或函数
6.结构化编程:
    - Dijkstra结构化编程规则:每一个函数、函数中的每一个代码块都应该有一个入口、一个出口。每一个函数只能有一个return语句,循环中不能有break或continue语句,永远不能由goto

第四章——注释

1.注释不能美化糟糕的代码:与其花时间编写解释你写出的糟糕代码的注释,不如花时间整理你那糟糕的代码。
2.用代码来阐述:用代码来解释意图而不是注释
3.好注释:
    - 对意图的解释:某个决定后面的意图。
    - 阐释:翻译参数或返回值,阐释性注释自己就有不正确的风险。当改变了参数或者返回值的时候记得更新阐释性注释。
    - 警示:不要那么作,或者怎么作会有怎样的风险和结果
    - TODO注释: 应该作,可是还没作。不要让TODO成为在系统中留下糟糕代码的借口。
4.坏注释:
    - 喃喃自语
    - 冗余注释
    - 误导性注释
    - 循规式注释

第五章——格式

1.概念间垂直方向上的间隔:代码中的空白行会增长代码的可读性。在封包声明、导入声明、每一个函数之间都要用空白行隔开。
@Service("userService")
    public class UserServiceImpl implements IUserService {
        @Resource
        private UserDao userDao;
        public User getUserById(int userId) {
            // TODO Auto-generated method stub
            return userDao.selectById(userId);
        }
        @Override
        public List<User> selectUser(int start, int limit) {
            // TODO Auto-generated method stub
            return userDao.selectUser(start,limit);
        }
        @Override
        public List<User> getUsersListPage(Page page) {
            // TODO Auto-generated method stub
            return userDao.getUsersListPage(page);
        }

    }
@Service("userService")
    public class UserServiceImpl implements IUserService {

        @Resource
        private UserDao userDao;
        public User getUserById(int userId) {
            // TODO Auto-generated method stub
            return userDao.selectById(userId);
        }

        @Override
        public List<User> selectUser(int start, int limit) {
            // TODO Auto-generated method stub
            return userDao.selectUser(start,limit);
        }

        @Override
        public List<User> getUsersListPage(Page page) {
            // TODO Auto-generated method stub
            return userDao.getUsersListPage(page);
        }

    }
2.垂直方向上的靠近:空白行隔开了概念,靠近的代码行则暗示它们之间的紧密联系。紧密相关的代码应该互相靠近。
3.垂直距离:
    - 变量声明:变量声明应该尽量在其使用的位置。
    - 实体变量:在类的顶部声明,在设计良好的类中,实体变量应该被该类的大多数方法所用。
    - 相关函数:调用函数和被调用函数放在一块儿,而且调用函数放在被调用函数上面。
private String distributionAward(ReferralRankRecord referralRankRecord) {
        Map<Integer, String> awardRules = JsonUtils.jsonToMap(awardsConfiguration,
                new TypeReference<Map<Integer, String>>() {
                });

        List<Integer> rankingDemarcation = new ArrayList<>(awardRules.keySet());
        int awardGradeIndex = binarySearchAward(rankingDemarcation, referralRankRecord.getRankingNumber());
        if (awardGradeIndex > awardDemarcation && referralRankRecord.getCount() < 3) {
            return noAward;
        }

        return awardRules.get(awardGradeIndex);
    }

    private int binarySearchAward(List<Integer> rankingDemarcation, int rankingNumber) {
        int left = 0;
        int right = rankingDemarcation.size() - 1;
        while (left <= right) {
            int mid = left + (right - left) / 2;
            if (rankingDemarcation.get(mid) == rankingNumber) {
                return rankingDemarcation.get(mid);
            } else if (rankingDemarcation.get(mid) < rankingNumber) {
                left = mid + 1;
            } else {
                right = mid - 1;
            }
        }

        return rankingDemarcation.get(right);
    }
- 概念相关:概念相关的代码应该放到一块儿。相关性越强,彼此之间的距离就应该越短。
    - 自顶向下贯穿源代码模块的信息流,好比自上向下展现函数调用依赖顺序。
4.横向格式:
    - 水平方向上的区隔与靠近:
        * 空格字符能够把相关性较弱的事物分隔开
        * 赋值操做周围加上空格字符用于强调赋值语句有两个肯定而重要的要素:左边和右边。空格字符增强了分割效果。
left=mid+1;
left = mid + 1;
- 函数名和左圆括号之间不加空格代表函数和其参数紧密相关,括号中的参数要隔开代表参数是相互分离的。
private int binarySearchAward(List<Integer> rankingDemarcation, int rankingNumber)
private int binarySearchAward (List<Integer> rankingDemarcation, int rankingNumber)
- 水平对齐:
public class ReferralRankRecord {

                    private int rankingNumber;          // 排名
                    private int userId;             // 转介绍人userId
                    private int count;              // 转介绍数量
                    private long latestUpdateTime;  //最近一条转介绍记录更新时间
                }
public class ReferralRankRecord {

                    private int  rankingNumber;          // 排名
                    private int  userId;                 // 转介绍人userId
                    private int  count;                  // 转介绍数量
                    private long latestUpdateTime;       //最近一条转介绍记录更新时间
                }
- 缩进:缩进能够帮助程序员快速跳过与当前关注的情形无关的范围,若是没有缩进,程序将变得没法阅读
            * 类声明不缩进
            * 类中方法相对该类缩进一个层级
            * 方法实现相对于方法声明缩进一个层级
            * 代码块的实现相对于其容器代码块缩进一个层级
            * 扩展和缩进范围,避免范围层级坍塌到一行
        - 团队规则:
            * 同一个团队,同一种代码风格
            * 把规则编写进IDE,一直沿用

第六章——对象和数据结构

1.数据抽象:数据抽象则是指对数据类型的定义和使用过程
    - 以抽象形态表述数据,而不是暴露数据的细节,具象机动车和抽象机动车的例子,若是机动车改为以自然气为燃料,具象机动车接口须要作修改,而抽象机动车类不须要修改
public interface Vehicle{
              double getFuelTankCapacityInGallons();
              double getGallonsofGasoline();
            }
public interface Vehicle{
              double getPecentFuelRemaining();
            }
- 暴露抽象接口可使依赖方无须了解数据具体实现就能操做数据本体。
    - 若是上述的机动车改为以电力为能源,或者变成混动模式,抽象机动车类则也要作相应的修改?
    - 不必定都须要暴露抽象的概念,如pecent,若是就是想单纯的取某个属性,如长方体的长、宽、高,人的身高、体重、生日这些具体的属性
2.数据、对象的反对称性
    - 对象与数据结构之间的差别
        * 对象把数据隐藏于抽象以后,暴露出操做数据的函数
        * 数据结构暴露其具体数据,没有提供有意义的函数。
    - 过程式代码(使用数据结构的代码)便于在不改动既有数据结构的前提下添加新函数,面向对象代码便于在不改动既有函数的前提下添加新类。
    - 过程式代码难以添加新数据结构,由于必须修改全部函数。面向对象代码难以添加新函数,由于必须修改全部类。
3.得墨忒耳定律
    - 对象O的M方法,能够访问/调用以下的:
        * 对象O自己
        * M方法中建立或实例化的任意对象
        * 做为参数传递给M的对象
        * 对象O直接的组件对象
    - 一些比喻
        * 不要和陌生人说话
        * 在超市购完物付款时,是将钱包中的钱取出交给收银员,而不是直接把钱包交个收银员让收银员把钱拿出来
        * 人能够命令一条狗行走(walk),可是不该该直接指挥狗的腿行走,应该由狗去指挥控制它的腿如何行走
    - 为何要遵循这必定律
        * 能够更改一个类,而无需更改许多其它的类
        * 隐藏了某个类是如何工做的
        * 改变调用方法,而无需改变其余的东西
        * 让代码更少的耦合,主叫方法只耦合一个对象,而并不是全部的内部依赖
        * 更好的模拟现实世界,想象超市付款和命令狗行走的比喻
    - 违反得墨忒耳定律的例子
        * 链式调用——火车失事代码,这类连串的调用一般被认为是肮脏的风格,应该避免
        ```java
        final String outputDir = ctxt.getOptions().getScratchDir().getAbsolutePath();
        ```

        但有时消除这些"."可能并不太合适
        ```java
            getStudent().getParents().getBirthdays();
        ```
        改为下面这样可能并不太好,并无太大意义

        ```java
        getStudentParentsBirthdays()
        ```

        Java8的stream操做也不难理解,更简洁?
activityService.getPermittedActivitysByUserId(userId).stream()
                .map(assistanceActivityWrapper::wrap).collect(Collectors.toList());
4.混杂:
    - 一半是对象,一半是数据结构,这是一种糟糕的设计。它增长了添加新函数的难度,也增长了添加新数据结构的难度。
    - 隐藏结构:
        * 既然取得路径的目的是为了建立文件,那么不妨让 ctxt 对象来作这件事:
        ```java
            BufferedOutputStream bos = ctxt.createScratchFileStream(classFileName);
        ```
    - 数据传送对象
        * 最为精炼的数据结构,是一个只有公共变量、没有函数的类
        * 这种数据结构有时被称为 DTO,Data Transfer Objects,数据传送对象
        * 最多见的是Bean结构,其拥有赋值器和取值器操做的私有变量。
public class CommodityInfo {

                    private String title; //商品详情,产品参数,常见问题
                    private List<String> imageUrls;

                    public String getTitle() {
                        return title;
                    }

                    public void setTitle(String title) {
                        this.title = title;
                    }

                    public List<String> getImageUrls() {
                        return imageUrls;
                    }

                    public void setImageUrls(List<String> imageUrls) {
                        this.imageUrls = imageUrls;
                    }
                }
* Active Record:
            ** 是一种特殊的DTO形式,它拥有公共变量,一般也有save和find相似的可浏览的方法
            ** 但有些程序员在这类对象中塞入了业务规则方法,形成对象和数据结构的混合体
5.小结:
    - 对象暴露行为,隐藏数据,因此便于添加新对象难于添加新行为
    - 数据结构暴露数据,没有明显行为,便于添加新行为而难以添加新数据结构

五六章思考题

1.Lombok的反作用:
    - @Data注解 编译时自动添加Setter、Getter、toString()、equals()和hashCode()方法,除了Setter、Getter外,其它方法不太能用到
    - 使用@Data、@Getter、@Setter后get和set方法不显示了,不容易发现问题,isOpen,生成的get和set方法分别是isOpen()和getOpen,但有些框架如jackson默认的set方法是getIsOpen(),会报错
    - @SneakyThrows 隐藏异常 将检查异常包装为运行时异常,对API调用者不友好,调用者不能显示的获知须要处理检查异常
3.带业务逻辑的 Getter/Setter 是否是好的?
    - 很差,Getter/Setter单纯的取和设置某个属性的值就好,不要掺杂业务逻辑

第七章——错误处理

1.错误处理是必须的,但若是对错误的处理搞乱了原来代码的逻辑,就是处理不当或者是错误的作法。
2.使用错误异常而非返回码:
    - 抛异常,调用者能够选择处理即捕获异常,也能够选择不处理即继续向上层抛出异常
    - 使用返回码,调用者须要知道每一个返回码表明的含义,写一堆if-else逻辑,代码不够整洁
    - 使用异常可使错误处理从主逻辑中分离出来,使主逻辑清晰
3.先写Try-Catch-Finally语句
    - try代码块像是事务,不管try代码块中发生了什么,catch代码块将程序维持在一种持续状态
    - 在编写可能抛出异常的代码时,最好先写try-catch-finally语句,能帮你定义代码的用户应该期待什么,不管try代码块中执行的代码出什么错都同样。
4.使用不可控异常(unchecked exception):
    - 可控异常(checked exception)就是指在方法签名中标注异常。
    - 可控异常的代价是违反开放闭合原则,你对较底层的代码修改时,可能会波及不少上层代码。
    - 开放封闭原则Marting定义:
        - “对于扩展是开放的”,这意味着模块的行为是能够扩展的,当应用程序的需求改变时,咱们能够对其模块进行扩展,使其具备知足那些需求变动的新行为。换句话说,不能够改变模块的功能。
        - 加入抽象类,底层便于扩展,高层代码能够保持不变。客户端依赖抽象类基类,所以提供任何一个具体子类给客户端都不会违背开放封闭原则
        - 接口继承要好于实现继承。接口继承有强大的自适应能力。基于实现继承的,给继承顶部节点添加新成员的改动会影响到该层级结构下的全部成员,而接口要比类灵活的多。
        - 依赖接口的最大优点是接口变化的可能性要比实现小不少,若是接口发生变化,客户端也必需要作相应的改动。
        - 需求迭代带来的接口变化有时候是不免的
5.给出异常发生的环境说明:
    - 抛出的每一个异常,都应提供足够的环境说明,以便判断错误的来源
    - 利用日志系统,传递足够多的信息给catch块,并记录下来
    - 利于线上问题的排查
6.依调用者须要定义异常类:
    - 在应用程序中定义异常类时,最重要的要考虑这些异常是如何被捕获的
    - 对重复式处理方法的多种异常应进一步打包后抛出,而不是多个相同的catch?
    - 对异常的处理如有固定流程,也应该打包?
7.避免NPE,从函数不返回null、不向函数传递null参开始

第八章——边界

1.接口调用者和接口提供者之间存在张力,第三方程序包和框架提供者追求普适性,而使用者追求定制化以知足特定需求
2.学习性测试的好处不仅是免费
    - 学习性测试时一种精确试验,帮助咱们增进对API的理解
        - 腾讯广点通API的沙盒工具
        - 头条巨量引擎的广告预览工具
    - 学习性测试不光免费,当第三方程序包发布了新版本,能够运行学习性测试,看看程序包的行为有没有改变,可能帮助咱们确保第三方程序包按照咱们想要的方式工做
3.学习使用log4j
4.使用尚不存在的代码:
    - mock咱们想要获得的接口和数据,好处之一是有助于保持客户代码集中于它该完成的工做,坏处多是对接有问题
    - 约定好API的逻辑,mock数据,并行开发,逻辑约定的越好,对接越无缝
5.整洁的边界:
    - 边界上的改动,有良好的软件设计,无需巨大投入和重写便可进行修改
    - 边界上的代码须要清晰的分割和定义指望的测试。依靠你能控制的东西,好过依靠你控制不了的东西,省得往后受它控制
    - 可使用适配器模式将咱们的接口转换为第三方提供的接口

第九章——单元测试

1.保持测试整洁:
    - 测试代码和生产代码同样重要
2.测试带来的好处:
    - 单元测试让代码可扩展、可维护、可复用
    - 测试可让你毫无顾虑的改进架构和设计
    - 测试有利于防止生产代码腐败
3.测试代码越脏,生产代码就会越脏
4.整洁的测试:
    - 可读性、可读性、可读性
    - 构造-操做-检验(BUILD_OPERATE-CHECK)
        - 第一个环节构造测试数据
        - 第二个环节操做测试数据
        - 第三个部分检验操做是否获得指望的结果
    - 面向特定领域的测试语言:
        - 测试API并不是起初就被设计出来,而是由测试代码进行后续重构逐渐演进而来
        - 开发者须要将测试代码重构为更简洁而具备表达力的形式
    - 双重标准
        - 与CPU效率或内存有关的问题,在生产环境不能够作,可是在测试环境中彻底没有问题
        - StringBuffer丑陋?
5.每一个测试一个概念:
    - 一个测试函数只测试一个概念
6.F.I.R.S.T
    - 测试代码要够快,要频繁测试,以更早、更快的发现生产代码的bug
    - 每一个测试都应该相互独立,某个测试不该该为下一个测试设定条件。应该能够单独运行每一个测试,以及以任何顺序运行测试
    - 测试代码应该不挑环境,不管是在测试环境、生产环境仍是没有网络的本地,在什么时候何地都应该可重复的运行
    - 测试应该有布尔值输出,测试的结果不该该从日志或者对比文本等不直观的方式被展示出来
    - 测试应该被及时编写,最好应该在编写生产代码以前编写测试代码

第十章——类

1.类的组织:
    - 类应该从变量列表开始:
        - 公共静态变量应该先出现,接着是私有实体变量、不多有公共变量
        - 公共函数在变量列表以后,公共函数调用的私有工具函数跟在公共函数后面,符合自顶向下原则
    - 封装:
        - 保持变量和工具函数的私有性但并不执着于此
        - 放松封装老是下策
2.类应该短小
    - 用权责来衡量类的大小
    - 单一权责原则(SRP):类或者模块应该有且仅有一个加以修改的理由
    - 内聚:
        - 类中的每一个方法都应该操做一个或多个实体变量
        - 内聚性高意味着类中的方法和变量相互依赖、互相结合成一个逻辑总体
3.为了修改而组织:
    - 开放-闭合原则(OCP):对扩展开放,对修改封闭
    - 隔离修改:借助接口和抽象类来隔离这些细节带来的影响

善用工具

  • 规范化本身的代码不能只靠意志,更要靠工具,少点我的风格,多点通用规矩,并学会使用CheckStyle工具。
  • 很难命名的变量、函数,找小伙伴商量下,别本身一我的憋着
  • 宁肯变量名长,也不要让变量名短得让人没法揣测其含义
  • 避免重复(DRY),尽量杜绝重复代码,凡是须要用至少两次的代码,给它单独写成一个类或函数
相关文章
相关标签/搜索