阅读《代码整洁之道》的一些感悟

第二章 有意义的命名

  • 名副其实

    • 通过名称能看出变量代表的含义

      int d;// 消逝的时间
      int elapsedTimeInDays;
      int daysSinceCreation;
      
      • 选择体现本意的名称能让人更容易理解和修改代码
      public List<int[]> getThem() {
      	List<int[]> list1 = new ArrayList<int[]>();
          for (int[] x : theList)
              if (x[0] == 4)
                  list1.add(x);
          return list1;
      }
      
      • 疑问

        • theList中是什么类型的东西
        • theList 零下标代表什么
        • 值4的意义是什么
        • 返回的列表如何使用
      • 示例说明:

        • 比方说,我们在开发一种扫雷游戏,盘面名为theList的单元格列表,那就将其名称改为gameBoard
        • 盘面上每个单元格都用一个简单数组表示,并且零下标代表一种状态值,当值为4时表示“已标记”
        • 那么返回盘面上已标记的位置,我们称为flaggedCells
      public List<int[]> getFlaggedCells() {
      	List<int[]> flaggedCells = new ArrayList<int[]>();
          for (int[] cell : gameBorad)
              if (cell[STATUS_VALUE] == FLAGGED)
                  flaggedCells.add(cell);
          return flaggedCells;
      }
      
      • 此种情况下,还可继续优化,不用int数组表示单元格,而是使用Cell类,Cell#isFlagged代表表格是否被标记
      public List<Cell> getFlaggedCells() {
      	List<Cell> flaggedCells = new ArrayList<Cell>();
          for (Cell cell : gameBorad)
              if (cell.isFlagged())
                  flaggedCells.add(cell);
          return flaggedCells;
      }
      
  • 避免误导

    • XYZControllerForEfficientHandlingOfStringsXYZControllerForEfficientStoragelingOfStrings
  • 做有意义的区分

    • 很难区分调用哪个函数
      • getActiveAccount()
      • getActiveAccounts()
      • getActiveAccountInfo()
    • 以下变量命名如缺少明确约定,该如何区分
      • moneyAmoutmoney
      • customercustomerInfo
      • accountDataaccount
  • 使用可搜索的名称

    • 单字母名称和数字常量有个问题,很难在一大篇文字中找出来
    • 而较长变量名称可以很方便被检索出
    • 单字母名称仅应该用于短方法的本地变量,名称长短应与其作用域大小相对应
    • 示例
    for (int j=0; j<34; j++) {
        s += (t[j]*4)/5;
    }
    
    int realDaysPerIdealDay = 4;
    const int WORK_DAYS_PER_WEEK = 5;
    int sum = 0;
    for (int j=0; j<NUMBER_OF_TASKS; j++) {
        int realTaskDays = taskEstimate[j] * realDaysPerIdealDay;
        int realTaskWeeks = (realdays/WORK_DAYS_PER_WEEK);
        sum += realTaskWeeks;
    }
    

第二章 类与接口

  • 类名

    • 类名和对象名是名词或名词短语
      • CustomerWikiPageAccountAddressParser
      • 避免使用ManagerProcessorDataInfo这样的类名
  • 方法名

    • 方法名应当是动词或者动词短语

      • postPaymentdeletePagesave

      • 属性访问器、修改器和断言应根据其值命名,并依Javabean标准加上setgetis前缀

      • 重载构造器时,建议使用静态工厂方法

    Complex fulcruPoint = Complex.FromRealNumber(23.0);
    
    通常好于
    
    Complex fulcrumPoint = new Complex(23.0);
    

第三章 如何写好函数

  • 函数的第一要则是要短小

  • 第二要则还要更短小

  • 函数应该做一件事。做好这件事。只做这一件事

    • 如何判断函数是做了一件事还是多件事
      • 函数所包含步骤都在函数名下的同一抽象层
    • 例子见代码整洁之道 3.2 33页
  • 每个函数一个抽象层级

  • 自顶向下读代码:向下规则

    • 我们想要让代码拥有自顶向下的阅读顺序
    • 我们想要让每个函数后面都更着位于下一抽象层级的函数
    • 这样在查看函数列表时,就能循环抽象层级向下阅读了
public class Sports {
    
    private Ball ball;
    
    public void playBall(boolean flag) {
        if(flag) {
            playBasketball();
        } else {
            //todo
        }
    }
    
    private void playBasketball() {
        wearBasketballSuite();
        warmUp();
        play(new Basketball());
    }
    
    private void wearBasketballSuite() {
        wearBasketballClothes();
        wearBasketballShoes();
	}
    
    private void wearBasketballClothes() {
        System.out.println("already wore basketball clothes");
    }
    
    private void wearBasketballShoes() {
        System.out.println("already wore basketball shoes");
    }
    
    private void warmUp() {
        //todo
    }
    
    private void play(Ball ball) {
        //todo
    }
}
  • 抽离try/catch 代码块:try/catch代码块专职处理异常,将业务逻辑封装为新的函数,扔在try/catch代码块中,这样一来,错误处理和业务功能处理美妙地分割开来

  • 使用异常代替错误码:当错误码定义为枚举或类时,如果该枚举或者类修改了,会导致依赖其的类需要重新构建和部署了;而如果使用异常代替错误码,就直接可以从异常类派生出来

别重复自己

  • 面向对象编程将代码集中到基类,避免了冗余
  • 面向方面编程、面向组件编程均是消除重复代码的一种策略

第四章 注释

第五章 格式

  • 纵向格式

    • 几乎所有的代码都是从上往下读,从左往右读。每行展现一个表达式或一个子句,每组代码行展示一条完整的思路。这些思路通过空白行区隔开来
    • 紧密联系的函数放在一起
    • 被调用的函数应该放在执行调用的函数下面 —> 建立了自顶向下贯穿源代码模块的良好信息流
  • 横向格式

    • 每一行的字符数不宜太多

得墨忒耳律(The Law of Demeter)

模块不应了解它所操作对象的内部情况

该定律认为,类C的方法f只应该调用以下对象的方法(只跟朋友谈话,不与陌生人谈话)

  • C
  • 由f 创建的对象
  • 作为参数传递给f的对象
  • 由C的实体变量持有的对象

第六章 对象与数据结构

  • 对象暴露行为,隐藏数据
  • 数据结构暴露数据,而没有明显的行为

第七章 错误处理

  • 使用异常而非使用返回码

  • 使用不可控异常

  • 给出异常发生的环境说明:充分的错误消息:失败的操作和失败类型

  • 依调用者需要定义异常类

  • 定义常规流程

    • 当异常中也需要处理业务逻辑时,可以将try …catch 中的逻辑封装为函数
    • 这种手法叫作特例模式

在这里插入图片描述

  • 别返回null值:如果方法返回null值,请抛出异常,或者使用特例对象来代替

    • 这样就不用频繁地检查方法的返回值是否为空
    • 避免空指针异常
public List<Stirng> getUserNameList() {
    List<String> userNames = userDao.getUserNameList();
    if (Collections.isEmpty(userNames)) {
        return Collections.emptyList();
    }
    return userNames;
}
  • 禁止向方法传入null,除非方法要求这样做
将错误处理隔离看待,独立于主要逻辑之外,这样的代码强固而整洁

第八章 边界

  • 使用第三方代码时,可以抽象自己的接口,通过接口调用第三方代码,降低应用与第三方耦合
  • 当第三方代码升级或功能改变时,通过调整包装的接口来实现相同的功能

第十章 类

标准的Java约定:

  • 类应该从一组变量列表开始,顺序如下

    • 公共静态常量
    • 私有静态变量
    • 私有实体变量
  • 公共函数跟在变量列表之后

    • 某个公共函数调用的私有函数紧随在该公共函数后面
    • 这才符合自顶向下原则
  • 类应该短小

    • 对于函数,通过计算代码行数衡量大小,而对于类,我们计算权责(responsibility)
    • 单一职责原则(SRP)认为,类或模块应有且只有一条加以修改的理由
  • 内聚

    • 在类中,函数应保证使用到尽可能多的成员变量,这样的类才更内聚
    • 保持函数和参数列表短小的策略
    • 在类中,少数函数共享了许多成员变量,这些变量仅被少数函数使用,就将其拆分为新的子类吧
  • 隔离修改

    • OCP (开放-闭合原则):对扩展开发,对修改关闭
    • DIP (依赖倒置原则) :类应该依赖于抽象而不是依赖于具体细节

第十一章 系统

  • 将系统的构造和使用分隔开

    • 应尽量避免以下写法
    • 应将对象构造的启始和设置,与正常的运行时逻辑分离出来
    public Service getService() {
    	if (service == null) {
            service = new MyServiceImpl(...);
    	}
        return service
    }
    
  • 将构造与使用分开的方法

    • 将全部构造过程搬迁到main或称之为main的模块中

      • 设计系统的其余部分时,假设所有对象都已正确构造和设置
      • 其余部分只是使用构造和设置好的对象,而对构造过程一无所知
    • 需要应用程序自己构造对象时,建议使用抽象工厂模式

      • 应用可自行控制何时创建,但是无法知晓构造细节
    • 依赖注入

    • AOP

第十二章 迭进

Kent Beck提出简单设计的四条规则

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

第十三章 并发编程

对象是过程的抽象,线程是调度的抽象

第十四章 逐步改进

逐步改进

第十七章 味道与启发

  • 注释:注释应该谈及代码自身没提到的东西

    • 不恰当的信息
    • 废弃的注释
    • 冗余注释
    • 糟糕的注释
    • 注释掉的代码
  • 环境

  • 函数

    • 过多的参数

      • 没参数最好
      • 一个次之,两个、三个再次之
      • 三个以上参数非常值得质疑,应坚决避免
    • 输出参数(对于修改对象状态的函数来说)

      • 输出参数违反直觉
      • 如果函数非要修改什么东西的状态,就修改它所在对象的状态就好了
    • 标志参数

      • 布尔值参数说明函数不止做一件事,应该消灭掉
    • 死函数

      • 不被调用的方法应该丢弃
  • 一般性问题

    • 一个源文件存在多种语言
    • 明显的行为未被实现
    • 不正确的边界行为
      • 行为的边界处理得不全面,当函数处于边界条件或极端条件时,会出现异常
  • 忽视安全

  • 重复

    • DRY (Don’t Repeat Yourself)
  • 在错误的抽象层级上的代码

    • 较高层级概念放在基类中
    • 较低层级概念放在派生类中
      • 只与细节实现有关的常量、变量或工具函数放在子类中
  • 基类依赖于派生类

    • 基类和派生类部署在不同jar中
      • 派生类修改后,派生类组件重新部署时,不需要部署基类组件
  • 信息过多:设计良好的模块有着非常小的接口

    • 优秀的开发人员会限制类或模块中暴露的接口数量
      • 类中的方法越少越好
      • 函数知道的变量越少越好
      • 类拥有的实体变量越少越好
  • 死代码

    • 删除不执行的代码
      • 不会发生条件的if语句
      • 不会抛出异常的try/catch块
      • 不会被调用的小工具方法
      • 不会发生的switch/case条件
  • 垂直分隔

    • 变量和函数应该靠近被使用的地方定义
      • 本地变量定义与使用的垂直距离要短
      • 私有函数首次使用与定义时的垂直距离要短
  • 前后不一致

    • 最小惊异原则:小心选择约定,选中后,就小心持续遵循
      • 如使用response来持有HttpServletResponse对象,其他使用HttpServletResponse的地方也使用同样的变量名
      • 方法命名上也要前后一致,处理同一对象的系列方法使用类似的命名
        • 操作 + 对象 来命名
  • 混淆视听

    • 没用的变量
    • 从不调用的函数
    • 没信息量的注释
    • 上述都应被移除
  • 选择算子函数

    • 不要试图通过参数来改变函数的行为
    • 这样的话,应该重构为两/多个函数
  • 位置错误的权责

  • 代码应放在读者自然而然期待它所带在的地方

  • 不恰当的静态方法

  • 静态方法不能被重写,当某个方法存在多态的情形时,不应该被设为static

  • 使用解释性变量

    • 让程序可读的最有力方法之一就是用有意义的单词命名变量
  • 函数名称应该表达其行为

    Date newDate = date.add(5);
    
    • 这里add的是5天、5个星期、还是5个小时呢
    • 这里返回一个新日期,旧日期会改动吗
    • 如果函数向日期添加5天且修改该日期,命名为addDaysToincreaseByDays
    • 如果函数返回一个表示5天后的日期,而不修改日期实体,命名为daysLaterdaysSince
  • 把逻辑依赖改为物理依赖

    • A依赖于B,但是A中依赖了应属于B中的成员C,这就叫作逻辑依赖
    • C提取到B中,通过AB的依赖而访问C,这便从逻辑依赖转换为了物理依赖
  • 用多态替代If/ElseSwitch/Case

    • 出现多个Swtich/Case时,应考虑使用 多态
  • 遵循标准约定

  • 遵循团队的编码标准

  • 用命名常量替代魔术数

  • 抽离if|while中的条件为恰当命名的函数

  • 避免否定性条件(便于理解)

  • 函数只该做一件事

  • 掩蔽时序耦合

    • 常常有必要使用时序耦合,不应该掩蔽它

      public static MoogDiver {
      	Gradient gradient;
          List<Spline> splines;
          
          public void dive(String reason) {
              saturateGradient();
              reticulateSplines();
              diveForMoog(reason);
          }
      }
      
    • 三个函数的次序很重要

      • 捕鱼先织网
      • 织网先编绳
    • 然而代码并没有强制这种时序耦合,程序员在调用saturateGradient之前调用reticulateSplines,从而导致抛出UnsaturatedGradientException异常。

    • 代码重构后,如下所示

      public static MoogDiver {
      	Gradient gradient;
          List<Spline> splines;
          
          public void dive(String reason) {
              Gradient gradient = saturateGradient();
              List<Spline> splines = reticulateSplines(gradient);
              diveForMoog(splines, reason);
          }
      }
      
    • 这样通过创建顺序队列暴露了时序耦合,每个函数都产生出下个函数所需的结果,这样必须按顺序调用函数了

    • 虽然增加了函数的复杂度,却曝露了该种情况真正的时序复杂性,从而避免了异常

  • 封装边界条件

  • 函数应该只在一个抽象层级上

  • 拆分不同抽象层级时重构的最重要功能之一

  • 在较高层级放置可配置数据

  • 位于较高层级的配置性常量易于修改,它们向下贯穿应用程序

  • 不要继承常量

  • 枚举代替常量,枚举拥有方法和字段

  • 使用描述性名称、名称应与抽象层级相符

  • 为较大作用范围变量选用较长名称

  • 名称应该说明副作用

    • 名称应该说明函数、变量或类的一切信息,不要用名称掩蔽副作用

    • 不要用简单的动词来描述做了不止一个简单动作的函数

      public ObjectOutputStream getOos() throws IOException {
      	if (oos == null) {
      		oos = new ObjectOutputStream(socket.getOutputStream());
      	}
      	return oos;
      }
      
    • 该函数不只是获取一个oos,如果oos不存在,还会创建一个,更好的命名可以叫作createOrReturnOos

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值