通过1000个代码重构,学习到的知识点

通过1000个代码重构,学习到的知识点 (What I learned from doing 1000 code reviews)(翻译)

在LinkedIn工作的时候,我大部分的工作都是做代码检查。有一些建议不断地反复出现,所以我决定把我和团队分享的清单放在一起。

建议 1: 出错时请抛(throw)出一个异常(exception)

我看到的一个常见的模式是:

List<String> getSearchResults(...) {
  try {
    List<String> results = // make REST call to search service
    return results;
  } catch (RemoteInvocationException e) {
    return Collections.emptyList();
  }
}

这种模式实际上造成了我开发过的移动应用程序的中断。

我们使用的搜索后端开始抛出异常。然而,应用程序的API服务器有一些类似于上面的代码。因此,从应用程序的角度来看,它获得了200的成功响应,并愉快地显示了空列表。

相反,如果API抛出了一个异常,那么我们的监控系统就会立即将其提取出来,并修复。

很多时候,当你遇到异常时,为了便利返回空对象。在java中空对象的例子是Optional.empty(),null,empty list。在解析URL时总是出现这样的问题。如果无法从字符串中解析URL,返回值为null。可以自问:“为什么URL是错误的?”我们应该在上游解决的数据问题吗?“

空对象不是处理这项工作的合适工具。如果发生异常,则应抛出异常

建议2:尽可能使用具体的类型(type)

这个建议基本上跟stringly typed programming(强类型编程)是相反的。

我经常看到类似这些例子的代码。

void doOperation(String opType, Data data); 
// where opType is "insert", "append", or "delete", this should have clearly been an enum

String fetchWebsite(String url);
// where url is "https://google.com", this should have been an URN

String parseId(Input input);
// the return type is String but ids are actually Longs like "6345789"

用具体的类型可以让你避免一类错误,也是推荐java,这种强类型语言的理由。

现在的问题是:如何成为终结“写坏的强类型代码(stringly typed code)”的程序员?。答案是:因为外部不使用强类型。有很多的方式接受字符串,比如:

  • query and path parameters in urls
  • JSON
  • databases that don’t support enums
  • poorly written libraries(写的不好的libraries)

在这些情况下,你应该使用以下策略来避免这个问题:

将字符串解析和序列化保持在程序的上下文中。

下面举个例子:

// Step 1: Take a query param representing a company name / member id pair and parse it
// example: context=Pair(linkedin,456)
Pair<String, Long> companyMember = parseQueryParam("context");
// this should throw an exception if malformed

// Step 2: Do all the stuff in your application
// MOST if not all of your code should live in this area

// Step 3: Convert the parameter back into a String at the very end if necessary
String redirectLink = serializeQueryParam("context");

这赋予了许多优点。可以发现错误的数据。如果出现问题,应用程序会提前报错。在数据经过一次验证之后,你也不必捕获解析异常。此外,使用强类型会展现更多方法的叙述,你不需要为每一个方法写javadocs。

建议3:请用Optionals代替null

java8中提供Optional类。Optional类的Javadoc描述如下:

这是一个可以为null的容器对象。如果值存在则isPresent()方法会返回true,调用get()方法会返回该对象。

问题:什么是唯一有自己的缩写的异常?答:NPE(Null Pointer Exception)空指针异常 。

这是在java中经常出现的异常,被称为十亿美元的错误

使用Optional 可以避免NPE,然而,它必须正确使用。

这里有一些建议关于Optional的使用。

  • 你不能简单的调用get()方法,需要考虑Optional没有值的情况,并提出一个合理的默认值。
  • 如果没有一个合理的默认值,那么可以使用.map().flatMap()方法延迟定义。
  • 如果外部库返回null,用Optional.ofNullable()包裹相应的值,相信我,你以后会感谢你自己的。null会在程序中冒泡(bubble up),所以停止在程序中使用它。
  • 在方法的返回类型中使用Optional. 因为你不需要读Javadoc,来找不存在值的情况。

你应该避免使用类似这种方法:

// AVOID:
CompletableFuture<T> method(CompletableFuture<S> param);
// PREFER: 
T method(S param);

// AVOID:
List<T> method(List<S> param);
// PREFER:
T method(S param);

// AVOID: 
T method(A param1, B param2, Optional<C> param3);
// PREFER:
T method(A param1, B param2, C param3);
T method(A param1, B param2);
// This method is clearly doing two things, it should be two methods
// The same is true for boolean parameters

以上的方法中有共同之处是什么?它们使用容器对象,如Optional,List,Task作为方法的参数。当返回类型是相同的容器时,情况更糟。(一个带有Optional参数的方法并返回一个Optional

Why?

1) Promise<A> method(Promise<B> param) is less flexible than simply having

2) A method(B param).

如果你有一个Promise<B>,那么你可以使用1),或者你可以使用2),通过“提升”函数和map (ie. promise.map(method))。

但是,如果您只有B,那么您可以轻松地使用2),但是您不能使用1),这使得2)更灵活。

我喜欢称之为“unlifting”,因为它是相对常见的功能实用方法“lift”。应用这些改写使方法更灵活、更容易使用的用户。

参考

看看我的另一篇关于”Practical Functional Programming”的文章:

https://hackernoon.com/practical-functional-programming-6d7932abc58b