-
Notifications
You must be signed in to change notification settings - Fork 0
重构代码
hexi edited this page Feb 3, 2016
·
1 revision
对于整洁代码,有多少程序猿就有多少定义,我罗列了一些大师对整洁代码的看法。
- 减少依赖
- 便于维护
- 整洁的代码只做好一件事
- 从不隐藏设计者的意图,
- 干净利落的抽象和直接了当的控制语句
- 有单元测试和验收测试,
- 有意义的命名,
- 他只提供一种而非多种做一件事的途径,
- 代码应该通过字面表达含义
- 能通过所有测试,
- 没有重复代码
- 整洁的代码只做好一件事
- 从不隐藏设计者的意图,
- 干净利落的抽象和直接了当的控制语句
- 有单元测试和验收测试,
- 有意义的命名,
- 他只提供一种而非多种做一件事的途径,
- 代码应该通过字面表达含义
- 能通过所有测试,
- 没有重复代码
如果你不明白整洁对代码有何意义,尝试去写整洁的代码就毫无所益。。。。
- 注释
- 不恰当的信息
- 废弃的注释
- 冗余的注释:如果蛛丝描述的是某种充分自我描述的东西,那么注释就是多余的。注释应该谈及代码自身没有提到的东西。 i++; //increment i
- 糟糕的注释:别画蛇添足,别闲扯,保持简洁,用最少的字符准确传递想要表达的信息
- 注释掉的代码:它污染了所属的模块,分散了想要读他的人的注意力,注释掉的代码纯属厌物。
- 函数
- 过多的参数 函数的参数量应该少。没参数最好,一个次之,两个、三个再之。三个以上的参数非常值得质疑,应坚决避免。
- 输出参数 输出参数违反直觉。读者期望参数用于输入而非输出。 Rect rc = new Rect(); windowManager.getDefaultDisplay().getRectSize(rc) Matrix matrix = new Matrix(); float scale = (float)rc.width() / bmp.getWidth();
- 标识参数 布尔值参数大声宣告函数做了不止一件事。他们令人迷惑,应该消除掉。
- 死函数 永不调用的方法应该丢弃。保留死代码纯属浪费。别害怕删除函数。记住,源代码控制系统还会记得它。
- 一般性的问题
- 一个源文件中存在多种语言。 理想的源文件包括且只包括一种语言。现实上, 我们可能会不得不使用多于一种语言。 但应该尽力减少原文件中额外语言的数量和范围。
- 明显的行为未被实现。 函数或类应该有理由实现其他程序猿有理由期待的行为。例如,考虑将一个日期名称翻译为表示日期的枚举的函数。 Day day = DayDate.StringToDay(String dayName); 我们期望字符串Monday翻译为Day.MONDAY。我们也期望常用缩写形式也能被翻译出来,我们期待函数忽略大小写。 如果明显行为未被实现,读者和用户就不能再依靠他们对函数名的直觉。他们不再信任原作者,不得不阅读代码的细节。
- 不正确的边界行为。 别依赖直觉,追索每种边界条件,并编写测试。
- 忽视安全。
- 重复。 DRY(Don't Repeat Yourself)原则,Kent Beck将它列为极限编程的核心原则之一。 每次看到重复代码,都代表遗漏了抽象。较隐蔽的形态是在不同的模块中不断重复出现、检测同一组条件的switch/case或if/else链。可以用多态来代替之。 更隐蔽的形态是采用类似算法但具体代码在不同的模块。这也是一种重复,可以使用模板方法模式或策略模式来修正。
- 在错误的抽象层级上的代码 所有较低层级的概念放在派生类中,所有较高层级的概念放在基类中。例如只与细节实现有关的常量、变量或工具函数不应该在基类中出现,基类应该对这类东西一无所知。
- 基类依赖于派生类,通常来说,基类对派生类一无所知。
- 信息过多。 设计良好的模块有这非常小的接口,让你事半功倍。不提供许多需要依靠的函数,所以耦合度低,类中暴露的方法越少越好,函数知道的变量越少越好。 隐藏你的数据,工具函数,常量和临时变量。不要创建拥有大量方法或大量实体变量的类。不要为子类创建大量受保护的函数和变量。
- 死代码。 就是不执行的代码。例如永不会执行的if条件和switch/case,从不抛出异常的catch块中。如果你找到死代码,就体面的埋葬它,将它从系统中删除掉。
- 垂直分割。 变量和函数应该在靠近被使用的地方定义。
- 前后不一致。 如果在特定的函数中用response的变量来持有HttpServletResponse对象,则在其他用到HttpServletResponse对象的函数中也用同样的变量名称。
- 人为耦合。 一般来说,认为耦合是指两个没有直接目的的模块之间的耦合。其根源是将变量、常量或函数放在不恰当地放在临时方便的位置。这是种漫不经心的偷懒行为。
- 特性依赖。 类的方法只应对其所属的类中的变量和函数感兴趣,不该垂青其他类中的变量和函数。当方法通过某个其他对象的访问器和修改器来操作该对象内部数据, 则他就依恋该对象所属类的范围。
- 选择算子参数。 选择算子参数不一定是boolean类型,可能是枚举元素、整数或任何一种用于选择函数行为的参数。 使用多个函数,通常优于向单个函数传递某些代码来选择函数行为。
- 晦涩的意图。 代码尽可能要有表达力。联排表达式、匈牙利语标记法和魔术数都遮蔽了作者的意图。 例如下面的overTimePay函数可能的一种表现形式:
public int m_otCalc() { return iThsWkd * iThsRte + (int) Math.round(0.5 * iThsRte * Math.max(0, iThsWkd - 400)); }
- 位置错误的权责。
- 不恰当的静态方法。 如果的确需要静态函数,确保没机会打算让它有多台行为。
- 尽量使用解释性变量。
- 函数名应该表达其行为。
- 理解算法 好多可笑的代码出现,是因为人们没有花时间去理解算法。他们硬塞足够多的fi语句和标识,从不停下来考虑发生了什么,勉强让系统能工作。
- 把逻辑依赖改为物理依赖。
- 用多态替代if/else或switch/case。
- 用常量命名替代魔术数。
- 封装条件。 if (shouldBeDeleted(timer)) 要好于 if (timer.hasExpired() && !timer.isRecurrent())
- 避免否定性条件。 if (buffer.shouldCompact()) 要好于 if (!buffer.shouldNotCompact())
- 函数只该做一件事。
例如:
这段代码做了三件事。它遍历所有雇员,检查是否该给雇员付工资,然后付薪水。代码可以写的更好,如:
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); }
- 掩蔽时序耦合。
假设三个函数的调用顺序很重要,必须要按照这种顺序调用,更好的方式是:
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); } }
- 封装边界条件。
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; } ```
- 函数应该只在一个抽象层级上。
- 避免传递浏览。 通常我们不想让某个模块了解太多其他协作者的信息,更具体地说,如果A和B协作,B和C协作,我们不想让使用A的模块了解C模块的信息。 例如不应该写a.getB().getC().doSomething()的代码。
- 采用描述性的名称。
不要太快取名。确认名称具有描述性。记住,事物的意义随着软件的演化而变化,所以要经常性的重新估量名称是否合适。
软件中得名称对于软件的可读性有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; }
- 名称应该说明副作用。
不要用简单的动词来描述做了不止一个简单动作的函数。如:
该函数不只是获取一个oos,如果oos不存在,还会创建一个。所以更好的名称大概是createOrReturnOos。
public ObjectOutputStream getOos() throws IOException { if (oos == null) { oos = new ObjectOutputStream(socket.getOutputStream()); } return oos; }
写整洁的代码,需要遵循大量的小技巧,贯彻刻苦习得”整洁感“,这种代码感就是关键所在。
- 借用美国童子军军规:让营地比你来时更干净。如果每次签入时,代码都比签出时干净,那么代码就不会腐坏。清理并不一定要花多少功夫,也许只是改好一个变量名,拆分一个有点过长的函数,消除一点点重复的代码,清理一个嵌套if语句。
- 有意义的命名。
- 还有就是有关函数、格式、对象和数据结构、类等等
- 个人认为很少有人能不经重构,一次就能写出整洁的代码。大师也是在不断地迭代和重构中完成的。
当我想增加一个notificaionType的枚举时,需要在多处修改代码。
public static NotificationType fromInt(int value) 方法要增加判断
ForegroundHandler.public boolean canHandle(NotificationType dataType)要修改代码
CommonHandler.public boolean canHandle(NotificationType dataType)要修改代码
MainActivity.private void switchActivity()要修改代码
重构后的代码:
- 在NotificationType类增加了一个实例方法,public boolean isNeedHandle(),那么ForegroundHandler和CommonHandler的canHandle的实现就可以通过调用参数的isNeedHandle方法处理。
- 在NotificationType.public static NotificationType fromInt(int value)的实现里,我通过遍历values,如果找到值等于参数值,就返回该枚举。 所以以后增加一个推送类型是只要创建一个枚举实例,且修改MainActivity的switch/case就可以了。