不准确的命名
public void processChapter(long chapterId) {
//查询出实体,然后修改他的状态为翻译中
}
这个函数的名字叫 processChapter(处理章节)
可以是可以,但是太广泛了。
命名过于宽泛,不能精准描述,这是很多代码在命名上存在的严重问题,也是代码难以理解的根源所在。
命名要能够描述出这段代码在做的事情。
修改成 changeChapterToTranlsating 呢?
它也不算是一个好名字,因为它更多的是在描述这段代码在做的细节。
一个好的名字应该描述意图,而非细节。
这段函数应该命名 startTranslation。
用技术术语命名
List<Book> bookList = service.getBooks();
bookList 变量之所以叫 bookList,原因就是它声明的类型是 List.
编程有一个重要的原则是面向接口编程,这个原则从另外一个角度理解,就是不要面向实现编程,因为接口是稳定的,而实现是易变的.
我现在需要的是一个不重复的作品集合,也就是说,我需要把这个变 量的类型从 List 改成 Set。变量类型你一定会改,但变量名你会改吗?这还真不一定,一 旦出现遗忘,就会出现一个奇特的现象,一个叫 bookList 的变量,它的类型是一个 Set。这样,一个新的混淆就此产生了.
所以,这个名字可以命名成 books。
List<Book> books = service.getBooks();
虽然这里我们只是以变量为例说明了以技术术语命名存在的问题,事实上,在实际的代码中,技术名词的出现,往往就代表着它缺少了一个应有的模型。
public Book getByIsbn(String isbn) {
Book cachedBook = redisBookStore.get(isbn);
redisBookStore.put(isbn, book);
return book;
}
这里真正需要的是一个缓存。Redis 是缓存这个模型的一个实现:
public Book getByIsbn(String isbn) {
Book cachedBook = cache.get(isbn);
Book book = doGetByIsbn(isbn);
cache.put(isbn, book);
return book;
}
再进一步,缓存这个概念其实也是一个技术术语,从某种意义上说,它也不应该出现在业务代码中。这方面做得比较好的是 Spring。使用 Spring 框架时,如果需要缓存,我们通 常是加上一个 Annotation(注解):
@Cacheable("books")
public Book getByIsbn(String isbn) {...}
用业务语言写代码
public void approveChapter(long chapterId, long userId) {
//这个函数的意图是,确认章节内容审核通过
}
chapterId 是审核章节的 ID.这个 userId 就是审核人的 userId.
这个 userId 并不是一个好的命名,因为它还需要更 多的解释,更好的命名是 reviewerUserId,之所以起这个名字,因为这个用户在这个场景 下扮演的角色是审核人(Reviewer)。
好的命名,是体现业务含义的命名。
英语命名
要求是写出来的代码要像是在用英语表达。
public void completedTranslate(final List<ChapterId> chapterIds) {
//它要做的是将一些章节的信息标记为翻译完成.
}
但是completedTranslate 并不是一个正常的英语函数名。
作者想 表达的是“完成翻译”。
常见的命名规则是:类名是一个名词,表示一个对象,而方法名则是一个动词,或者是动宾短语,表示一个动作。
completedTranslate 并不是一个有效的动宾结构,所以,这个函数名可以改成 completeTranslation。
但作为函数名,它应该是一个动词。
同样是审核,一个用了 audit,一个用了 review。
udit 会有更官方的味道,更合适的翻译应该是审计,而 review 则有更多核查的意思,二者相比,review 更适合这里的场景。
在这种情况下,最好的解决方案还是建立起一个业务词汇表,千万不要臆想。
重复代码
@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;
}
}
调用的时候:
public void sendBook() {
executeTask(this.service::sendBook);
}
如果再有一些通用的结构调整,比如,在任务执行前后要加上一些 日志信息,这样的改动就可以放到 executeTask 这个函数里,而不用四处去改写了。
上面的代码, 它们的动词不同,但是 除了这几个动词之外的其它部分是相同的 ,它们在结构上是重复的。
if
if (user.isEditor()) {
service.editChapter(chapterId, title, content, true);
} else {
service.editChapter(chapterId, title, content, false);
}
业务逻辑** : **章节只有在审核通过之后, 才能去做后续的处理
这个代码,别人来阅读的时候,要看到最后面才知道区别,也不好阅读
boolean approved = user.isEditor();
service.editChapter(chapterId, title, content, approved);
修改成这样就方便了。
如果将来审核通过的条件改变了,变化的点全都在 approved 的这个变量的赋值上面 。我们可以直接提取一个方法函数。
boolean approved = isApproved(user);
service.editChapter(chapterId, title, content, approved);
private boolean isApproved(final User user) {
return user.isEditor();
}
代码是集体所有,这样,就没有代码属于谁的说法了。不能透明沟通的人, 不适合在团队中工作。 在一个职业的队伍里,谁发现谁修改 。
很多问题是可以用设计模式和工具框架去解决的。 很多if条件处理不同的逻辑,这种情况一般都用策略模式去解决
长函数
**平铺直叙的代码存在的两个典型问题: **
把多个业务处理流程放在一个函数里实现;
把不同层面的细节放到一个函数里实现。
大类
一个人理解的东西是有限的,没有人能同时面对所有细节。
public class User {
private long userId;
private String name;
private String nickname;
private String email;
private String phoneNumber;
private AuthorType authorType;
private ReviewStatus authorReviewStatus;
private EditorType editorType;
...
}
用户 ID(userId)、姓名(name)、昵称 (nickname) 之类应该是一个用户的基本信息,后面的邮箱(email)、电话号码 (phoneNumber) 也算是和用户相关联的。今天的很多应用都提供使用邮箱或电话号码 登录的方式,所以,这个信息放在这里,也算是可以理解
作者类型(authorType) 、 作者审核状态 (authorReviewStatus), 编辑类型(editorType)
普通的用户既不是作者,也不是编辑。作者和编辑这些相关的字段,对普通用户来 说,都是没有意义的。其次,对于那些成为了作者的用户,编辑的信息意义也不大,因为 作者是不能成为编辑的,反之亦然,编辑也不会成为作者,作者信息对成为编辑的用户也 是没有意义的。
拆分
public class User {
private long userId;
private String name;
private String nickname;
private String email;
private String phoneNumber;
...
}
public class Author {
private long userId;
private AuthorType authorType;
private ReviewStatus authorReviewStatus;
...
}
public class Editor {
private long userId;
private EditorType editorType;
}
但是 userId、name、nickname 几项,算是用户的基本信息,而 email、 phoneNumber 这些则属于用户的联系方式 。
还可以拆
public class User {
private long userId;
private String name;
private String nickname;
private Contact contact;
...
}
public class Contact {
private String email;
private String phoneNumber;
...
}
长参数
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) {
...
Book book = Book.builder
.title(title)
.introduction(introduction)
.coverUrl(coverUrl)
.type(type)
.channel(channel)
.protagonists(protagonists)
.tags(tags)
.completed(completed)
.build();
this.repository.save(book);
}
这个函数的参数列表里,包含了一部作品所要 拥有的各种信息,比如:作品标题、作品简介、封面 URL、作品类型、作品归属的频道、 主角姓名、作品标签、作品是否已经完结等等 .
一般来说,我们可以封装一个类。
public class NewBookParamters {
private String title;
private String introduction;
private URL coverUrl;
private BookType type;
private BookChannel channel;
}
然后就剩一个参数了
public void createBook(final NewBookParamters parameters) {
...
}
但是这样,在参数里面使用的时候,难道再一个个get出来?是不是有点多此一举?
public void createBook(final NewBookParamters parameters) {
...
Book book = Book.builder
.title(parameters.getTitle())
.introduction(parameters.getIntroduction())
.coverUrl(parameters.getCoverUrl())
.type(parameters.getType())
.channel(parameters.getChannel())
.protagonists(parameters.getProtagonists())
.tags(parameters.getTags())
.completed(parameters.isCompleted())
.build();
this.repository.save(book);
}
上面是菜鸡的代码。 还没有形成对软件设计的理解。我们并不是简 单地把参数封装成类,站在设计的角度,我们这里引入的是一个新的模型。**一个模型的封装应该是以行为为基础的。 **
可以这样写:
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 两个参数都是一样的,因为它们都有相同的逻辑,没有什么变化。
不同的数据变动方向也是不同的 关注点。这里表现出来的就是典型的动数据(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) {
...
}
参数:待修改章节的 ID、标题和内容,最后一个参数表示这次修改是否直接 审核通过
前面几个参数是修改一个章节的必要信息,而这里的重点就在最后这个参数上。
之所以要有这么个参数,从业务上说,如果是作者进行编辑,之后要经过审核,而如果编辑来编辑的,那审核就直接通过,因为编辑本身扮演了审核人的角色。所以,你发现了, 这个参数实际上是一个标记,标志着接下来的处理流程会有不同。
但是这种写法很容易造成 彩旗(flag)飘飘,各种标记满天飞 。 不仅变量里有标 记,参数里也有。很多长参数列表其中就包含了各种标记参数。这也是很多代码产生混乱的一个重要原因
一种简单的方式就是,将标记参数代表的不同路径拆分出来。回到这段代 码上,这里的一个函数可以拆分成两个函数,一个函数负责“普通的编辑”,另一个负 责“可以直接审核通过的编辑”。
// 普通的编辑,需要审核
public void editChapter(final long chapterId,
final String title,
final String content) {
...
}
// 直接审核通过的编辑
public void editChapterWithApproval(final long chapterId,
final String title,
final String content) {
...
}
标记参数在代码中存在的形式很多,有的是布尔值的形式,有的是以枚举值的形式,还有的就是直接的字符串或者整数。无论哪种形式,我们都可以通过拆分函数的方式将它们拆开。在重构中,这种手法叫做移除标记参数
构造一个对象的方法有很多,比如,builder 模式
滥用 if、for
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);
}}}
业务:我们根据作品 ID 找到 要分发的 EPUB,然后检查 EPUB 的有效性。对于有效的 EPUB,我们要为它注册 ISBN 信息,注册成功之后,将这个 EPUB 发送出去。
这里 “平铺直叙地写代码”方式是不好的。
一个着手点 是 for 循环,因为通常来说,for 循环处理的是一个集合,而循环里面处理的是这个集合中的一个元素。所以,我们可以把循环中的内容提取成一个函数,让这个函数只处理一个元 素,就像下面这样:
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);
}
}
}
在 distributeEpub 里,造成缩进的原因是 if 语句.在检查某个先决条件,只有条件通过时,才继续执行后续的代码。.
卫语句取代嵌套的条件表达式
private void distributeEpub(final Epub epub) {
if (!epub.isValid()) {
return;
}
boolean registered = this.registerIsbn(epub);
if (!registered) {
return;
}
this.sendEpub(epub);
}
if else
public double getEpubPrice(final boolean highQuality, final int chapterSequenc
double price = 0;
if (highQuality && chapterSequence > START_CHARGING_SEQUENCE) {
price = 4.99;
} else if (sequenceNumber > START_CHARGING_SEQUENCE
&& sequenceNumber <= FURTHER_CHARGING_SEQUENCE) {
price = 1.99;
} else if (sequenceNumber > FURTHER_CHARGING_SEQUENCE) {
price = 2.99;
} else {
price = 0.99;
}
return price;
}
修改成这样:
public double getEpubPrice(final boolean highQuality, final int chapterSequenc
if (highQuality && chapterSequence > START_CHARGING_SEQUENCE) {
return 4.99;
}
if (sequenceNumber > START_CHARGING_SEQUENCE
return 1.99;
}
if (sequenceNumber > FURTHER_CHARGING_SEQUENCE) {
return 2.99;
}
return 0.99;
重复的 Switch
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;
}
}
这两段代码,分别计算了用户在网站上购买作品在线阅读所支付的价格,以及购买 EPUB 格式电子书所支付的价格。其中,用户实际支付的价格会根据用户在系统中的用户级别有所差异,级别越高,折扣就越高。
使用多态解决
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();
这段代码表达的是“获得一部作品作者的名字”。作品里有作者信息,想要获得作者的名字,通过“作者”找到“作者姓名”。
如果你想写出上面这段代码,是不是必须得先了解 Book 和 Author 这两个类的实现细节?也就是说,我们必须得知道,作者的姓名是存储在作品的作者字段里的。这时你就要注意了:当你必须得先了解一个类的细节,才能写出代码时,这只能说明一件事,这个封装是失败的。
**过长的消息链,**解决这种代码的重构手法叫隐藏委托关系
class Book {
public String getAuthorName() {
return this.author.getName();
}
String name = book.getAuthorName();
根据章节信息获取 EPUB, 的价格
public double getEpubPrice(final boolean highQuality, final int chapterSequenc
}
问题就出在返回值的类型上,也就是价格的类型上
这种采用基本类型的设计缺少了一个模型。
虽然价格本身是用浮点数在存储,但价格和浮点数本身并不是同一个概念,有着不同的行为需求。比如,一般情况下,我们要求商品价格是大于 0 的,但 double 类型本身是没有这种限制的
就以“价格大于 0”这个需求为例,如果使用 double 类型你会怎么限制呢?我们通常会 这样写:
if (price <= 0) {
throw new IllegalArgumentException("Price should be positive");
}
如果使用 double 作为类型,那我们要在使用的地方都保证价格的正确性,像这样的价格校验就应该是使用的地方到处写的。
我们可以引入一个 Price 类型,
class Price {
private long price;
public Price(final double price) {
if (price <= 0) {
throw new IllegalArgumentException("Price should be positive");
}
this.price = price;
}
}
这种引入一个模型封装基本类型的重构手法,叫做以对象取代基本类型.
如果我们想要让价格在对外呈现时只有两位,在没有 Price 类的时候,这样的逻辑就会散落代码的各处,事实上,代码里很多重复的逻辑就是这样产生的。而现在我们可以在 Price 类里提供 一个方法:
public double getDisplayPrice() {
BigDecimal decimal = new BigDecimal(this.price);
return decimal.setScale(2, BigDecimal.ROUND_HALF_UP).doubleValue();
}
价格就是一个 double。这里的误区就在于, 一些程序员只看到了模型的相同之处,却忽略了差异的地方。Books 可能不需要提供 List 的所有方法,价格的取值范围与 double 也有所差异
这种以基本类型为模型的坏味道称为基本类型偏执.
可变的数据
相比于读数据,修改是一个更危险的操作。
变量的声明与赋值
EpubStatus status = null;
CreateEpubResponse response = createEpub(request);
if (response.getCode() == 201) {
status = EpubStatus.CREATED;
} else {
status = EpubStatus.TO_CREATE;
}
这段代码在做的事情是向另外一个服务发请求创建 EPUB(一种电子书格式),如果创建成功,返回值是 HTTP 的 201,也就表示创建成功,然后就把状态置为 CREATED;而如果没有成功,则把状态置为 TO_CREATE。后面对于 TO_CREATE 状态的作品,还需要再次尝试创建。
if…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,限制变量的赋值
异常处理的场景
InputStream is = null;
try {
is = new FileInputStream(...);
...
} catch (IOException e) {
...
} finally {
if (is != null) {
is.close();
}
}
之所以要把 InputStream 变量 is 单独声明,是为了能够在 finanlly 块里面访问到。其实,这段代码写成这样,一个重要的原因是 Java 早期的版本只能写成这样,而如果采用 Java 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);
这是一段给作者赋予作品读写权限的代码,逻辑比较简单,但这段代码中也存在一些坏味 道。我们把注意力放在 permissions 这个集合上。之所以要声明这样一个 List,是因为 grantTo 方法要用到一个 List 作为参数。
java 9可以这样写
List<Permission> permissions = List.of(
Permission.BOOK_READ,
Permission.BOOK_WRITE
);
check.grantTo(Role.AUTHOR, permissions);
没有Java9,可以使用Guava
List<Permission> permissions = ImmutableList.of(
Permission.BOOK_READ,
Permission.BOOK_WRITE
);
check.grantTo(Role.AUTHOR, permissions);
hashMap
private static Map<Locale, String> CODE_MAPPING = new HashMap<>();
static {
CODE_MAPPING.put(LOCALE.ENGLISH, "EN");
CODE_MAPPING.put(LOCALE.CHINESE, "CH");
}
改进
private static Map<Locale, String> CODE_MAPPING = ImmutableMap.of(
LOCALE.ENGLISH, "EN",
LOCALE.CHINESE, "CH"
);
前面的代码是命令式的代码,而后面的代码是声明式的代码。
声明式的代码体现的意图,是更高层面的抽象,把意图和实现分开,从某种意义上来说,也是一种分离关注点。
我们学习编程不仅仅是要学习实现功能,编程的风格也要与时俱进。
依赖混乱
1 @PostMapping("/books")
2 public NewBookResponse createBook(final NewBookRequest request) {
3 boolean result = this.service.createBook(request);
4 }
这段代码是创建一部作品的入口,也就是说,它提供了一个 REST 服务,只要我们对 /books 这个地址发出一个 POST 请求,就可以创建一部作品出来。那么,这段代码有问题吗?
问题出在传递 的参数上。请问,这个 NewBookRequest 的参数类应该属于哪一层,是 resource (有的团队称之为 Controller)层,还是 service 层呢?
既然它是一个请求参数,通常要承载着诸如参数校验和对象转换的职责,按照我们通常的理解,它应该属于 resource 层。如果这个理解是正确的,问题就来了,它为什么会传递给 service 层呢?
NewBookRequest 这个本来应该属于接口层的参数,现在成了核心业务的一部分,也就是说,即便将来我们提供了 RPC 的接口,它也要知道 REST的接口长什么样子,显然,这是有问题的。
既然 NewBookRequest 属于 resource 层是有问题的,那我们假设它属于 service 层呢? 正如我们前面所说,一般请求都要承担对象校验和转化的工作。如果说这个类属于 service 层,但它用在了 resource 的接口上,作为 resource 的接口,它会承载一些校验和对象转换的角色,而 service 层的参数是不需要关心这些的。如果 NewBookRequest 属于 service 层,那校验和对象转换的职责到底由谁来完成
有时候,我们service还需要去获取userid,这个是不能前端传递的,需要自己去获取。
一个关键点在于,我们缺少了一个模型。
class NewBookParameter {
}
class NewBookRequest {
public NewBookParameters toNewBookRequest() {
...
}
}
@PostMapping("/books")
public NewBookResponse createBook(final NewBookRequest request) {
boolean result = this.service.createBook(request.toNewBookParameter());
...
}
这里我们引入了一个 NewBookParameter 类,把它当作 service 层创建作品的入口,而在 resource 中,我们将 NewBookRequest 这个请求类的对象转换成了NewBookParameter 对象,然后传到 service 层。
两个对象之间可以是不相同的,有了两层不同的参数,我们就可以给不同层次上的模型以不同的约定了。
同样表示价格,在请求对象中,我们可以是一个 double 类型,而在业务参数对象中,它应该是 Price 类型。
需要传人user ID
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))
}
@Entity
@Table(name = "user")
@JsonIgnoreProperties(ignoreUnknown = true)
class User {}
这是一个 User 类的声明,它有 @Entity 这个 Anntation,表示它是一个业务实体的对象,但它的上面还出现了 @JsonIgnoreProperties,这是就是处理 JSON 的一个 Annotation。JSON 会在哪用到,通常都是在传输中。业务实体和传输对象应该具备的特质在同一个类中出现,显然,这也是没有构建好防腐层的结果,把两个职责混在了一起。
@Task
public void sendBook() {
try {
this.service.sendBook();
} catch (Throwable t) {
this.feishuSender.send(new SendFailure(t)));
throw t;
}
}
一旦发送过程出了问题,要通过即时通信工具发送给相关人等
这里就写了飞书发送,就有点问题。
业务代码中任何与业务无关的东西都是潜在的坏味道。
飞书肯定不是业务的一部分,它只是当前选择的一个具体实现。比如,Kafka、 Redis、MongoDB 等等,通常也都是一个具体的实现
需要隔离
interface FailureSender {
void send(SendFailure failure);
}
class FeishuFailureSenderS implements FailureSender {
}
@Test
public void should_follow_arch_rule() {
JavaClasses clazz = new ClassFileImporter().importPackages("...");
ArchRule rule = layeredArchitecture()
.layer("Resource").definedBy("..resource..")
.layer("Service").definedBy("..service..")
.whereLayer("Resource").mayNotBeAccessedByAnyLayer()
.whereLayer("Service").mayOnlyBeAccessedByLayers("Resource");
rule.check(clazz);
}
在这里,我们定义了两个层,分别是 Resource 层和 Service 层,而且我们要求 Resource 层的代码不能被其它层访问,而 Service 层的代码只能由 Resource 层方法访问。这就是 我们的架构规则,一旦代码里有违反这个架构规则的代码,这个测试就会失败,问题也就会暴露出来。
如果后期把飞书替换成email的话,新建一个继承那个FailureSender接口的EmailFailureSenderImp然后加上@Service,再把飞书的那个服务的@Service注释或删除掉吗?
不是,依赖注入的 bean 其实都是有名字的,可以指定一个名字就好了,也就是 Qulifier。指定service。
不一致
命名中的不一致
enum DistributionChannel {
WEBSITE
KINDLE_ONLY
AL
}
目前的分发渠
道包括网站(WEBSITE)、只在 Kindle(KINDLE_ONLY),还是全渠道(ALL)。
既然WEBSITE,KINDLE_ONLY二者都有只在一个平台上架发布的含义,为什么不都叫 XXX 或者
XXX_ONLY?
类似含义的代码应该有一致的名字,比如,很多团队里都会 把业务写到服务层,各种服务的命名也通常都是 XXXService,像 BookService.
修改之后
enum DistributionChannel {
WEBSITE
KINDLE
AL
}
像判断字符串是否为空或空字符串,有很多处理方法。但是经常因为引入了其它程序库,相应的依赖就出现在我
们的代码中。所以,我们必须约定,哪种做法是我们在项目中的标准做法,以防出现各自为战的现象。比如,在我的团队中,我们就选择 Guava 作为基础库,因为相对来说,它的 风格更现代,所以,团队就约定类似的操作都以 Guava 为准。
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)
}
根据要处理的作品 ID 获取其中已经审核通
过的作品,然后,发送一个 HTTP 请求在翻译引擎中创建出这个作品。
这些代码不是一个层次的代码,
首先是获取审核通过的作品,这是一个业务动作,接下来的三行其实是在做一件事,也就是发送创建作品的请求。
所以我们把三行的代码提取成一个函数
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)
}
发出一个请求去创建作品,本质上并不属于这个业 务类的一部分。所以,我们还可以通过引入一个新的模型,将这个部分调整出去:
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)
}
}
利用Java8的特性