Khái niệm về Immutable là vô cùng quan trọng trong hầu hết tất cả các ngôn ngữ, bao gồm cả Java. Với việc Java đưa ra phiên bản 8, Immutable đã trở nên quan trọng hơn. Phiên bản này giới thiệu Functional Programming, cũng như java.time
API mới. Cả hai đều phụ thuộc nhiều vào Immutable.
Một Immutable class là một class mà các Instance sẽ không thể chỉnh sửa. Thông tin được lưu trữ trong Immutable Object được cung cấp khi Object được tạo, và sau đấy nó không thể thay đổi và chỉ có thể đọc. Khi chúng ta không thể chỉnh sửa Immutable Object, chúng ta cần làm việc xung quanh điều này. Ví dụ, giả sử chúng ta có một class Spaceship
và chúng ta muốn thay đổi vị trí của nó, chúng ta phải trả về một Object mới với thông tin đã được chỉnh sửa
public Spaceship exploreGalaxy() { return new Spaceship(name, Destination.OUTER_SPACE); }
Ưu điểm Immutable
Nếu chỉ nghe qua bạn sẽ thấy Immutabe không có nhiều tác dụng, nhưng trong thực tế nó có rất nhiều ưu điểm.
Đầu tiên, Immutable class giảm đáng kể những cố gắng cần thiết để thực thi một hệ thống ổn định và có khả năng tránh được lỗi nghiêm trọng.
Các đặc tính của Immutable giúp ngăn chúng không bị thay đổi là vô cùng hữu ích khi tạo một kiểu hệ thống như thế.
Hãy hình dung rằng chúng ta có một class Bank
dùng để tạo cho một ngân hàng lớn. Sau khi thị trường tài chính rơi vào khủng hoảng, ngân hàng lo sợ cho phép các khách hàng của họ có số dư âm. Nên họ muốn đặt ra các quy tắc mới và thêm các phương thức kiểm tra để đưa ra một lỗi IllegalArgumentException
bất cứ khi nào một hàm trả về kết quả số dư âm. Kiểu quy tắc này được gọi là một bất biến.
public class BankAccount{ [...] private void validate(long balance) { if (balance < 0) { throw new IllegalArgumentException("balance must not be negative:"+ balance); } } }
Trong một class điển hình, phương thức validate()
này sẽ được gọi bất cứ khi nào số dư thay đổi. Giả sử khi khách hàng rút tiền, trả nợ, hoặc chuyển tiền từ tài khoản, chúng ta sẽ phải gọi hàm validate()
. Tuy nhiên với một Immutable class, chúng ta sẽ chỉ cần gọi hàm validate()
một lần trong constructor của class.
public BankAccount(long balance) { validate(balance); this.balance = balance; }
Bởi vì Immutable Object không bao giờ thay đổi, điều kiện này luôn là true
trong toàn bộ thời gian tồn tại của Object. Do đó việc kiểm tra sẽ không cần thiết nữa. Bất cứ khi nào phương thức thay đổi số dư được gọi, một Object mới sinh ra được trả về, gọi Constructor và thực hiện xác nhận lại số dư. Điều này là vô cùng hữu ích vì nó cho phép chúng ta tập trung tất cả các bất biến và bảo đảm các Object là phù hợp trong toàn bộ thời gian nó tồn tại.
Tương tự như vậy, Immutable được sử dụng để hỗ trợ cho hệ thống có khả năng tránh được lỗi xảy ra. Hãy hình dung rằng bạn muốn rút tiền từ ngân hàng, nhưng trong lúc đang rút tiền từ tài khoản và tiền đang được lấy ra khỏi ATM thì có một vài lỗi xảy ra. Đối với class thông thường thì tiền của bạn sẽ bị mất, Object của tài khoản cũng bị thay đổi. Nhưng đối với Immutable class sẽ có một lỗi được đưa ra ngăn tài khoản của bạn bị mất tiền trước khi bạn thực sự nhận được tiền.
public ImmutableAccount withdraw(long amount) { long newBalance = newBalance(amount); return new ImmutableAccount(newBalance); } private long newBalance(long amount) { // exception during balance calculation }
Một Immutable Object có thể không bao giờ gặp tình huống state không nhất quán, ngay cả trong trường hợp có lỗi xảy ra. Điều này giúp cho hệ thống ổn định và loại bỏ những lỗi không mong muốn làm cho hệ thống mất ổn định. Sự ổn định này không gây tốn kém ngoài việc phải kiểm tra khi khởi tạo.
Ưu điểm thứ hai của Immutable là chúng có thể được chia sẻ thoải mái giữa các Object. Có thể hiểu rằng chúng ta đang tạo các bản sao của tài khoản. Khi chúng ta sao chép Object, chúng ta để cho cả 2 Object sử dụng Object Blance
giống nhau
Tuy nhiên bởi vì cả 2 Object đều là Immutable, nên nếu chúng ta thay đổi Blance
của một Object thì Object còn lại sẽ không thay đổi. Một Object khác tạo một Immutable Instance mới của class, thì cả 2 Object không bị ảnh hưởng.
Với một lý do tương tự, Immutable không cần sao chép Constructor khi sao chép Object. Immutable thậm trí có thể được chia sẻ thoải mái khi sử dụng một thuật toán lock-free trong môi trường chạy đa luồng, nơi các hoạt động được chạy song song.
Immutable cũng là một lựa chọn hoàn hảo cho key của các phần tử trong Map và Set bởi vì các key này sẽ không bao giờ thay đổi.
Nhược điểm
Như bạn có thể thấy, sự cứng nhắc của Immutable có thể là ưu điểm lớn, nhưng nó cũng có thể là nhược điểm. Điểm yếu lớn nhất của Immutable là khả năng gặp vấn đề về hiệu suất. Mỗi khi bạn có một state mới cần thay đổi trong class, bạn cần tạo một Object mới. Do đó, bạn sẽ thường xuyên cần tạo nhiều Immutable Object hơn so với tạo các Mutable Object. Theo logic, khi chúng ta tạo nhiều Object, thì hệ thống sẽ cần cung cấp nhiều tài nguyên cho bạn sử dụng.
Đây có thể là vấn đề hoặc không, nó phụ thuộc nhiều vào yếu tố trong thực tế. Bạn đang thực hiện cái gì? Loại phần cứng mà chương trình đang hoạt động trên đó là gì? Bạn đang tạo một ứng dụng desktop hay web? Chương trình của bạn lớn mức nào? Dựa trên những yếu tố này giúp xác định có nên tạo một Immutable class hay không khi gặp vấn đề liên quan đến hiệu suất. Thông thường, chúng ta sẽ cố gắng sử dụng Immutable càng nhiều càng tốt. Ban đầu thì tất cả các class là Immutable và tạo điều kiện để nó Immutable bằng cách tạo các class nhỏ với một vài phương thức đơn giản. Code sạch sẽ và đơn giản là chìa khóa. Nếu bạn có code sạch, điều đó tạo điều kiện cho Immutable, và nếu bạn có các Immutable, code của bạn sẽ sạch hơn. Khi bạn có một chương trình, thực hiện test nó. Xem cách nó thực thi như thế nào, nếu hiệu suất không đạt yêu cầu, loại bỏ dần các Immutable.
Cách tạo Immutable
Chúng ta sẽ cùng nhau tìm hiểu cách tạo Immutable class. Hãy chuyển đổi class Spacship
từ Mutable sang Immutable class
public class Spaceship { public String name; public Destination destination; public Spaceship(String name) { this.name = name; this.destination = Destination.NONE; } public Spaceship(String name, Destination destination) { this.name = name; this.destination = destination; } public Destination currentDestination() { return destination; } public Spaceship exploreGalaxy() { destination = Destination.OUTER_SPACE; } […] }
Để biến một Mutable class thành Immutable thì bạn nên theo 3 bước sau:
- Tạo tất cả các trường là private hoặc final
- Không cung cấp bất kỳ phương thức nào có khả năng chỉnh sửa state
- Đảm bảo class không được extended hay kế thừa
- Đảm bảo hạn chế truy cập đến các trường Mutable
Tạo tất cả các trường là private hoặc final
Việc đầu tiên để tạo một Immutable class là thay đổi tất cả các trường thành private hoặc final. Chúng ta tạo các biến private nên sẽ không thể bị truy cập từ bên ngoài. Nếu chúng bị truy cập từ bên ngoài thì giá trị của nó có thể bị thay đổi. Chúng ta cũng tạo các trường final để truyền đi một thông điệp rằng các trường này sẽ không bị thay đổi trong class. Nếu ai đó cố gắng thay đổi giá trị thì một lỗi sẽ được đưa ra.
private final String name; private final Destination destination;
Không cung cấp bất kỳ phương thức nào có khả năng chỉnh sửa state
Tiếp theo chúng ta sẽ kiểm soát để cho các state của Object không bị thay đổi. Như đã được trình bày ở bên trên, định nghĩa về Immutable là các state của Object không bị thay đổi. Bất cứ khi nào bạn cung cấp một phương thức thay đổi state của Object thì một Object sẽ được tạo và trả về.
public ImmutableSpaceship exploreGalaxy() { return new ImmutableSpaceship(name, Destination.OUTER_SPACE); }
Bất kỳ trường nào mà chúng ta không thay đổi như là name
, có thể được sao chép từ Object hiện tại. Điều này là vì, như đã nói ở trên, các Object Immutable có thể được chia sẻ thoải mái. Các trường mà chúng ta cần thay đổi sẽ được khởi tạo trong một Object mới.
Đảm bảo class không được extended hay kế thừa
Để ngăn không cho class bị thay đổi, chúng ta cũng cần bảo vệ class không bị extends
. Nếu class bị extends
nó sẽ bị override
phương thức trong class. Một override
phương thức có thể thay đổi Object, vi phạm quy tắc của Immutable. Hãy cùng xem ví dụ sau
public class EvilSpaceship extends Spaceship { [...] @Override public EvilSpaceship exploreGalaxy() { this.destination = Destination.OUTER_SPACE; return this; } }
Để ngăn chặn EvilSpaceship
phá hủy quy tắc Immutbale, thiết lập class là final
public final class Spaceship
Đảm bảo hạn chế truy cập đến các trường Mutable
Cuối cùng chúng ta cần đảm bảo các trường Mutable là không thể truy cập. Nhớ rằng, các trường Immutable có thể được chia sẻ thoải mái, nên nó không gặp vấn đề gì nếu chúng ta độc quyền truy cập đến các trường. Bất kỳ ai có quyền truy cập đến các trường có thể thay đổi nó, bằng cách đó làm thay đổi Object của chúng ta. Để ngăn chặn một ai đó truy cập đến các trường Mutable, chúng ta không bao giờ nên lấy hoặc trả về một tham chiếu trực tiếp cho một Object đích đến. Thay vào đó chúng ta phải tạo một sao chép Mutable Object (deep copy) và làm việc với nó thay thế. Miễn là Mutable Object không bao giờ được chia sẻ trực tiếp. Một thay đổi bên trong Object bên ngoài sẽ không có bất kỳ thay đổi nào đến Immutable Object. Để đạt được truy cập độc quyền chúng ta phải kiểm tra tất cả các phương thức public và các Constructor cho các tham chiếu điểm đến đi vào và đi ra
Public Constructor không nhận bất kỳ tham chiếu điểm đến. Destination
Object được tạo an toàn và không thể bị truy cập từ bên ngoài, nên public Constructor là tốt như chính nó
Trong phương thức currentDestination()
, chúng ta trả về Destination Object là một vấn đề. Thay vì trả về một tham chiếu thực sự, tạo một sao chép cho Destination
Object và trả về một tham chiếu đến bản sao
public Destination currentDestination() { return new Destination(destination); }
Phương thức final public chúng ta có là phương thức newDestination()
. Nó nhận một tham chiếu Destination
và trực tiếp chuyển tiếp nó đến Constructor. Nghĩa là nó tham chiếu đến Object giống nhau như bất cứ cái gì được gọi phương thức này. Để ngăn chặn việc này, chúng ta có thể thực hiện sao chép trong phương thức này hoặc trong Constructor. Chúng ta sẽ thực thi điều này trong Constructor
private ImmutableSpaceship(String name, Destination destination) { this.name = name; this.destination = new Destination(destination); }
Tốt hơn hết là làm những thay đổi này bên trong một private Construcor, bởi vì nếu chúng ta tạo phương thức khác thay đổi destination
, chúng cũng tự động tạo bản sao của trường Mutable này
public final class ImmutableSpaceship { private final String name; private final Destination destination; public ImmutableSpaceship(String name) { this.name = name; this.destination = new Destination("NONE"); } private ImmutableSpaceship(String name, Destination destination) { this.name = name; this.destination = new Destination(destination); } public Destination currentDestination() { return new Destination(destination); } public ImmutableSpaceship newDestination(Destination newDestination) { return new ImmutableSpaceship(this.name, newDestination); } […] }