Skip to content
hexi edited this page Feb 3, 2016 · 1 revision

整洁的代码和糟糕的代码的定义

对于整洁代码,有多少程序猿就有多少定义,我罗列了一些大师对整洁代码的看法。

  • 减少依赖
  • 便于维护
  • 整洁的代码只做好一件事
  • 从不隐藏设计者的意图,
  • 干净利落的抽象和直接了当的控制语句
  • 有单元测试和验收测试,
  • 有意义的命名,
  • 他只提供一种而非多种做一件事的途径,
  • 代码应该通过字面表达含义
  • 能通过所有测试,
  • 没有重复代码
  • 整洁的代码只做好一件事
  • 从不隐藏设计者的意图,
  • 干净利落的抽象和直接了当的控制语句
  • 有单元测试和验收测试,
  • 有意义的命名,
  • 他只提供一种而非多种做一件事的途径,
  • 代码应该通过字面表达含义
  • 能通过所有测试,
  • 没有重复代码
糟糕的代码顾名思义就是整洁的代码的反义词

整洁的代码的意义

如果你不明白整洁对代码有何意义,尝试去写整洁的代码就毫无所益。。。。

如何定位糟糕的代码

  1. 注释
    1. 不恰当的信息
    2. 废弃的注释
    3. 冗余的注释:如果蛛丝描述的是某种充分自我描述的东西,那么注释就是多余的。注释应该谈及代码自身没有提到的东西。 i++; //increment i
    4. 糟糕的注释:别画蛇添足,别闲扯,保持简洁,用最少的字符准确传递想要表达的信息
    5. 注释掉的代码:它污染了所属的模块,分散了想要读他的人的注意力,注释掉的代码纯属厌物。
  2. 函数
    1. 过多的参数 函数的参数量应该少。没参数最好,一个次之,两个、三个再之。三个以上的参数非常值得质疑,应坚决避免。
    2. 输出参数 输出参数违反直觉。读者期望参数用于输入而非输出。 Rect rc = new Rect(); windowManager.getDefaultDisplay().getRectSize(rc) Matrix matrix = new Matrix(); float scale = (float)rc.width() / bmp.getWidth();
    3. 标识参数 布尔值参数大声宣告函数做了不止一件事。他们令人迷惑,应该消除掉。
    4. 死函数 永不调用的方法应该丢弃。保留死代码纯属浪费。别害怕删除函数。记住,源代码控制系统还会记得它。
  3. 一般性的问题
    1. 一个源文件中存在多种语言。 理想的源文件包括且只包括一种语言。现实上, 我们可能会不得不使用多于一种语言。 但应该尽力减少原文件中额外语言的数量和范围。
    2. 明显的行为未被实现。 函数或类应该有理由实现其他程序猿有理由期待的行为。例如,考虑将一个日期名称翻译为表示日期的枚举的函数。 Day day = DayDate.StringToDay(String dayName); 我们期望字符串Monday翻译为Day.MONDAY。我们也期望常用缩写形式也能被翻译出来,我们期待函数忽略大小写。 如果明显行为未被实现,读者和用户就不能再依靠他们对函数名的直觉。他们不再信任原作者,不得不阅读代码的细节。
    3. 不正确的边界行为。 别依赖直觉,追索每种边界条件,并编写测试。
    4. 忽视安全。
    5. 重复。 DRY(Don't Repeat Yourself)原则,Kent Beck将它列为极限编程的核心原则之一。 每次看到重复代码,都代表遗漏了抽象。较隐蔽的形态是在不同的模块中不断重复出现、检测同一组条件的switch/case或if/else链。可以用多态来代替之。 更隐蔽的形态是采用类似算法但具体代码在不同的模块。这也是一种重复,可以使用模板方法模式或策略模式来修正。
    6. 在错误的抽象层级上的代码 所有较低层级的概念放在派生类中,所有较高层级的概念放在基类中。例如只与细节实现有关的常量、变量或工具函数不应该在基类中出现,基类应该对这类东西一无所知。
    7. 基类依赖于派生类,通常来说,基类对派生类一无所知。
    8. 信息过多。 设计良好的模块有这非常小的接口,让你事半功倍。不提供许多需要依靠的函数,所以耦合度低,类中暴露的方法越少越好,函数知道的变量越少越好。 隐藏你的数据,工具函数,常量和临时变量。不要创建拥有大量方法或大量实体变量的类。不要为子类创建大量受保护的函数和变量。
    9. 死代码。 就是不执行的代码。例如永不会执行的if条件和switch/case,从不抛出异常的catch块中。如果你找到死代码,就体面的埋葬它,将它从系统中删除掉。
    10. 垂直分割。 变量和函数应该在靠近被使用的地方定义。
    11. 前后不一致。 如果在特定的函数中用response的变量来持有HttpServletResponse对象,则在其他用到HttpServletResponse对象的函数中也用同样的变量名称。
    12. 人为耦合。 一般来说,认为耦合是指两个没有直接目的的模块之间的耦合。其根源是将变量、常量或函数放在不恰当地放在临时方便的位置。这是种漫不经心的偷懒行为。
    13. 特性依赖。 类的方法只应对其所属的类中的变量和函数感兴趣,不该垂青其他类中的变量和函数。当方法通过某个其他对象的访问器和修改器来操作该对象内部数据, 则他就依恋该对象所属类的范围。
    14. 选择算子参数。 选择算子参数不一定是boolean类型,可能是枚举元素、整数或任何一种用于选择函数行为的参数。 使用多个函数,通常优于向单个函数传递某些代码来选择函数行为。
    15. 晦涩的意图。 代码尽可能要有表达力。联排表达式、匈牙利语标记法和魔术数都遮蔽了作者的意图。 例如下面的overTimePay函数可能的一种表现形式:
     public int m_otCalc() {
            return iThsWkd * iThsRte +
                (int) Math.round(0.5 * iThsRte *
                    Math.max(0, iThsWkd - 400));
     }
    
    1. 位置错误的权责。
    2. 不恰当的静态方法。 如果的确需要静态函数,确保没机会打算让它有多台行为。
    3. 尽量使用解释性变量。
    4. 函数名应该表达其行为。
    5. 理解算法 好多可笑的代码出现,是因为人们没有花时间去理解算法。他们硬塞足够多的fi语句和标识,从不停下来考虑发生了什么,勉强让系统能工作。
    6. 把逻辑依赖改为物理依赖。
    7. 用多态替代if/else或switch/case。
    8. 用常量命名替代魔术数。
    9. 封装条件。 if (shouldBeDeleted(timer)) 要好于 if (timer.hasExpired() && !timer.isRecurrent())
    10. 避免否定性条件。 if (buffer.shouldCompact()) 要好于 if (!buffer.shouldNotCompact())
    11. 函数只该做一件事。 例如:
      public void pay() {
          for (Employee e: employees) {
              if (e.isPayday()) {
                  Money pay = e.calculatePay();
                  e.deliverPay(pay);
              }
          }
      }
      
      这段代码做了三件事。它遍历所有雇员,检查是否该给雇员付工资,然后付薪水。代码可以写的更好,如:
      public void pay() {
          for (Employee e: employees) {
              payIfNecessary(e);
          }
      }
      
      private void payIfNecessary(Employee e) {
          if (e.isPayday()) {
              calculateAndDeliverPay(e);
          }
      }
      
      private void calculateAndDeliverPay(Employee e) {
          Money pay = e.calculatePay();
          e.deliverPay(pay);
      }
      
    12. 掩蔽时序耦合。
      public class MoogDiver {
          Gradient gradient;
          List<Spline> splines;
      
          public void dive(String reason) {
              saturateGradient();
              retriculateSplines();
              diveForMoog(reason)
          }
      }
      
      假设三个函数的调用顺序很重要,必须要按照这种顺序调用,更好的方式是:
      public class MoogDiver {
          Gradient gradient;
          List<Spline> splines;
      
          public void dive(String reason) {
              Gradient gradient = saturateGradient();
              List<Spline> splines retriculateSplines(gradient);
              diveForMoog(splines, reason);
          }
      }
      
    13. 封装边界条件。
      if (level + 1 < targs.length) {
          parts = new Parse(body, tags, level + 1, offset + endTag);
          body = null;
      }
    
    
     注意,level + 1,出现了两次。这个是应该封装到名为nextLevel之类的变量中的边界条件。
     ```
     int nextLevel = level + 1;
     if (nextLevel < targs.length) {
         parts = new Parse(body, tags, nextLevel, offset + endTag);
         body = null;
     }
     ```
    
    1. 函数应该只在一个抽象层级上。
    2. 避免传递浏览。 通常我们不想让某个模块了解太多其他协作者的信息,更具体地说,如果A和B协作,B和C协作,我们不想让使用A的模块了解C模块的信息。 例如不应该写a.getB().getC().doSomething()的代码。
    3. 采用描述性的名称。 不要太快取名。确认名称具有描述性。记住,事物的意义随着软件的演化而变化,所以要经常性的重新估量名称是否合适。 软件中得名称对于软件的可读性有90%的作用。你要花时间明智的取名,名称太重要了,不可随意对待。 符号和魔术数的大杂烩:
      public int x() {
          int q = 0;
          int z = 0;
          for (int kk = 0; kk < 10; kk++) {
              if (l[z] == 10) {
                  q += 10 + (l[z + 1] + l[z + 2]);
                  z += 1;
              } else if (l[z] + l[z + 1] == 100) {
                  q += 10 + l[z + 2];
                  z += 2;
              } else {
                  q += l[z] + l[z + 1];
                  z += 2;
              }
          }
          return q;
      }
      
      
      再看下面的重构:
      public int score() {
          int score = 0;
          int frame = 0;
          for (int frameNumber = 0; frameNumber < 10; frameNumber++) {
              if (isStrike(frame)) {
                  score += 10 + nextTwoBallsForStrike(frame);
                  frame +1;
              } else if (isSpare(frame)) {
                  score += 10 + nextBallForSpare(frame);
                  frame += 2;
              } else {
                  score += twoBallsInFrame(frame);
                  frame += 2;
              }
          }
          return score;
      }
      
    4. 名称应该说明副作用。 不要用简单的动词来描述做了不止一个简单动作的函数。如:
      public ObjectOutputStream getOos() throws IOException {
          if (oos == null) {
              oos = new ObjectOutputStream(socket.getOutputStream());
          }
          return oos;
      }
      
      
      该函数不只是获取一个oos,如果oos不存在,还会创建一个。所以更好的名称大概是createOrReturnOos。

如何干掉糟糕的代码

写整洁的代码,需要遵循大量的小技巧,贯彻刻苦习得”整洁感“,这种代码感就是关键所在。

  • 借用美国童子军军规:让营地比你来时更干净。如果每次签入时,代码都比签出时干净,那么代码就不会腐坏。清理并不一定要花多少功夫,也许只是改好一个变量名,拆分一个有点过长的函数,消除一点点重复的代码,清理一个嵌套if语句。
  • 有意义的命名。
  • 还有就是有关函数、格式、对象和数据结构、类等等
  • 个人认为很少有人能不经重构,一次就能写出整洁的代码。大师也是在不断地迭代和重构中完成的。

工作实践

当我想增加一个notificaionType的枚举时,需要在多处修改代码。

public static NotificationType fromInt(int value) 方法要增加判断 enter image description here

ForegroundHandler.public boolean canHandle(NotificationType dataType)要修改代码 enter image description here

CommonHandler.public boolean canHandle(NotificationType dataType)要修改代码 enter image description here

MainActivity.private void switchActivity()要修改代码 enter image description here

重构后的代码:

  1. 在NotificationType类增加了一个实例方法,public boolean isNeedHandle(),那么ForegroundHandler和CommonHandler的canHandle的实现就可以通过调用参数的isNeedHandle方法处理。
  2. 在NotificationType.public static NotificationType fromInt(int value)的实现里,我通过遍历values,如果找到值等于参数值,就返回该枚举。 所以以后增加一个推送类型是只要创建一个枚举实例,且修改MainActivity的switch/case就可以了。