paint-brush
Học cách sống với các đối tượng bất biến và đáng tin cậy trong Javatừ tác giả@artemsutulov
624 lượt đọc
624 lượt đọc

Học cách sống với các đối tượng bất biến và đáng tin cậy trong Java

từ tác giả Artem Sutulov1m2022/05/28
Read on Terminal Reader
Read this story w/o Javascript

dài quá đọc không nổi

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. Sử dụng các đối tượng bất biến 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 khi dự án phát triển đủ. Trong bài viết này, tôi sẽ trình bày 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ả. Cách mặc định sẽ không có bộ định tuyến với một đối tượng không có bộ định vị. Cách duy nhất để làm điều đó là tạo một thể hiện mới và đặt các giá trị mới vào chính lớp chịu trách nhiệm.

Companies Mentioned

Mention Thumbnail
Mention Thumbnail
featured image - Học cách sống với các đối tượng bất biến và đáng tin cậy trong Java
Artem Sutulov HackerNoon profile picture

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ả.

Vấn đề

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à:


  1. Không thực hiện các phương thức setter
  2. Làm cho tất cả các trường cuối cùng
  3. Không chia sẻ các phiên bản cho các đối tượng có thể thay đổi
  4. Không cho phép các lớp con ghi đè các phương thức (tôi sẽ bỏ qua nó trong bài viết này)


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?

Vài lời về các lớp mẫu của chúng tôi

Giả sử chúng ta có Tài khoản. Mỗi tài khoản có một id , statusemail . 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.



Các trạng thái tài khoả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ó.

Dung dịch

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.

Thay đổi tài khoản theo cách mới

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ự kết luận

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, đừng quên :

  1. Làm cho tất cả các trường cuối cùng
  2. Không cung cấp người thiết lập
  3. Không chia sẻ liên kết đến các đối tượng có thể thay đổi
  4. Không cho phép các lớp con ghi đè các phương thức
  5. Cung cấp các phương pháp đột biến từ lớp của bạn
  6. Thực hiện riêng 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.
  7. Duy trì tính nhất quán của các trường bằng cách sử dụng xác nhận giá trị
  8. Sử dụng 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