代码整洁之道
1、简介
阅读本书的两个原因,1:你是个程序员;2:你想成为一个更好的程序员。我们都曾瞟一眼自己亲手造成的混乱,决定弃之于不顾。我们都曾看到自己的蓝程序居然能运行,然后断言能运行的烂程序总比什么都没有强。我们都曾说过有朝一日再不回头清理。当然,凡此种种,皆因我们没听过勒布朗法则:Later equals never。
将源自 Robert C. Martin 的 Clean Code的软件工程原则适配到 Java 。 这不是一个代码风格指南, 而是一个使用 Java 来生产 可读的, 可重用的, 以及可重构的软件的指南。中的每一项原则都不是强制遵守的, 甚至只有更少的能够被广泛认可。 这些仅仅是指南而已, 但是却是 Clean Code 作者多年经验的结晶。
当然知道这些指南并不能马上让你成为一个更加出色的软件开发者, 并且使用它们工作多年也并不意味着你不再会犯错误。 每一段代码最开始都是草稿, 像湿粘土一样被打造成最终的形态。 最后当我们和搭档们一起审查代码时清除那些不完善之处, 不要因为最初需要改善的草稿代码而自责, 而是对那些代码下手。
2、命名
使用有意义并且可读的变量名称
不好的:
String yyyymmdstr = new SimpleDateFormat("YYYY/MM/DD").format(new Date());
好的:
String currentDate = new SimpleDateFormat("YYYY/MM/DD").format(new Date());
避免使用误导性变量名称
不好的:
public class Car{
private String p_name;
}
好的:
public class Car{
String name;
}
为相同类型的变量使用相同的词汇
不好的:
getUserInfo();
getClientData();
getCustomerRecord();
好的:
getUser();
使用可搜索的名称
我们要阅读的代码比要写的代码多得多, 所以我们写出的代码的可读性和可搜索性是很重要的。 使用没有意义的变量名将会导致我们的程序难于理解, 将会伤害我们的读者, 所以请使用可搜索的变量名。
不好的:
setTimeout(blastOff, 86400000);
好的:
public static final int MILLISECONDS_IN_A_DAY = 86400000;
setTimeout(blastOff, MILLISECONDS_IN_A_DAY);
使用解释性的变量
不好的:
String address = "One Infinite Loop, Cupertino 95014"; String cityZipCodeRegex = "/^[^,\\\\]+[,\\\\\\s]+(.+?)\\s*(\\d{5})?$/"; saveCityZipCode(address.split(cityZipCodeRegex)[0], address.split(cityZipCodeRegex)[1]);
好的:
String address = "One Infinite Loop, Cupertino 95014"; String cityZipCodeRegex = "/^[^,\\\\]+[,\\\\\\s]+(.+?)\\s*(\\d{5})?$/"; String city = address.split(cityZipCodeRegex)[0]; String zipCode = address.split(cityZipCodeRegex)[1]; saveCityZipCode(city, zipCode);
避免心理映射
显示比隐式更好
不好的:
String [] l = {"Austin", "New York", "San Francisco"}; for (int i = 0; i < l.length; i++) { String li = l[i]; doStuff(); doSomeOtherStuff();
好的:
String[] locations = {"Austin", "New York", "San Francisco"}; for (String location : locations) { doStuff(); doSomeOtherStuff();
避免添加不必要的上下文
如果你的类名/对象名有意义, 不要在变量名上再重复。
不好的:
class Car { public String carMake = "Honda"; public String carModel = "Accord"; public String carColor = "Blue"; } void paintCar(Car car) { car.carColor = "Red"; }
好的:
class Car { public String make = "Honda"; public String model = "Accord"; public String color = "Blue"; } void paintCar(Car car) { car.color = "Red"; }
3、函数
函数应当只做一件事情
这是软件工程中最重要的一条规则, 当函数需要做更多的事情时, 它们将会更难进行编写、 测试和推理。 当你能将一个函数隔离到只有一个动作, 他们将能够被容易的进行重构并且你的代码将会更容易阅读。 如 果你严格遵守本指南中的这一条, 你将会领先于许多开发者。
不好的:
public void emailClients(List<Client> clients) { for (Client client : clients) { Client clientRecord = repository.findOne(client.getId()); if (clientRecord.isActive()){ email(client); } } }
好的:
public void emailClients(List<Client> clients) { for (Client client : clients) { if (isActiveClient(client)) { email(client); } } } private boolean isActiveClient(Client client) { Client clientRecord = repository.findOne(client.getId()); return clientRecord.isActive(); }
函数名称应该说明它要做什么
不好的:
private void addToDate(Date date, int month){
好的:
private void addMonthToDate(Date date, int month){
函数应该只有一个抽象级别
当在你的函数中有多于一个抽象级别时, 你的函数通常做了太多事情。 拆分函数将会提升重用性和测试性。
不好的:
void parseBetterJSAlternative(String code){ String[] REGECES={}; String[] statements=code.split(" "); String[] tokens={}; for(String regex: Arrays.asList(REGECES)){ for(String statement:Arrays.asList(statements)){
好的:
String[] tokenize(String code){ String[] REGECES={}; String[] statements=code.split(" "); String[] tokens={}; for(String regex: Arrays.asList(REGECES)){ for(String statement:Arrays.asList(statements)){
函数参数 (两个以下最理想)
限制函数参数的个数是非常重要的, 因为这样将使你的函数容易进行测试。 一旦超过三个参数将会导致组合爆炸, 因为你不得不编写大量针对每个参数的测试用例。
没有参数是最理想的, 一个或者两个参数也是可以的, 三个参数应该避免, 超过三个应该被重构。 通常, 如果你有一个超过两个函数的参数, 那就意味着你的函数尝试做太多的事情。 如果不是, 多数情况下一个 更高级对象可能会满足需求。
当你发现你自己需要大量的参数时, 你可以使用一个对象。
不好的:
void createMenu(String title,String body,String buttonText,boolean cancellable){}
好的:
class MenuConfig{ String title; String body; String buttonText; boolean cancellable; } void createMenu(MenuConfig menuConfig){}
移除冗余代码
竭尽你的全力去避免冗余代码。 冗余代码是不好的, 因为它意味着当你需要修改一些逻辑时会有多个地方 需要修改。
想象一下你在经营一家餐馆, 你需要记录所有的库存西红柿, 洋葱, 大蒜, 各种香料等等。 如果你有多 个记录列表, 当你用西红柿做一道菜时你得更新多个列表。 如果你只有一个列表, 就只有一个地方需要更 新!
你有冗余代码通常是因为你有两个或多个稍微不同的东西, 它们共享大部分, 但是它们的不同之处迫使你使 用两个或更多独立的函数来处理大部分相同的东西。 移除冗余代码意味着创建一个可以处理这些不同之处的 抽象的函数/模块/类。
让这个抽象正确是关键的, 这是为什么要你遵循 Classes 那一章的 SOLID 的原因。 不好的抽象比冗 余代码更差, 所以要谨慎行事。 既然已经这么说了, 如果你能够做出一个好的抽象, 才去做。 不要重复 你自己, 否则你会发现当你要修改一个东西时时刻需要修改多个地方。
不好的:
void showDeveloperList(List<Developer> developers){ for(Developer developer:developers){ render(new Data(developer.expectedSalary,developer.experience,developer.githubLink)); } } void showManagerrList(List<Manager> managers){ for(Manager manager:managers){ render(new Data(manager.expectedSalary,manager.experience,manager.portfolio)); } }
好的:
void showList(List<Employee> employees){ for(Employee employee:employees){ Data data=new Data(employee.expectedSalary,employee.experience,employee.githubLink); String portfolio=employee.portfolio; if("manager".equals(employee)){ portfolio=employee.portfolio; } data.portfolio=portfolio; render(data); } }
不要使用标记位做为函数参数
标记位是告诉你的用户这个函数做了不只一件事情。 函数应该只做一件事情。 如果你的函数因为一个布尔值 出现不同的代码路径, 请拆分它们。
不好的:
void createFile(String name,boolean temp){ if(temp){ new File("./temp"+name); }else{ new File(name); } }
好的:
void createFile(String name){ new File(name); }void createTempFile(String name){ new File("./temp"+name); }
避免副作用
如果一个函数做了除接受一个值然后返回一个值或多个值之外的任何事情, 它将会产生副作用, 它可能是 写入一个文件, 修改一个全局变量, 或者意外的把你所有的钱连接到一个陌生人那里。
现在在你的程序中确实偶尔需要副作用, 就像上面的代码, 你也许需要写入到一个文件, 你需要做的是集 中化你要做的事情, 不要让多个函数或者类写入一个特定的文件, 用一个服务来实现它, 一个并且只有一 个。
重点是避免这些常见的易犯的错误, 比如在对象之间共享状态而不使用任何结构, 使用任何地方都可以写入 的可变的数据类型, 没有集中化导致副作用。 如果你能做到这些, 那么你将会比其它的码农大军更加幸福。
不好的:
String name="Ryan McDermott"; void splitIntoFirstAndLastName(){ name=name.split(" ").toString(); } splitIntoFirstAndLastName(); System.out.println(name);
好的:
String name="Ryan McDermott"; String splitIntoFirstAndLastName(){ return name.split(" ").toString(); } String newName=splitIntoFirstAndLastName(); System.out.println(name); System.out.println(newName);
函数式编程优于指令式编程
函数式语言更加简洁 并且更容易进行测试, 当你可以使用函数式编程风格时请尽情使用。
不好的:
List<Integer> programmerOutput=new ArrayList<>(); programmerOutput.add(500); programmerOutput.add(1500); programmerOutput.add(150); programmerOutput.add(1000); int totalOutput=0; for(int i=0;i<programmerOutput.size();i++){ totalOutput+=programmerOutput.get(i); }
好的:
List<Integer> programmerOutput=new ArrayList<>(); programmerOutput.add(500); programmerOutput.add(1500); programmerOutput.add(150); programmerOutput.add(1000); int totalOutput= programmerOutput.stream().filter(programmer -> programmer > 500).mapToInt(programmer -> programmer).sum();
函数分隔指令和询问
函数要么做什么事,要么回答什么事,但二者常常混淆在一起。函数应该修改某对象的状态,或者返回对象的相关信息。要把指令和询问分隔开,防止混淆的发生。
不好的:
public boolean set(String key,String value);if(set("username","fuHeng"))...
好的:
if(key.contains("username")){ set("username","fuHeng");}
封装条件语句
不好的:
if(fsm.state.equals("fetching")&&listNode.isEmpty(){
好的:
void shouldShowSpinner(Fsm fsm, String listNode) { return fsm.state.equals("fetching")&&listNode.isEmpty(); } if (shouldShowSpinner(fsmInstance, listNodeInstance)) {
避免负面条件
不好的:
void isDOMNodeNotPresent(Node node) {
好的:
void isDOMNodePresent(Node node) {
避免条件语句
这看起来似乎是一个不可能的任务。 第一次听到这个时, 多数人会说: “没有 if 语句还能期望我干 啥呢”, 答案是多数情况下你可以使用多态来完成同样的任务。 第二个问题通常是 “好了, 那么做很棒, 但是我为什么想要那样做呢”, 答案是我们学到的上一条代码整洁之道的理念: 一个函数应当只做一件事情。 当你有使用 if 语句的类/函数是, 你在告诉你的用户你的函数做了不止一件事情。 记住: 只做一件 事情。
不好的:
class Airplane{ int getCurisingAltitude(){ switch(this.type){ case "777": return this.getMaxAltitude()-this.getPassengerCount(); case "Air Force One": return this.getMaxAltitude(); case "Cessna": return this.getMaxAltitude() - this.getFuelExpenditure(); } } }
好的:
class Airplane {
移除僵尸代码
僵死代码和冗余代码同样糟糕。 没有理由在代码库中保存它。 如果它不会被调用, 就删掉它。 当你需要 它时, 它依然保存在版本历史记录中。
不好的:
void oldRequestModule(String url) {
好的:
void newRequestModule(String url) {
4、注释
注释的恰当用法是弥补我们在用代码表达意图时遭遇的失败。每次用代码表达,你都应该夸奖一下自己;每次写注释,你都应该做个鬼脸,感受自己在表达能力上的失败。
- 注释不能也不能掩饰代码的糟糕
- 能用代码阐明清楚就不要写注释
- 好的注释:
- 法律信息
- 阐释意图
- 部分警示
- TODO(需要定期清理)
- 公共API的JavaDoc
- 坏的注释:
- 误导性、多余、自话自语、循环式、归属和署名
- 日志式(删除)
- 注释代码
仅仅对包含复杂业务逻辑的东西进行注释
注释是代码的辩解, 不是要求。 多数情况下, 好的代码就是文档。
不好的:
void hashIt(String data) {
好的:
void hashIt(String data) { long hash = 0; int length = data.length(); for (int i = 0; i < length; i++) { char mchar = data.charAt(i); hash = ((hash << 5) - hash) + mchar;
不要在代码库中保存注释掉的代码
因为有版本控制, 把旧的代码留在历史记录即可。
不好的:
doStuff();
好的:
doStuff();
不要有日志式的注释
记住, 使用版本控制! 不需要僵尸代码, 注释掉的代码, 尤其是日志式的注释。 使用 git log 来 获取历史记录。
不好的:
void combine(String a, String b) { return a + b; }
好的:
void combine(String a, String b) { return a + b; }
避免占位符
它们仅仅添加了干扰。 让函数和变量名称与合适的缩进和格式化为你的代码提供视觉结构。
不好的:
好的:
String[] model = {"foo","bar"}; void action(){
5、格式
格式化是主观的。 就像其它规则一样, 没有必须让你遵守的硬性规则。 重点是不要因为格式去争论, 这 里有大量的工具来自动格式化, 使用其中的一个即可! 因为做为工程师去争论格式化就是在浪费时间和金钱。
针对自动格式化工具不能涵盖的问题(缩进、 制表符还是空格、 双引号还是单引号等), 这里有一些指南。
使用一致的大小写
大小写告诉你关于你的变量、 函数等的很多事情。 这些规则是主观的, 所以你的团队可以选择他们想要的。 重点是, 不管你们选择了什么, 要保持一致。
不好的:
int DAYS_IN_WEEK = 7; int daysInMonth = 30; String[] songs = {"Back In Black", "Stairway to Heaven", "Hey Jude"}; String[] Artists = {"ACDC", "Led Zeppelin", "The Beatles"}; void eraseDatabase() {} void restore_database() {} class animal {} class Alpaca {}
好的:
int DAYS_IN_WEEK = 7; int DAYS_IN_MONTH = 30; String[] songs = {"Back In Black", "Stairway to Heaven", "Hey Jude"}; String[] artists = {"ACDC", "Led Zeppelin", "The Beatles"}; void eraseDatabase() {} void restoreDatabase() {} class Animal {} class Alpaca {}
函数的调用方与被调用方应该靠近
如果一个函数调用另一个, 则在代码中这两个函数的竖直位置应该靠近。 理想情况下,保持被调用函数在被 调用函数的正上方。 我们倾向于从上到下阅读代码, 就像读一章报纸。 由于这个原因,保持你的代码可以按照这种方式阅读。
不好的:
class PerformanceReview { String employee; public PerformanceReview(String employee) { this.employee = employee; } String lookupPeers() { return db.lookup(this.employee, "peers"); } String lookupManager() { return db.lookup(this.employee, "manager"); } void getPeerReviews() { String peers = this.lookupPeers();
好的:
class PerformanceReview { String employee; public PerformanceReview(String employee) { this.employee = employee; } void perfReview() { this.getPeerReviews(); this.getManagerReview(); this.getSelfReview(); } void getPeerReviews() { String peers = this.lookupPeers();
6、对象和数据结构
数据抽象,隐藏实现并不是在变量之间放一个函数层。它关乎抽象,类并不是简单地用取值器和赋值器将其变量推向外面,而是暴露抽象接口,以便用户无需了解数据的实现就能操作数据本体。
使用 getters 和 setters
使用 getters 和 setters 来访问对象上的数据比简单的在一个对象上查找属性 要好得多。 “为什么?” 你可能会问, 好吧, 原因请看下面的列表:
- 当你想在获取一个对象属性的背后做更多的事情时, 你不需要在代码库中查找和修改每一处访问;
- 使用
set 可以让添加验证变得容易; - 封装内部实现;
- 使用 getting 和 setting 时, 容易添加日志和错误处理;
- 继承这个类, 你可以重写默认功能;
- 你可以延迟加载对象的属性, 比如说从服务器获取。
不好的:
class BankAccount{ public int balance=1000; } BankAccount bankAccount=new BankAccount(); bankAccount.balance-=100;
好的:
class BankAccount{ private int blance=1000; public int getBlance() { return blance; } public void setBlance(int blance) { if(verifyIfAmountCanBeSetted(blance)){ this.blance = blance; } } void verifyIfAmountCanBeSetted(int amount){
7、错误处理
抛出错误是一件好事情! 他们意味着当你的程序有错时运行时可以成功确认,并且通过停止执行当前堆栈 上的函数来让你知道,结束当前进程(在Node中), 在控制台中用一个堆栈跟踪提示你。
不要忽略捕捉到的错误
对捕捉到的错误不做任何处理不能给你修复错误或者响应错误的能力。 向控制台记录错误 (console.log ) 也不怎么好, 因为往往会丢失在海量的控制台输出中。 如果你把任意一段代码用 try/catch 包装那就 意味着你想到这里可能会错, 因此你应该有个修复计划, 或者当错误发生时有一个代码路径。
不好的:
try { functionThatMightThrow(); } catch (Exception error) { console.log(error); }
好的:
try { functionThatMightThrow(); } catch (Exception error) {
8、边界
第三方程序包或开源代码,我们需要注意边界问题,将外来代码干净利落地整合进自己的代码。
9、单元测试
测试比发布更加重要。 如果你没有测试或者测试不够充分, 每次发布时你就不能确认没有破坏任何事情。 测试的量由你的团队决定, 但是拥有 100% 的覆盖率(包括所有的语句和分支)是你为什么能达到高度自信 和内心的平静。 这意味着需要一个额外的伟大的测试框架, 也需要一个好的覆盖率工具。
没有理由不写测试。 当为团队选择了测试框架之后, 接下来的目标是为生产的每一个新的功能/模 块编写测试。 如果你倾向于测试驱动开发(TDD), 那就太棒了, 但是要点是确认你在上线任何功能或者重 构一个现有功能之前,达到了需要的目标覆盖率。
一个测试一个概念
不好的:
void testMakeMomentJSGreatAgain(){ Date date; date = new MakeMomentJSGreatAgain("1/1/2021"); date.addDays(30); Assert.equal(date.getString(),"1/31/2021"). date = new MakeMomentJSGreatAgain("2/1/2021"); date.addDays(28); Assert.equal(date.getString(),"02/29/2021"); date = new MakeMomentJSGreatAgain("2/1/2021"); date.addDays(28); Assert.equal(date.getString(),"03/01/2021"); }
好的:
void testThirtyDayMonths(){ Date date = new MakeMomentJSGreatAgain("1/1/2021"); date.addDays(30); Assert.equal(date.getString(),"1/31/2021"); } void testLeapYear(){ Date date = new MakeMomentJSGreatAgain("2/1/2021"); date.addDays(28); Assert.equal(date.getString(),"02/29/2021"); } void testNonLeapYear(){ Date date = new MakeMomentJSGreatAgain("2/1/2021"); date.addDays(28); Assert.equal(date.getString(),"03/01/2021"); }
整洁测试的五条规则
- 快速(Fast)
- 独立(Independent)
- 可重复(Repeatable)
- 自足验证(Self-Validating)
- 及时(Timely)
10、类
使用方法链
这个模式在 Java 中是非常有用的, 并且你可以在许多类库比如 Glide 和 OkHttp 中见到。 它使你的代码变得富有表现力, 并减少啰嗦。 因为这个原因, 我说, 使用方法链然后再看看你的代码 会变得多么简洁。 在你的类/方法中, 简单的在每个方法的最后返回 this , 然后你就能把这个类的 其它方法链在一起。
不好的:
class Car{ private String make; private String model; private String color; public void setMake(String make) { this.make = make; } public void setModel(String model) { this.model = model; } public void setColor(String color) { this.color = color; } public void save(){ console.log(this.make, this.model, this.color); } } Car car=new Car(); car.setColor("pink"); car.setMake("Ford"); car.setModel("F-150"); car.save();
好的:
class Car{ private String make; private String model; private String color; public Car setMake(String make) { this.make = make; return this; } public Car setModel(String model) { this.model = model; return this; } public Car setColor(String color) { this.color = color; return this; } public Car save(){ console.log(this.make, this.model, this.color); return this; } } Car car=new Car() .setColor("pink") .setMake("Ford") .setModel("F-150") .save();
组合优先于继承
正如设计模式四人帮所述, 如果可能, 你应该优先使用组合而不是继承。 有许多好的理由去使用继承, 也有许多好的理由去使用组合。这个格言 的重点是, 如果你本能的观点是继承, 那么请想一下组合能否更好的为你的问题建模。 很多情况下它真的 可以。
那么你也许会这样想, “我什么时候改使用继承?” 这取决于你手上的问题, 不过这儿有一个像样的列表说 明什么时候继承比组合更好用:
- 你的继承表示"是一个"的关系而不是"有一个"的关系(人类->动物 vs 用户->用户详情);
- 你可以重用来自基类的代码(人可以像所有动物一样行动);
- 你想通过基类对子类进行全局的修改(改变所有动物行动时的热量消耗);
不好的:
class Employee{ private String name; private String email; }
好的:
class EmployeeTaxData{ private String ssn; private String salary; public EmployeeTaxData(String ssn, String salary) { this.ssn = ssn; this.salary = salary; } }class Employee{ private String name; private String email; private EmployeeTaxData taxData; void setTaxData(String ssn,String salary){ this.taxData=new EmployeeTaxData(ssn,salary); } }
10.5、SOLID
单一职责原则 (SRP)
正如代码整洁之道所述, “永远不要有超过一个理由来修改一个类”。 给一个类塞满许多功能, 就像你在航 班上只能带一个行李箱一样, 这样做的问题你的类不会有理想的内聚性, 将会有太多的理由来对它进行修改。 最小化需要修改一个类的次数时很重要的, 因为如果一个类拥有太多的功能, 一旦你修改它的一小部分, 将会很难弄清楚会对代码库中的其它模块造成什么影响。
不好的:
class UserSettings { User user; void changeSettings(UserSettings settings) { if (this.verifyCredentials()) {
好的:
User user; UserAuth auth; public UserSettings(User user) { this.user = user; this.auth = new UserAuth(user); } void changeSettings(UserSettings settings) { if (this.auth.verifyCredentials()) {
开闭原则 (OCP)
Bertrand Meyer 说过, “软件实体 (类, 模块, 函数等) 应该为扩展开放, 但是为修改关闭。” 这 是什么意思呢? 这个原则基本上说明了你应该允许用户添加功能而不必修改现有的代码。
不好的:
class AjaxAdapter extends Adapter { private String name; public AjaxAdapter() { this.name = "ajaxAdapter"; } } class NodeAdapter extends Adapter { private String name; public NodeAdapter() { this.name = "nodeAdapter"; } } class HttpRequester { public HttpRequester(Adapter adapter) { this.adapter = adapter; } void fetch(String url) { if ("ajaxAdapter".equals(this.adapter.name)) { makeAjaxCall(url); } else if ("httpNodeAdapter".equals(this.adapter.name)) { makeHttpCall(url); } } } void makeAjaxCall(String url) {
好的:
class AjaxAdapter extends Adapter { private String name; public AjaxAdapter() { this.name = "ajaxAdapter"; } void request(String url){ } } class NodeAdapter extends Adapter { private String name; public NodeAdapter() { this.name = "nodeAdapter"; } void request(String url){ } } class HttpRequester { public HttpRequester(Adapter adapter) { this.adapter = adapter; } void fetch(String url) { this.adapter.request(url); } }
里氏代换原则 (LSP)
这是针对一个非常简单的里面的一个恐怖意图, 它的正式定义是: “如果 S 是 T 的一个子类型, 那么类 型为 T 的对象可以被类型为 S 的对象替换(例如, 类型为 S 的对象可作为类型为 T 的替代品)儿不需 要修改目标程序的期望性质 (正确性、 任务执行性等)。” 这甚至是个恐怖的定义。
最好的解释是, 如果你又一个基类和一个子类, 那个基类和字类可以互换而不会产生不正确的结果。 这可 能还有有些疑惑, 让我们来看一下这个经典的正方形与矩形的例子。 从数学上说, 一个正方形是一个矩形, 但是你用 “is-a” 的关系用继承来实现, 你将很快遇到麻烦。
不好的:
class Rectangle { protected int width; protected int height; public Rectangle() { this.width = 0; this.height = 0; } void setColor(String color) {
好的:
class Shape { void setColor(String color) {
接口隔离原则 (ISP)
接口隔离原则说的是 “客户端不应该强制依赖他们不需要的接口。”
不好的:
interface I { public void method1(); public void method2(); public void method3(); public void method4(); public void method5(); } class A{ public void depend1(I i){ i.method1(); } public void depend2(I i){ i.method2(); } public void depend3(I i){ i.method3(); } } class B{ public void depend1(I i){ i.method1(); } public void depend2(I i){ i.method4(); } public void depend3(I i){ i.method5(); } } class C implements I{ public void method1() { System.out.println("类B实现接口I的方法1"); } public void method2() { System.out.println("类B实现接口I的方法2"); } public void method3() { System.out.println("类B实现接口I的方法3"); }
好的:
interface I1 { public void method1(); } interface I2 { public void method2(); public void method3(); } interface I3 { public void method4(); public void method5(); } class A{ public void depend1(I1 i){ i.method1(); } public void depend2(I2 i){ i.method2(); } public void depend3(I2 i){ i.method3(); } } class B{ public void depend1(I1 i){ i.method1(); } public void depend2(I3 i){ i.method4(); } public void depend3(I3 i){ i.method5(); } } class C implements I1, I2{ public void method1() { System.out.println("类B实现接口I1的方法1"); } public void method2() { System.out.println("类B实现接口I2的方法2"); } public void method3() { System.out.println("类B实现接口I2的方法3"); } } class D implements I1, I3{ public void method1() { System.out.println("类D实现接口I1的方法1"); } public void method4() { System.out.println("类D实现接口I3的方法4"); } public void method5() { System.out.println("类D实现接口I3的方法5"); }
依赖反转原则 (DIP)
这个原则阐述了两个重要的事情:
- 高级模块不应该依赖于低级模块, 两者都应该依赖与抽象;
- 抽象不应当依赖于具体实现, 具体实现应当依赖于抽象。
这个一开始会很难理解, 但是如果你使用过 Angular.js , 你应该已经看到过通过依赖注入来实现的这 个原则, 虽然他们不是相同的概念, 依赖反转原则让高级模块远离低级模块的细节和创建, 可以通过 DI 来实现。 这样做的巨大益处是降低模块间的耦合。 耦合是一个非常糟糕的开发模式, 因为会导致代码难于 重构。
如上所述, JavaScript 没有接口, 所以被依赖的抽象是隐式契约。 也就是说, 一个对象/类的方法和 属性直接暴露给另外一个对象/类。 在下面的例子中, 任何一个 Request 模块的隐式契约 InventoryTracker 将有一个 requestItems 方法。
不好的:
class InventoryRequester { private String REQ_METHODS; public InventoryRequester() { this.REQ_METHODS = "HTTP"; } void requestItem(String item) {
好的:
interface Requester{ void requestItem(String item); } class InventoryTracker { List<String> items; Requester requester; public InventoryTracker(List<String> items, Requester requester) { this.items = items; this.requester = requester; } void requestItems() { this.items.stream().forEach(item->requester.requestItem(item)); } } class InventoryRequesterV1 implements Requester{ String REQ_METHODS; public InventoryRequesterV1() { this.REQ_METHODS="HTTP"; } @Override public void requestItem(String item) {
11、系统
侵害性架构湮灭领域逻辑,冲击敏捷能力。当领域逻辑受到困扰,质量也就堪忧,因为缺陷更容易隐藏,用户需求更难实现。当敏捷能力收到损害时,生产力也会降低TDD的好处遗失殆尽。
12、迭进
Kent Beck关于简单设计的四条规则:
- 运行所有测试
- 不可重复
- 表达程序员的意图
- 尽可能减少类和方法的数量
- 以上规则按照重要程度排列
13、并发编程
并发第一要诀是遵循单一职责原则。将系统切分为线程相关和无关的POJO。
14-16、改进实例
Args逐步改进、Junit内幕、重构SerialDate
17、启发与味道
这一小节是对全书提纲挈领的作用,仅仅看一下标题,都可以学到很多东西。
- 注释
- 不恰当的信息
- 废弃的注释
- 冗余注释
- 糟糕的注释
- 注释掉的代码
- 环境
- 函数
- 一般性问题
- 一个源文件中存在多种语言
- 明显的行为未被实现
- 不正确的边界行为
- 忽视安全
- 重复
- 在错误的抽象层次上的代码
- 基类依赖于派生类
- 信息过多
- 死代码
- 垂直分隔
- 前后不一致
- 混淆视听
- 人为耦合
- 特性依恋
- 选择算子参数
- 晦涩的意图
- 位置错误的权责
- 不恰当的静态方法
- 使用解释性的变量
- 函数名称应该表达其行为
- 理解算法
- 把逻辑依赖改为物理依赖
- 用多态替代if/else或switch/case
- 遵循标准约定
- 用命名常量替代魔术数
- 准确
- 结构甚于约定
- 封装条件
- 避免否定性条件
- 函数只该做一件事
- 掩蔽时序耦合
- 别随意
- 封装边界条件
- 函数应该只在一个抽象层级上
- 在较高层级放置可配置数据
- 避免传递浏览
- JAVA
- 通过使用通配符避免过长的导入清单
- 不要继承常量
- 常量 VS枚举
- 名称
- 采用描述性名称
- 名称应与抽象层级相符
- 尽可能使用标准命名法
- 无歧义的名称
- 为较大作用范围选用较长名称
- 避免编码
- 名称应该说明副作用
- 测试
- 测试不足
- 使用覆盖率工具
- 别略过小测试
- 被忽略的测试就是对不确定事务的疑问
- 注意边界条件
- 全面测试相近的缺陷
- 测试失败的模式有启发性
- 测试覆盖率的模式有启发性
- 测试应该快速
|