赞
踩
问题描述:
修改意见:
技术易变,改变之后容易忽略更改命名。
问题描述:
修改意见:
对于英语:最低限度的要求是写出来的代码要像是在用英语表达。
问题描述:
修改意见:
问题描述:
解决办法:
问题描述:
解决办法:
重复代码过多,发生改动需要全盘修改。真正应该做的是,先提取出函数,然后,在需要的地方调用这个函数。
原文中没有提到作用域的问题。在实际编写的时候,需要注意 Extract 出的函数的作用域,避免代码因为团队成员的变动而腐败。
@Task
public void sendBook() {
try {
this.service.sendBook();
} catch (Throwable t) {
this.notification.send(new SendFailure(t)));
throw t;
}
}
@Task
public void sendChapter() {
try {
this.service.sendChapter();
} catch (Throwable t) {
this.notification.send(new SendFailure(t)));
throw t;
}
}
重复的代码结构,会造成大量的冗余,比如其中的 catch 操作。根据面向对象的设计来说,应该针对这一段代码进行接口设计。
private void executeTask(final Runnable runnable) {
try {
runnable.run();
} catch (Throwable t) {
this.notification.send(new SendFailure(t)));
throw t;
}
}
之后再使用的时候,就能直接通过实现接口的方式来达到目的。
@Task
public void sendBook() {
executeTask(this.service::sendBook);
}
@Task
public void sendChapter() {
executeTask(this.service::sendChapter);
}
经过改造之后,就很容易解决结构上的重复。
if (user.isEditor()) {
service.editChapter(chapterId, title, content, true);
} else {
service.editChapter(chapterId, title, content, false);
}
这种结构的 if 语句,只想到 if 语句判断之后要做什么,而没有想到这个 if 语句判断的到底是什么。如果做到更加优雅的编写呢?
boolean approved = user.isEditor();
service.editChapter(chapterId, title, content, approved);
对于进一步的 Extract 操作,可以使用函数来表示:
private boolean isApproved(final User user) {
return user.isEditor();
}
只要你看到 if 语句出现,而且 if 和 else 的代码块长得又比较像,就可以使用这样的原则进行抽取改造。
写代码要想做到 DRY,一个关键点是能够发现重复。
DRY:Dont’ Repeat Yourself。
记住:不要重复自己,不要复制粘贴。
参考:无代码低代码如何实现(代码dry)-天道酬勤-花开半夏
对于函数长度容忍度高,这是导致长函数产生的关键点。
一个好的程序员面对代码库时要有不同尺度的观察能力,看设计时,要能够高屋建瓴,看代码时,要能细致入微。
一般来说,主要有以下原因:
为了避免代码中出现不利于维护和理解的长函数,我们需要遵循的原则是:把函数写短,越短越好。
大类有两种表现形式:类里面的函数特别多,类里面有特别多的字段和函数。
一个人理解的东西是有限的,没有人能同时面对所有细节。
如果一个类里面的内容太多,它就会超过一个人的理解范畴,顾此失彼就在所难免。
我们需要避免大类带来的问题,就需要解决大类,即拆解大类成为小类。
将大类拆解成小类,本质上在做的工作是一个设计工作。
支撑我们来做这种分析和设计的就是单一职责原则。
把类写小,越小越好。
一旦参数列表变得很长,我们就很难对这些内容进行把控。
长参数列表的问题是数量多,解决这个问题的关键就在于,减少参数的数量。
public void createBook(final String title,
final String introduction,
final URL coverUrl,
final BookType type,
final BookChannel channel,
final String protagonists,
final String tags,
final boolean completed) {
// ...
}
在这样的代码中,每增加一个条件,就会往这里面的代码增加一个参数。一旦参数越来越多,就会导致长参数列表。
一个很好的解决办法就是,将参数列表封装成类/对象。
public class NewBookParamters {
private String title;
private String introduction;
private URL coverUrl;
private BookType type;
private BookChannel channel;
private String protagonists;
private String tags;
private boolean completed;
}
这样的方式解决了传参的问题,但是在使用该参数的时候,是不是要逐一使用 get 或者其他方法来讲属性字段提取出来呢?
一个模型的封装应该是以行为为基础的。
那么根据这个情况,该模型配套的行为应该是创建行为。
public class NewBookParamters { private String title; private String introduction; private URL coverUrl; private BookType type; private BookChannel channel; private String protagonists; private String tags; private boolean completed; public Book newBook() { return Book.builder .title(title) .introduction(introduction) .coverUrl(coverUrl) .type(type) .channel(channel) .protagonists(protagonists) .tags(tags) .completed(completed) .build(); } }
如果需求扩展,需要增加创建作品所需的内容,那这个参数列表就是不变的,相对来说,它就是稳定的。
在使用上,通过构造的方式进行。
public void createBook(final NewBookParamters parameters) {
// ...
Book book = parameters.newBook();
this.repository.save(book);
}
把长参数列表封装成一个类,这能解决大部分的长参数列表,但并不等于所有的长参数列表都应该用这种方式解决,因为不是所有情况下,参数都属于一个类。
public void getChapters(final long bookId,
final HttpClient httpClient,
final ChapterProcessor processor) {
HttpUriRequest request = createChapterRequest(bookId);
HttpResponse response = httpClient.execute(request);
List<Chapter> chapters = toChapters(response);
processor.process(chapters);
}
在这种情况下,因为每次调用该函数的时候,bookId 的变化频率同 httpClient 和 processor 这两个参数的变化频率是不同的。一边是每次都变,另一边是不变的。
进行动静分离,就是将不会改变的对象转化为成员变量而不是通过参数传递。
public void getChapters(final long bookId) {
HttpUriRequest request = createChapterRequest(bookId);
HttpResponse response = this.httpClient.execute(request);
List<Chapter> chapters = toChapters(response);
this.processor.process(chapters);
}
长参数列表固然可以用一个类进行封装,但能够封装出这个类的前提条件是:这些参数属于一个类,有相同的变化原因。
所以对于不方便封装成一个类的对象,最好是使用动静分离的方式进行拆分。
public void editChapter(final long chapterId,
final String title,
final String content,
final boolean apporved) {
// ...
}
代码之中的 approved 属于布尔标记。代码之中的逻辑可能根据该标记有不同的处理方式,通常会使用 if-else 的形式写在函数中。
**将标记参数代表的不同路径拆分出来。**一方面可以进行函数 Extract ,一方面可以减少参数长度。
// 普通的编辑,需要审核
public void editChapter(final long chapterId,
final String title,
final String content) {
...
}
// 直接审核通过的编辑
public void editChapterWithApproval(final long chapterId,
final String title,
final String content) {
...
}
这里的一个函数可以拆分成两个函数,一个函数负责“普通的编辑”,另一个负责“可以直接审核通过的编辑”。
在重构中,这种手法叫做移除标记参数(Remove Flag Argument)。
应该尽量写“短小”的代码。
这是由人类理解复杂问题的能力决定的,只有短小的代码,我们才能有更好地把握,而要写出短小的代码,需要我们能够“分离关注点”。
变化频率相同,则封装成一个类。
变化频率不同:
减小参数列表,越小越好。
public void distributeEpubs(final long bookId) {
List<Epub> epubs = this.getEpubsByBookId(bookId);
for (Epub epub : epubs) {
if (epub.isValid()) {
boolean registered = this.registerIsbn(epub);
if (registered) {
this.sendEpub(epub);
}
}
}
}
这种嵌套语句,产生的原因就是:平铺直叙写代码。
通过对代码的修改,可以变成这样的形式。
public void distributeEpubs(final long bookId) {
List<Epub> epubs = this.getEpubsByBookId(bookId);
for (Epub epub : epubs) {
this.distributeEpub(epub);
}
}
private void distributeEpub(final Epub epub) {
if (epub.isValid()) {
boolean registered = this.registerIsbn(epub);
if (registered) {
this.sendEpub(epub);
}
}
}
此种情况下,如果代码的嵌套层数比较多,可以采用 Extract 抽取方法的形式降低嵌套层数。
但是如果是 for 循环语句的层数比较多的时候,考虑到 Java 中这种情况下,通常是 List、Set 等形式的集合,可以采用 Stream 流的形式来降低代码的嵌套层数。
通常来说,if 语句造成的缩进,很多时候都是在检查某个先决条件,只有条件通过时,才继续执行后续的代码。
这样的代码可以使用**卫语句(guard clause)**来解决,也就是设置单独的检查条件,不满足这个检查条件时,立刻从函数中返回。
这是一种典型的重构手法:以卫语句取代嵌套的条件表达式(Replace Nested Conditional with Guard Clauses)。
private void distributeEpub(final Epub epub) {
if (!epub.isValid()) {
return;
}
boolean registered = this.registerIsbn(epub);
if (!registered) {
return;
}
this.sendEpub(epub);
}
在编程的时候,要注意函数至多有一层缩进,且不要使用 else 关键字。else 也是一种不好的编程习惯。
在软件开发中,有一个衡量代码复杂度常用的标准,叫做圈复杂度(Cyclomatic complexity,简称 CC),圈复杂度越高,代码越复杂,理解和维护的成本就越高。在圈 复杂度的判定中,循环和选择语句占有重要的地位。圈复杂度可以使用工具来检查。有很多可以检查圈复杂度的工具,比如在 Java 中使用 Checkstyle 就可以进行圈复杂度的检查,你可以限制最大的圈复杂度,当圈复杂度大于某个值的时候, 就会报错。
之所以会出现重复的 switch,通常都是缺少了一个模型。所以,应对这种坏味道,重构的手法是:以多态取代条件表达式(Relace Conditional with Polymorphism)。
public double getBookPrice(final User user, final Book book) { double price = book.getPrice(); switch (user.getLevel()) { case UserLevel.SILVER: return price * 0.9; case UserLevel.GOLD: return price * 0.8; case UserLevel.PLATINUM: return price * 0.75; default: return price; } } public double getEpubPrice(final User user, final Epub epub) { double price = epub.getPrice(); switch (user.getLevel()) { case UserLevel.SILVER: return price * 0.95; case UserLevel.GOLD: return price * 0.85; case UserLevel.PLATINUM: return price * 0.8; default: return price; } }
在进行重构的时候,应该使用多态来进行消除:
interface UserLevel { double getBookPrice(Book book); double getEpubPrice(Epub epub); } class RegularUserLevel implements UserLevel { public double getBookPrice(final Book book) { return book.getPrice(); } public double getEpubPrice(final Epub epub) { return epub.getPrice(); } class GoldUserLevel implements UserLevel { public double getBookPrice(final Book book) { return book.getPrice() * 0.8; } public double getEpubPrice(final Epub epub) { return epub.getPrice() * 0.85; } } class SilverUserLevel implements UserLevel { public double getBookPrice(final Book book) { return book.getPrice() * 0.9; } public double getEpubPrice(final Epub epub) { return epub.getPrice() * 0.85; } } class PlatinumUserLevel implements UserLevel { public double getBookPrice(final Book book) { return book.getPrice() * 0.75; } public double getEpubPrice(final Epub epub) { return epub.getPrice() * 0.8; } } }
在调用的时候,只需要以下的步骤即可:
public double getBookPrice(final User user, final Book book) {
UserLevel level = user.getUserLevel();
return level.getBookPrice(book);
}
public double getEpubPrice(final User user, final Epub epub) {
UserLevel level = user.getUserLevel();
return level.getEpubPrice(epub);
}
String name = book.getAuthor().getName();
当必须得先了解一个类的细节,才能写出代码时,说明这个封装是失败的。
这样的状况,不仅容易发生 NPE 空指针错误,还容易让人找不到目标数据。
为了解决这种过长的消息链,我们需要通过隐藏委托关系来解决。
class Book {
// ...
public String getAuthorName() {
return this.author.getName();
}
// ...
}
String name = book.getAuthorName();
要想提升代码水平,就要先从少暴露细节开始。这一点我们可以遵循迪米特法则(Law of Demeter)。
迪米特法则(Law of Demeter)又叫作最少知识原则(The Least Knowledge Principle),一个类对于其他类知道的越少越好,就是说一个对象应当对其他对象有尽可能少的了解,只和朋友通信,不和陌生人说话。英文简写为:LOD。
迪米特法则可以简单说成:talk only to your immediate friends。 对于OOD来说,又被解释为下面几种方式:一个软件实体应当尽可能少的与其他实体发生相互作用。每一个软件单位对其他的单位都只有最少的知识,而且局限于那些与本单位密切相关的软件单位。
为了避免封装的方法太多,我们应该考虑的问题是类应该提供哪些行为,而非简简单单地把数据换一种形式呈现出来。
一个好的封装是需要基于行为的。
public double getEpubPrice(final boolean highQuality, final int chapterSequenc
// ...
}
这样的返回值,看似正常,但其实是存在问题的。如果说要对返回的 double 数值进行测试,需要在调用之后一直重复判断。
这种采用基本类型的设计缺少了一个模型。
为了解决问题,可以新建一个模型来解决这个问题。
class Price {
private long price;
public Price(final double price) {
if (price <= 0) {
throw new IllegalArgumentException("Price should be positive");
}
this.price = price;
}
}
这样的返回值设计之后,就可以避免在很多地方出现重复的逻辑判断代码。
这种引入一个模型封装基本类型的重构手法,叫做以对象取代基本类型(Replace Primitive with Object)。
使用基本类型和使用继承出现的问题是异曲同工的。在设计类的时候,组合是优于继承的。
public Books extends List<Book> {
// ...
}
也就是说,在创建类的时候,需要优先使用下面的情况:
public Books {
private List<Book> books;
// ...
}
在设计的时候不要只看到了模型的相同之处,却忽略了差异的地方。这种情况称为基本类型偏执(Primitive Obsession)。
封装之所以有难度,主要在于它是一个构建模型的过程。
构建模型,封装散落的代码。
Setter 方法是一种缺乏封装的表现。setter 同 getter 一样,反映的都是对细节的暴露。这两种方法同时存在,意味着你不仅可以读数据,还能对数据进行修改操作。
public void approve(final long bookId) {
// ...
book.setReviewStatus(ReviewStatus.APPROVED);
// ...
}
比可变的数据更可怕的是,不可控的变化。因为暴露了 Setter 方法,那么在被调用的时候,变化就不可控,就有可能发生各种的变化。
缺乏封装 + 不可控变化, setter 方法带来的影响是比较严重的。
修改的方法是:用一个函数替代 setter,也就是用行为封装了起来。
public void approve(final long bookId) {
// ...
book.approve();
// ...
}
之后,在 Book 类中提供 approve 审核方法。
class Book {
public void approve() {
this.reviewStatus = ReviewStatus.APPROVED;
}
}
作为这个类的使用者,并不需要知道这个类到底是怎么实现的。更重要的是变化变得可控了。虽然审核状态这个字段还是会修改,但所有的修改都要通过几个函数作为入口。有任何业务上的调整,都会发生在类的内部,只要保证接口行为不变,就不会影响到其它的代码。
另外,对于在初始化过程中,需要使用到 setter 方法。对于这种只在初始化中使用的情况,没有必要以 setter 的形式存在,真正需要的是一个有参数的构造函数。
Book book = new Book(bookId, title, introduction);
消除 setter ,有一种专门的重构手法,叫做移除设值函数(Remove Setting Method)。
下面是 lombok.config 的配置,通过配置就可以禁用 @Setter 了。
lombok.setter.flagUsage = error
lombok.data.flagUsage = error
反对使用 setter,一个重要的原因就是它暴露了数据。暴露数据造成的问题就在于数据的修改,进而导致出现难以预料的 Bug 。
在程序编码的时候尽量减少可变数据(Mutable Data)的存在。
解决可变数据,还有一个解决方案是编写不变类。
一个更实用的做法是区分类的性质。我们最核心要识别的对象分成两种,实体和值对象。实体对象要限制数据变化,而值对象就要设计成不变类。
连赋值本身就是不好的编程习惯。
另外一个容易被忽略的就是全局数据(Global Data)。全局数据一样可能被多处修改,容易造成数据安全问题。
在编写代码的时候,注意限制可变的数据。
EpubStatus status = null;
CreateEpubResponse response = createEpub(request);
if (response.getCode() == 201) {
status = EpubStatus.CREATED;
} else {
status = EpubStatus.TO_CREATE;
}
这段代码中有两个问题,一个是 ELSE 的使用,一个是变量的初始化。对于变量而言,变量的初始化最好一次性完成。
初始化过程中,真正的问题就是不清晰,变量初始化与业务处理混在在一起。
保证变量初始化一次性完成。
final CreateEpubResponse response = createEpub(request);
final EpubStatus status = toEpubStatus(response);
private EpubStatus toEpubStatus(final CreateEpubResponse response) {
if (response.getCode() == 201) {
return EpubStatus.CREATED;
}
return EpubStatus.TO_CREATE;
}
在编码的过程中,要尽可能使用不变的量,即在能够使用 final 的地方尽量使用 final 变量。
另一个常见的坏习惯就是在 try-catch 语句块中:
InputStream is = null;
try {
is = new FileInputStream(...);
...
} catch (IOException e) {
...
} finally {
if (is != null) {
is.close();
}
}
在 JDK 1.7 之后,可以采用 try-with-resource 的写法,代码可以更简洁。
try (InputStream is = new FileInputStream(...)) {
// ...
}
在集合的初始化上,通常使用的是如下方式:
List<Permission> permissions = new ArrayList<>();
permissions.add(Permission.BOOK_READ);
permissions.add(Permission.BOOK_WRITE);
check.grantTo(Role.AUTHOR, permissions);
但实际上,出现这种写法的原因是在早期的 Java 版本中,没有提供很好的集合初始 化的方法。我们真正需要的是添加了元素的集合,而不是一个空集合。
为了改变这种初始化繁琐的过程,在 JDK 9 中提供了集合的初始化方法:
List<Permission> permissions = List.of(
Permission.BOOK_READ,
Permission.BOOK_WRITE
);
check.grantTo(Role.AUTHOR, permissions);
如果使用的是 JDK 1.9 以下的版本,可以使用 Guava(Google 提供的一个 Java 库)实现类似的效果。
List<Permission> permissions = ImmutableList.of(
Permission.BOOK_READ,
Permission.BOOK_WRITE
);
check.grantTo(Role.AUTHOR, permissions);
因为此 List 没有可变的需求,所以我们可以使用 ImmutableList 类来实现初始化的需求。
一次性完成初始化,更像是声明式的代码体现的意图,是更高层面的抽象,把意图和实现分开,能更加实现关注点的分离。
用声明式的标准来审视代码,可以看出很多代码的关注点糅合的地方。而我们要做的就是尽量分离关注点。
学习编程不仅仅是要学习实现功能,编程的风格也要与时俱进。
@PostMapping("/books")
public NewBookResponse createBook(final NewBookRequest request) {
boolean result = this.service.createBook(request);
// ...
}
按照通常的架构设计原则,service 层属于我们的核心业务,而 controller 层属于接口。二者相较而言,核心业务的重要程度更高一些,所以,service 的稳定程度也应该更高一些。同样的业务,我们可以用 REST 的方式对外提供,也可以用 RPC 的方式对外提供。
这样来看,那么这个其中的 request 参数放到哪里都会有问题,放到哪个层里都有问题。
这个问题出现的关键在于缺少了一个模型。
主要就是因为这个参数只能扮演一个层中的模型,所以只要再引入一个模型就可以破解这个问题。
class NewBookParameter {
// ...
}
class NewBookRequest {
public NewBookParameters toNewBookRequest() {
// ...
}
}
@PostMapping("/books")
public NewBookResponse createBook(final NewBookRequest request) {
boolean result = this.service.createBook(request.toNewBookParameter());
// ...
}
这样,在调用的时候,可以直接传入原始的参数。
class NewBookRequest {
public NewBookParameters toNewBookRequest(long userId) {
// ...
}
}
@PostMapping("/books")
public NewBookResponse createBook(final NewBookRequest request, final Authentication authentication) {
long userId = getUserIdentity(authentication);
boolean result = this.service.createBook(request.toNewBookParameter(userId));
// ...
}
这种场景就是个典型的缺陷,缺少防腐层。
在很多的业务场景中,会出现很多的具体实现代码。
@Task
public void sendBook() {
try {
this.service.sendBook();
} catch (Throwable t) {
this.feishuSender.send(new SendFailure(t)));
throw t;
}
}
这段代码的作用就是在执行 sendBook 方法的时候,出现异常通过 feishu 发送消息。这是一种符合直觉的做法,但是却不符合设计原则,违反了依赖倒置原则。
高层模块不应依赖于低层模块,二者应依赖于抽象。
抽象不应依赖于细节,细节应依赖于抽象。
这种用具体的实现来调用方法,就是违反了依赖倒置的设计原则。
业务代码中任何与业务无关的东西都是潜在的不好的设计。
在这里,feishu 肯定不是业务的一部分,它只是当前选择的一个具体实现。换言之,是否选择 feishu,与团队当前的状态是相关的,如果哪一天团队切换即时通信软件,这个实现就需要换掉。但是,团队是不可能切换业务的,一旦切换,那就是一个完全不同的系统了。
识别一个东西是业务的一部分,还是一个可以替换的实现,可以设想如果不用它,是否还有其它的选择。
如果存在其他的选择,那么最好就不使用具体的实现来进行调用。
interface FailureSender {
void send(SendFailure failure);
}
class FeishuFailureSenderS implements FailureSender {
// ...
}
在进行这样的改造之后,我们就可以在后面需要切换 IM 软件或者该部分的实现方式的时候,修改维护更加容易。
依赖混乱是编码的时候很容易出现的问题。
代码应该向着稳定的方向依赖。
enum DistributionChannel {
WEBSITE,
KINDLE_ONLY,
ALL
}
可以看到分发渠道包括网站(WEBSITE)、只在 Kindle(KINDLE_ONLY),还是全渠道(ALL)。但是其中,网站也代表了只在网站发布。这就意味着网站和 Kindle 都表示的是在单独一个渠道发布,然而 Kindle 渠道的命名缺加上了 ONLY 的结尾。这就是命名的不一致。
出于一致性,类似含义的代码应该有一致的名字。与之相反,一旦出现了不一致的名字,通常都应该表示不同的含义。
修改的方案,统一命名规则即可。
enum DistributionChannel {
WEBSITE,
KINDLE,
ALL
}
public String nowTimestamp() {
DateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date now = new Date();
return format.format(now);
}
这段代码很简单,就是获取当前的时间戳。
之后,在同一个项目中,又出现了另外一种写法。
public String nowTimestamp() {
LocalDateTime now = LocalDateTime.now();
return now.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
}
之所以会出现这样的问题,主要是因为一个项目中,应对同一个问题出现了多个解决方案。
出现方案不一致的原因主要有两种:
这一点解决方案,通常是在团队中规定一致的解决方案。
public void createBook(final List<BookId> bookIds) throws IOException {
List<Book> books = bookService.getApprovedBook(bookIds);
CreateBookParameter parameter = toCreateBookParameter(books);
HttpPost post = createBookHttpRequest(parameter);
httpClient.execute(post);
}
这段代码中,首先是获取审核通过的作品,这是一个业务动作,接下来的三行其实是在做一件事,也就是发送创建作品的请求。具体到代码上,这三行代码分别是创建请求的参数,根据参数创建请求,最后,再把请求发送出去。这三行代码合起来完成了一个发送创建作品请求这么一件事,而这件事才是一个完整的业务动作。
这个函数里的代码并不在一个层次上,有的是业务动作,有的是业务动作的细节。
针对这个问题,我们可以做出相关的修改。
public void createBook(final List<BookId> bookIds) throws IOException {
List<Book> books = bookService.getApprovedBook(bookIds);
createRemoteBook(books);
}
private void createRemoteBook(List<Book> books) throws IOException {
CreateBookParameter parameter = toCreateBookParameter(books);
HttpPost post = createBookHttpRequest(parameter);
httpClient.execute(post);
}
从结果上看,原来的函数(createBook)里面全都是业务动作,而提取出来的函数(createRemoteBook)则都是业务动作的细节,各自的语句都是在一个层次上了。
能够分清楚代码处于不同的层次,基本功还是分离关注点。
前面拆分出来的这个方法,我们已经知道它的作用是发出一个请求去创建作品,本质上并不属于这个业务类的一部分。所以,我们还可以通过引入一个新的模型,将这个部分调整出去。
public void createBook(final List<BookId> bookIds) throws IOException {
List<Book> books = this.bookService.getApprovedBook(bookIds);
this.translationEngine.createBook(books);
}
class TranslationEngine {
public void createBook(List<Book> books) throws IOException {
CreateBookParameter parameter = toCreateBookParameter(books);
HttpPost post = createBookHttpRequest(parameter);
httpClient.execute(post);
}
}
保持代码在各个层面上的一致性。
随着语言版本的升级,经常会出现一些新的语言特性。新的语言特性都是为了提高代码的表达性,减少犯错误的几率。
String name = book.getAuthor().getName();
这样的代码中,因为没有考虑到空指针的问题,所以是有问题的。其次,缺乏封装。
Author author = book.getAuthor();
String name = (author == null) ? null : author.getName();
正确的写法应该是这样的。但是,在 Java 8 中,提供了更先进的 Optional 操作类。 Optional 提供了一个对象容器,可以更方便地用来探测空指针。
class Book {
public Optional<Author> getAuthor() {
return Optioanl.ofNullable(this.author);
}
// ...
}
Optional<Author> author = book.getAuthor();
String name = author.isPresent() ? author.get().getName() : null;
除此之外,还有别的写法。
Optional<Author> author = book.getAuthor();
String name = author.map(Author::getName).orElse(null);
所以,在之后的编码中,如果为了避免空指针忘记判断,可以在项目中做一个约定,所有可能为 null 的返回值,都要返回 Optional,以此减少 NPE 的几率。
public ChapterParameters toParameters(final List<Chapter> chapters) {
List<ChapterParameter> parameters = new ArrayList<>();
for (Chapter chapter : chapters) {
if (chapter.isApproved()) {
parameters.add(toChapterParameter(chapter));
}
}
return new ChapterParameters(parameters);
}
这段代码,主要是向翻译引擎发送章节信息前准备参数的代码,这里首先筛选出审核通过的章节,然后,再把章节转换成与翻译引擎通信的格式,最后,再把所有得到的单个参数打包成一个完整的章节参数。
因为函数式编程的兴起,本身循环语句就应该尽量避免。不是我们不需要遍历集合,而是我们有了更好的遍历集合的方式。
针对上面这段代码,我们可以采用 stream 流的形式将这段代码进行改造。
public ChapterParameters toParameters(final List<Chapter> chapters) {
List<ChapterParameter> parameters = chapters.stream()
.filter(Chapter::isApproved)
.map(this::toChapterParameter)
.collect(Collectors.toList());
return new ChapterParameters(parameters);
}
在这段代码中,我们用到了 Java 8 提供的一些基础设施,比如,Stream、lambda 和方法引用等。
lambda 都是为了写短小代码提供的便利,最好的 lambda 应该只有一行代码。
代码评审,它的本质,就是沟通反馈的过程。
我们希望沟通要尽可能透明,尽可能及时。把这样的理解放到代码评审中,就是要尽可能多暴露问题,尽可能多做代码评审。
代码评审就是一个发现问题的过程。我们可以从以下几个方面来进行代码审视:
需要关注代码评审的频率。
代码评审暴露的问题越多越好,频率越高越好。
一个有生命力的代码不会保持静止,新的需求总会到来,所以写代码时需要时时刻刻保持嗅觉。
我们必须对新增接口保持谨慎。
是否每一次的需求都需要增加新的接口,我们需要保持谨慎的态度。
在实现新的需求的时候,通常出现有需求就改动实体的情况。但我们在更改实体之前要考虑实体的适用范围,要了解实体现有的使用范围,不要因为这次的改动影响到现有的系统运行。
对于一个业务系统而言,实体是其中最核心的部分,对它的改动必须有谨慎的思考。
随意修改实体,必然伴随着其它部分的调整,而经常变动的实体,就会让整个系统难以稳定下来。
谨慎地对待接口和实体的变动。
什么代码应该被重构?质量不好的代码需要被重构。
如何界定“质量不好”这个标准?这才是我们界定这个问题的关键。
要想写出高质量的代码,就必须要培养自己对于质量不好的代码的嗅觉灵敏性。
我们必须培养出自己的判断力,学会判断一个类内有多少实例变量算是太大、一个函数内有多少行代码才算太长…
更多的一些代码问题,可以参考:ThoughtWorks文集(精选版)_敏捷_Thoughtworks
我们需要在受控环境下的刻意练习,然后通过工作中的自然积累提升判断力。
从一开始就以合理的方式编程,从而极度避免代码出现问题。这就是极限编程,极限编程是唯一合理且有效的软件开发方法。
写好代码应该是程序员一辈子需要为之努力的事情。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。