6.5不要重复自己
《奔跑吧,程序员》章节:6.5不要重复自己,宠文网网友提供全文无弹窗免费在线阅读。!
系统中的每一项知识都必须具有单一、无歧义、权威的表示。
——Andrew Hunt、David Thomas,《程序员修炼之道》
避免重复是实现整洁代码最根本的原则之一。有点讽刺的是,避免重复的思想不断以不同的名称重复出现,比如不要重复自己(DRY)、单点真理、一次并且只有一次。重复可能出现在技术实现的任何一个地方,包括架构、代码、测试、过程、需求和文档,它可能是以下几个原因引起的。
· 我们需要用多种方式表示相同的信息,比如在数据库模式、数据库访问层、HTML标记和CSS中列出相同的列。
· 语言限制:比如在Java中指定getter和setter。
· 缺少反规范化:比如从数据库的表中导出数据。
· 缺少时间,导致需要复制和粘贴代码。
· 没有意识到这个问题,比如多个开发人员在一个大型代码库中创建自己的StringUtil类,因为他们不知道类似的类已经存在(也因为他们不知道开源库甚至有更好的版本)。
重复不仅因为要多次实现相同的事情而浪费了时间,而且还妨碍了我们对代码的理解和维护。如果代码不DRY,那么我们每需要回答一个有关代码问题,就可能必须去查看多个地方;我们每需要做修改,就必须确保不会错过任何一个副本;如果有一个副本不同步,就会导致矛盾和bug的出现。
如果发现自己在一次次地编写相同的代码,或者只是做一小点改变就会涉及代码库中的一半内容,那么就要想想办法让代码变得更加DRY。特别当我们必须一次次地重复相同的过程,那么就需要实现一种自动化的过程;如果不只在一个地方有相同的逻辑,那么就需要实现抽象,以便共享单一的实现。
举个例子,来看看BookParser代码是如何构造JSON的:
String csvLine, json = "["; while ((csvLine = csvReader.readLine()) != null) { // ... json += "{"; json += "title:\"" + title + "\","; json += "author:\"" + author + "\","; json += "pages:\"" + pages + "\","; json += "category:\"" + category + "\""; json += "},"; } json += "]";
这里有很多重复:将JSON元素放在括号([] 或 {})中的代码出现了多次,在JSON对象中创建键-值项的代码也出现了多次。所有这些重复导致了几个bug,你发现了吗?其中一个是经典的复制 / 粘贴错误,pages变量是一个Integer,在插入JSON的时候不应该放在引号中:
json += "pages:\"" + pages + "\",";
第二个bug是JSON数组最后一个元素的后面多了一个逗号:
json += "},";
我们可以修复这些bug,但最大的重复仍然没有去掉:JSON和CSV是通用的数据格式,我们没理由从头编写代码去处理它们。重新发明轮子是最常见和不必要的重复,只要有可能,就应该使用开源库去代替(阅读5.3节了解更多信息)。例如,可以使用Java的Jackson库,让代码更加DRY和稳定。因为Java是基于类的面向对象语言,表示数据的规范做法就是创建一个类:
public class Book { private String title; private String author; private int pages; private Category category; // (省略了构造函数和getter方法) }
可以构造出Book对象的一个List,代替手动生成JSON的String:
List books = new ArrayList<>(); while ((csvLine = csvReader.readLine()) != null) { csvFields = csvLine.split(","); String title = csvFields[TITLE.ordinal()]; String author = csvFields[AUTHOR.ordinal()]; Integer pages = Integer.parseInt(csvFields[PAGES.ordinal()]); Category category = Category.valueOf(csvFields[CATEGORY.ordinal()]); books.add(new Book(title, author, pages, category)); }
Jackson库可以使用类中的字段名作为JSON中的键,将大部分的Java类转换为等效的JSON表示。使用Jackson的ObjectMapper类,我们只需要两行代码就可以将上面的Book对象的List转换到JSON文件中:
ObjectMapper mapper = new ObjectMapper(); mapper.writeValue(outputJson, books);
与之类似,也可以使用Apache Commons CSV库简化CSV的解析过程。CSVParser类可以使用parse方法读取一个CSV文件并用withHeader方法把标签赋给每一列:
List records = CSVFormat .DEFAULT .withHeader(TITLE.name(), AUTHOR.name(), PAGES.name(), CATEGORY.name()) .parse(new FileReader(inputCsv)) .getRecords();
我们现在不用手动敲逗号去分隔每一行,也不用为列的下标烦恼。我们可以遍历从CSVParser中得到的记录,按照名称读取每一列的内容:
for (CSVRecord record : records) { String title = record.get(TITLE); String author = record.get(AUTHOR); Integer pages = Integer.parseInt(record.get(PAGES)); Category category = Category.valueOf(record.get(CATEGORY)); books.add(new Book(title, author, pages, category)); }
我们不再需要手动编写有很多bug的代码,而是利用流行的、久经考验的开源库。这样代码会更短,看起来更像惯用的Java,bug更少,重复也更少。下面是整个convertCsvToJson函数:
public void convertCsvToJson(File inputCsv, File outputJson) throws IOException { List books = new ArrayList<>(); List records = CSVFormat .DEFAULT .withHeader(TITLE.name(), AUTHOR.name(), PAGES.name(), CATEGORY.name()) .parse(new FileReader(inputCsv)) .getRecords(); for (CSVRecord record : records) { String title = record.get(TITLE); String author = record.get(AUTHOR); Integer pages = Integer.parseInt(record.get(PAGES)); Category category = Category.valueOf(record.get(CATEGORY)); books.add(new Book(title, author, pages, category)); } ObjectMapper mapper = new ObjectMapper(); mapper.writeValue(outputJson, books); }