Khi viết các dự án phức tạp, điều cần thiết là phải phát triển một văn hóa mã tốt. Việc sử dụng các đối tượng không thay đổi và nhất quán là một trong những điều quan trọng nhất.
Bạn có thể bỏ qua điều này và viết các đối tượng phức tạp như những đối tượng tiêu chuẩn, nhưng nó sẽ là một nguồn lỗi đáng kể khi dự án phát triển đủ.
Trongbài viết trước, tôi đã chỉ ra cách chúng ta có thể cải thiện tính nhất quán và độ tin cậy của các đối tượng tiêu chuẩn. Trong một vài từ:
Thêm xác thực khi đặt giá trị
Sử dụng java.util.Optional
cho mỗi trường nullable
Đặt các đột biến phức tạp ở một nơi thích hợp - cho chính lớp chịu trách nhiệm.
Nhưng những hành động đó không đủ để làm cho các đối tượng hoàn toàn đáng tin cậy. Trong bài viết này, tôi sẽ chỉ ra cách làm cho các đối tượng trở nên bất biến và làm cho chúng trở nên hùng hồn và hiệu quả.
Nếu chúng ta tạo một đối tượng có thể tuần tự hóa đơn giản với hàm tạo / getters / setters mặc định, thì bạn có thể làm cho nó theo cách chuẩn. Nhưng giả sử chúng ta viết một cái gì đó phức tạp hơn. Và rất có thể, nó được sử dụng ở vô số nơi.
Ví dụ: nó được sử dụng trong HashMaps và có thể trong môi trường đa luồng. Vì vậy, có vẻ như không còn là một ý tưởng hay nữa - hãy viết nó theo cách mặc định. Việc phá vỡ HashMap không phải là một vấn đề lớn và trạng thái không nhất quán giữa các luồng sẽ không khiến bạn phải chờ đợi lâu.
Những điều đầu tiên xuất hiện trong đầu khi nghĩ đến việc tạo ra các vật thể bất biến là:
Nhưng làm thế nào để sống chung với loại đối tượng đó? Khi chúng ta cần thay đổi nó, chúng ta cần sao một bản sao; làm thế nào chúng ta có thể làm tốt điều đó mà không cần sao chép mã và logic mỗi lần?
Giả sử chúng ta có Tài khoản. Mỗi tài khoản có một id
, status
và email
. Tài khoản có thể được xác minh qua email. Khi trạng thái được CREATED
, chúng tôi không mong đợi email sẽ được lấp đầy. Nhưng khi nó được VERIFIED
hoặc INACTIVE
, email phải được điền.
public enum AccountStatus { CREATED, VERIFIED, INACTIVE }
Việc triển khai canon 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 }
Hãy tưởng tượng chúng tôi đã tạo một tài khoản. Sau đó, ở đâu đó trong logic kinh doanh, chúng ta cần thay đổi một email.
var account = new Account("some-id", CREATED, null);
Làm thế nào chúng ta có thể làm điều đó? Cách mặc định sẽ không hoạt động, chúng tôi không thể có bộ định tuyến với một lớp bất biến.
account.setEmail("[email protected]");// we can not do that, we have no setters
Cách duy nhất để làm điều đó là tạo một phiên bản mới và đặt các giá trị trước đó của hàm tạo:
var withEmail = new Account(account.getId(), CREATED, "[email protected]");
Nhưng đó không phải là cách tốt nhất để thay đổi giá trị của một trường, nó tạo ra rất nhiều bản sao / dán và lớp Tài khoản không chịu trách nhiệm về tính nhất quán của nó.
Giải pháp được đề xuất là cung cấp các phương thức đột biến từ lớp Tài khoản và thực hiện sao chép logic bên trong lớp chịu trách nhiệm. Ngoài ra, điều cần thiết là phải thêm các xác thực bắt buộc và sử dụng Tùy chọn cho trường email, vì vậy chúng tôi sẽ không gặp vấn đề về NPE hoặc tính nhất quán.
Để xây dựng một đối tượng, tôi sử dụng mẫu 'Builder'. Nó khá nổi tiếng và có rất nhiều plugin cho IDE của bạn để tạo nó tự động.
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 }
Như bạn có thể thấy, có một copy
phương thức riêng trong lớp của chúng tôi trả về Builder
với một bản sao chính xác. Điều đó sẽ loại bỏ việc sao chép dán của tất cả các trường, tuy nhiên, điều quan trọng là không thể truy cập phương pháp này từ bên ngoài Account.java
bởi vì, với Builder đó bên ngoài, chúng tôi mất quyền kiểm soát trạng thái và tính nhất quán.
Bây giờ, hãy tạo một tài khoản:
var account = account() .id("example-id") .status(CREATED) .email((empty()) .build();
Khi cần thay đổi email, chúng tôi không cần phải chịu trách nhiệm tạo bản sao, chúng tôi chỉ cần gọi một phương thức từ chính Account
:
var withNewEmail = account.changeEmail("[email protected]");
Chứng minh điều đó trong một bài kiểm tra đơn vị:
@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)); }
Để xác minh tài khoản, chúng tôi không tạo bản sao có trạng thái VERIFIED
và email mới. Chúng tôi chỉ gọi phương thức là verify
, phương thức này không chỉ tạo bản sao cho chúng tôi mà còn kiểm tra tính hợp lệ của email.
@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); }
Sống với những đối tượng bất biến, nhất quán và đáng tin cậy là một điều khắc nghiệt, nhưng theo cách thích hợp, nó có thể trở nên dễ dàng hơn rất nhiều.
Khi triển khai một,
copy
phương thức bên trong lớp chịu trách nhiệm trả về một Bộ Builder
và sử dụng nó để tạo các phiên bản mới bên trong lớp của bạn.Optional
với các trường vô hiệu
Bạn có thể tìm thấy ví dụ hoạt động đầy đủ với nhiều bài kiểm tra đơn vị hơn trên GitHub .
Ảnh chính của Adam Nieścioruk trên Unsplash