paint-brush
学习如何在 Java 中使用不可变和可靠的对象经过@artemsutulov
624 讀數
624 讀數

学习如何在 Java 中使用不可变和可靠的对象

经过 Artem Sutulov1m2022/05/28
Read on Terminal Reader
Read this story w/o Javascript

太長; 讀書

在编写复杂的项目时,培养良好的代码文化至关重要。使用不可变和一致的对象是最重要的对象之一。您可以忽略这一点并将复杂对象编写为标准对象,但是当项目足够大时,它将成为错误的来源。在这篇文章中,我将展示如何使对象不可变,并使它们雄辩而有效。默认方式不会有带有没有设置器的对象的设置器。这样做的唯一方法是创建一个新实例并将新值放置到负责的类本身。

Companies Mentioned

Mention Thumbnail
Mention Thumbnail
featured image - 学习如何在 Java 中使用不可变和可靠的对象
Artem Sutulov HackerNoon profile picture

在编写复杂的项目时,培养良好的代码文化至关重要。使用不可变和一致的对象是最重要的对象之一。


您可以忽略这一点并将复杂对象编写为标准对象,但当项目足够大时,它将成为错误的重要来源。


在我之前的文章中,我展示了如何提高标准对象的一致性和可靠性。简单来说:


  • 设置值时添加验证

  • 对每个可为空的字段使用java.util.Optional

  • 将复杂的突变放置在适当的位置 - 到负责的类本身。


但这些行动不足以制造完全可靠的物体。在本文中,我将展示如何使对象成为不可变对象,并使它们雄辩而有效。

问题

如果我们使用默认构造函数/getters/setters 创建一个简单的可序列化对象,则可以将其设为标准方式。但是让我们假设我们写了一些更复杂的东西。最有可能的是,它在无数地方被使用。


例如,它用于 HashMaps 并且可能用于多线程环境。所以,这似乎不再是一个好主意——以默认方式编写它。 打破HashMap没什么大不了的,线程之间的不一致状态不会让等待很长时间。


在考虑制作不可变对象时首先想到是:


  1. 不要制作setter方法
  2. 使所有字段最终
  3. 不要将实例共享给可变对象
  4. 不允许子类覆盖方法(本文将省略)


但是如何与那种物体相处呢?当我们需要更改它时,我们需要进行复制;如果不每次都复制粘贴代码和逻辑,我们怎么能做好呢?

关于我们的示例类的几句话

假设我们有帐户。每个帐户都有一个idstatusemail 。可以通过电子邮件验证帐户。当状态为CREATED时,我们不希望电子邮件被填写。但是当它是VERIFIEDINACTIVE时,必须填写电子邮件。



帐户状态


public enum AccountStatus { CREATED, VERIFIED, INACTIVE }


佳能Account.java实现:


 public class Account { private final String id; private final AccountStatus status; private final String email; public Account(String id, AccountStatus status, String email) { this.id = id; this.status = status; this.email = email; } // equals / hashCode / getters }


假设我们已经创建了一个帐户。然后,在业务逻辑的某个地方,我们需要更改电子邮件。


 var account = new Account("some-id", CREATED, null);


我们怎么能做到这一点?默认方式行不通,我们不能使用不可变类的 setter。


 account.setEmail("[email protected]");// we can not do that, we have no setters


做到这一点的唯一方法是创建一个新实例并将之前的值放置到构造函数中:


 var withEmail = new Account(account.getId(), CREATED, "[email protected]");


但这不是更改字段值的最佳方式,它会产生大量复制/粘贴,并且 Account 类不负责其一致性。

解决方案

建议的解决方案是提供来自 Account 类的变异方法,并在负责的类中实现复制逻辑。此外,必须为电子邮件字段添加必需的验证和 Optional 用法,这样我们就不会遇到 NPE 或一致性问题。


为了构建一个对象,我使用了“Builder”模式。它非常有名,并且有很多插件可以让您的 IDE 自动生成它。


 public class Account { private final String id; private final AccountStatus status; private final Optional<String> email; public Account(Builder builder) { this.id = notEmpty(builder.id); this.status = notNull(builder.status); this.email = checkEmail(builder.email); } public Account verify(String email) { return copy() .status(VERIFIED) .email(of(email)) .build(); } public Account changeEmail(String email) { return copy() .email(of(email)) .build(); } public Account deactivate() { return copy() .status(INACTIVE) .build(); } private Optional<String> checkEmail(Optional<String> email) { isTrue( notNull(email).map(StringUtils::isNotBlank).orElse(false) || this.status.equals(CREATED), "Email must be filled when status %s", this.status ); return email; } public static final class Builder { private String id; private AccountStatus status; private Optional<String> email = empty(); private Builder() { } public static Builder account() { return new Builder(); } public Builder id(String id) { this.id = id; return this; } public Builder status(AccountStatus status) { this.status = status; return this; } public Builder email(Optional<String> email) { this.email = email; return this; } public Account build() { return new Account(this); } } // equals / hashCode / getters }


如您所见,我们的类中有一个私有方法copy ,它返回带有精确副本的Builder 。这消除了所有字段的复制粘贴,但是,不能从外部Account.java访问此方法至关重要,因为在外部使用该 Builder 时,我们将失去对状态和一致性的控制。

以新方式更改帐户

现在,让我们创建一个帐户:


 var account = account() .id("example-id") .status(CREATED) .email((empty()) .build();


当我们需要更改电子邮件时,我们不需要负责创建副本,我们只需从Account本身调用一个方法:


 var withNewEmail = account.changeEmail("[email protected]");


在单元测试中对此进行演示:


 @Test void should_successfully_change_email() { // given var account = account() .id("example-id") .status(VERIFIED) .email(of("[email protected]")) .build(); var newEmail = "[email protected]"; // when var withNewEmail = account.changeEmail(newEmail); // then assertThat(withNewEmail.getId()).isEqualTo(account.getId()); assertThat(withNewEmail.getStatus()).isEqualTo(account.getStatus()); assertThat(withNewEmail.getEmail()).isEqualTo(of(newEmail)); }


为了验证帐户,我们不会创建状态为VERIFIED的副本和新电子邮件。我们简单地调用方法verify ,它不仅会为我们创建一个副本,还会检查电子邮件的有效性。


 @Test void should_successfully_verify_account() { // given var created = account() .id("example-id") .status(CREATED) .build(); var email = "[email protected]"; // when var verified = created.verify(email); // then assertThat(verified.getId()).isEqualTo(created.getId()); assertThat(verified.getStatus()).isEqualTo(VERIFIED); assertThat(verified.getEmail().get()).isEqualTo(email); }


结论

与不可变、一致和可靠的对象一起生活是很苛刻的,但如果以适当的方式,它会变得容易得多。


实施时,不要忘记

  1. 使所有字段最终
  2. 不提供二传手
  3. 不共享指向可变对象的链接
  4. 不允许子类覆盖方法
  5. 提供您班级中的变异方法
  6. 实施私人的在返回Builder的负责类中copy方法,并使用它在您的类中创建新实例。
  7. 通过对值使用验证来保持字段的一致性
  8. Optional与可为空的字段一起使用


您可以在GitHub上找到带有更多单元测试的完整工作示例。


Adam NieściorukUnsplash上拍摄的主要照片