Bài 1: Sử dụng static factory methods thay vì sử dụng constructors
Static factory method và constructor là gì?
Như các bạn đã biết, Java là ngôn ngữ lập trình hướng đối tượng, các thành phần cấu thành nên một chương trình được biểu diễn dưới dạng class.
Cũng như các ngôn ngữ lập trình khác, khi tạo một đối tượng chúng ta thường hay gọi các hàm được gọi là constructor và hàm này thường có tên là new()
Còn static factory mehod được nhắc đến ở đây là gì? Nó cũng tương tự mục đích sử dụng của constructor là khởi tạo một object mới. Tuy nhiên đó gần như là một function thông thường nhưng làm nhiệm vụ của constructor.
Thông thường, constructor được hỗ trợ một cách native trong đại đa số các ngôn ngữ lập trình, chúng có cách viết khác hơn so với một hàm thông thường, nhưng bản chất bên dưới vẫn là một function của class đó, không hơn không kém.
Vấn đề của constructor mà Static factory method giải quyết được
Để dễ hình dung, chúng ta sẽ tìm hiểu bằng ví dụ cho dễ hiểu
public class User {
private String username;
private String email;
private String googleToken;
private boolean guest;
public User(String username, String email) {
// User thông thường
}
public User(String googleToken, boolean fromGoogle) {
// User từ Google OAuth
}
public User(boolean guest) {
// Guest user
}
}
Khởi tạo object với constructor
User user1 = new User(
"tuan",
"tuan@gmail.com"
);
User user2 = new User(
"eyJhbGciOi...",
true
);
User user3 = new User(
true
);
Ở đoạn code trên, cùng là hàm new() của constructor nhưng logic bên trong được quyết định bằng số lượng cũng như kiểu dữ liệu của các arguments được truyền vào.
Cùng trường hợp đó, nếu chúng ta sử dụng static factory method thì trông nó sẽ như thế nào?
public class User {
private User(...) {
}
public static User register(
String username,
String email) {
return new User(...);
}
public static User fromGoogle(
String googleToken) {
return new User(...);
}
public static User guest() {
return new User(...);
}
}
Nếu muốn khởi tạo các object từ static factory method thì user cần thực hiện
User user1 =
User.register(
"tuan",
"tuan@gmail.com"
);
User user2 =
User.fromGoogle(
"eyJhbGciOi..."
);
User user3 =
User.guest();
Hơi khó nhìn một chút nhưng chúng ta sẽ để hai ví dụ đó ở trên và cùng nhau phân tính vì sao static factory method được xem là tốt hơn.
1. Các hàm khởi tạo được đại diện bởi tên thay vì cách truyền tham số
Việc sử dụng các tham số khác nhau để diễn đạt object như thế nào sẽ được trả về theo mình thấy là ý tưởng tồi. Chúng ta thực tế chẳng biết thứ tự của chúng như thế nào để trả ra một object mong muốn trừ khi phải lần mò đến nơi object đó được khai báo, điều này khó khăn hơn rất nhiều so với việc chúng ta đặt tên các hàm khởi tạo một cách meaningful. Như ở trong ví dụ trên, mỗi khi các function được show ra từ code editor, chỉ cần đọc qua tên hàm chúng ta đã hiểu sơ qua những gì được trả về, hoặc ít nhất nhắc chúng ta nhớ đến những gì mà mình đã viết hoặc đã đọc về class đó trong quá khứ.
2. Không nhất thiết phải tạo mới một object mỗi khi được gọi
Như chúng ta đã nói ở trên, constructor được cung cấp một cách native bởi ngôn ngữ lập trình, do đó có những thứ chúng ta không thể nào thay đổi một cách tự do như cách một static factory function có thể làm, trong ngữ cảnh chúng ta đang nói đến là việc không cần tạo mới một object mỗi khi hàm được gọi. Constructor mặc định phải trả về một object mới, còn static factory function thì không. Nói như thế không phải static factory function không trả về gì cả, mà là nó không tạo mới nhiều lần mỗi khi hàm được gọi, chỉ là cho phép chúng ta chỉ sử dụng chung một object duy nhất, hàm trả về reference đến object đó thay vì tạo mới. Điều này sẽ hữu ích đối với các biến có giá trị immutable.
public final class AppConfig {
private static final AppConfig INSTANCE =
new AppConfig(
System.getenv("DB_URL"),
System.getenv("DB_USER")
);
private AppConfig(
String dbUrl,
String dbUsername
) {
...
}
public static AppConfig getInstance() {
return INSTANCE;
}
}
Để hiểu về final bạn có thể tìm hiểu thêm vì sao nó xuất hiện ở đây. Nôm na nó sẽ đánh dấu class là immutable hoặc nói cách khác là không thể được kế thừa bởi một class khác, việc cho phép kế thừa sẽ phá vỡ tính immutable của nó. Thông thường, không ai lại đi chỉnh sửa config được load lên từ biến môi trường cả, đại đa số nó sẽ bất biến. Nhưng điều gì xảy ra nếu như chúng ta gọi hàm contructor ở nhiều nơi? Nó sẽ tạo ra nhiều instance của class đó mặc cho giá trị bên trong của tất cả object đó đều là như nhau, dẫn đến tốn tài nguyên. Với cách làm như trên, tất cả những nơi sử dụng giá trị app config đều được trỏ tới cùng một vùng nhớ, tối ưu không gian lưu trữ trên RAM.
3. Không nhất thiết phải trả về kiểu class mà hàm khởi tạo được viết
public interface Shape {
double area();
}
public final class Shapes {
public static Shape circle(double radius) {
return new Circle(radius);
}
public static Shape rectangle(double w, double h) {
return new Rectangle(w, h);
}
}
class Circle implements Shape {
private final double radius;
Circle(double radius) {
this.radius = radius;
}
@Override
public double area() {
return Math.PI * radius * radius;
}
}
class Rectangle implements Shape {
private final double width;
private final double height;
Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
@Override
public double area() {
return width * height;
}
}
Shape s1 = Shapes.circle(5);
Shape s2 = Shapes.rectangle(10, 20);
Trong ví dụ trên, mặc dù chúng ta đang khởi tạo một hình tròn và một hình chữ nhật nhưng không sử dụng hàm của class Circle và Rectangle. Không giống như constructor, chúng có thể trả về một object thuộc bất kỳ subtype nào của kiểu trả về đã khai báo. Điều này mang lại sự linh hoạt rất lớn trong việc lựa chọn class của object được trả về.
Một ứng dụng của sự linh hoạt này là API có thể trả về object mà không cần công khai (public) class triển khai của chúng. Việc ẩn các implementation class theo cách này giúp API trở nên gọn nhẹ hơn rất nhiều. Kỹ thuật này đặc biệt phù hợp với các framework dựa trên interface, nơi interface đóng vai trò là kiểu trả về tự nhiên cho các static factory method.
4. Quyết định subtype dựa trên input
Ở ưu điểm thứ 3, static factory method có thể trả về bất kỳ subtype nào của kiểu đã khai bảo (Khai báo Shapes thì kiểu dữ liệu trả về là Circle hoặc Rectangle. Ở ưu điểm thứ 4, nó sẽ mở rộng hơn rằng static factory method có thể quyết định trả về subtype nào dựa trên input. Tức là không chỉ Shape.circle trả về Circle mà Shape.create(...) có thể trả về các implementation khác nhau tùy tình huống. Hãy cùng đọc ví dụ sau để thấy được ưu điểm nêu trên
public interface Cache {
void put(String key, Object value);
Object get(String key);
}
class SmallCache implements Cache {
}
class LargeCache implements Cache {
}
public final class Caches {
public static Cache create(int expectedSize) {
if (expectedSize < 1000) {
return new SmallCache();
}
return new LargeCache();
}
}
Cache c1 = Caches.create(100);
Cache c2 = Caches.create(100000);
Như chúng ta có thể thấy ở trên, Cache được coi như class cha và chúng ta có static factory method ở đây là create(). Chúng ta triển khai logic xử lý bên trong static factory method để quyết định trả về kiểu dữ liệu gì dựa trên dữ liệu đầu vào.
5. Class của object được trả về không nhất thiết phải tồn tại khi class chứa method được viết
Giá sử hôm nay chúng ta viết
public interface PaymentService {
void pay(long amount);
}
public class PaymentServices {
public static PaymentService getService() {
return new PaypalPaymentService();
}
}
Tại thời điểm code, khi chúng ta sử dụng return new PaypalPaymentService(); là chúng ta đang biết rõ được implementation là gì. Nhưng hãy thử tưởng tượng sản phẩm của chúng ta là một framwork và bạn muốn cho phép người khác viết StripePaymentService, MomoPaymentService, ZaloPaymentService trong tương lai, nhưng ngay tại thời điểm chúng ta viết PaymentServices thì các class này còn chưa tồn tại, vậy thì làm sao để PaymentServices.getService() có thể trả về chúng?
Vấn đề được giải quuết bằng cách yêu cầu Provider đăng ký với framework. Nhưng bằng cách nào?
Thành phần 1: Service Interface
public interface PaymentService {
void pay(long amount);
}
Đây là thứ mà client nhìn thấy
Thành phần 2: Provider Registration API
public final class PaymentServices {
private static final Map<String, PaymentService>
PROVIDERS = new HashMap<>();
private PaymentServices() {
}
public static void register(
String name,
PaymentService provider) {
PROVIDERS.put(name, provider);
}
public static PaymentService getService(
String name) {
return PROVIDERS.get(name);
}
}
Lúc này, user sẽ đăng ký service của họ bằng cách
PaymentServices.register(new StripePaymentService());
hoặc
PaymentServices.register(new MomoPaymentService());
Note: Tất nhiên các Service mà user đăng ký cần kế thừa
Điều này chỉ có static factory method mới làm được, bởi khi khai báo sử dụng constructor bằng new StripePaymentService() thì kiểu dữ liệu cần được biết chính xác tại thời điểm biên dịch.
Nhược điểm của static factory là gì?
Không thể subclass
Giá sử chúng ta có một immutable class
public final class Money {
private final int amount;
private Money(int amount) {
this.amount = amount;
}
public static Money of(int amount) {
return new Money(amount);
}
}
Người dùng có thể tạo object bằng Money money= Money.of(100) nhưng họ không thể extend từ class đó
class SpeciallMoney extends Money {}
Bởi vì Constructor là private và class còn là final
Nhưng có một cách work aroud hơi tà đạo để xử lý vấn đề này đó là sử dụng composition thay vì inheritance
Thay vì viết như sau
class AuditUserService extends UserService {
}
Chúng ta có thể sử dụng nó bằng cách
class AuditUserService {
private final UserService delegate;
}
Các hàm mà giới lập trình java thường mặc định về nhiệm vụ của nó mỗi khi nhắc đến tên
- from — Phương thức chuyển đổi kiểu (type-conversion method), nhận một tham số duy nhất và trả về một instance tương ứng của kiểu này. Ví dụ:
Date d = Date.from(instant); - of — Phương thức tổng hợp (aggregation method), nhận nhiều tham số và trả về một instance của kiểu đó chứa hoặc kết hợp các giá trị được truyền vào. Ví dụ:
Set<Rank> faceCards = EnumSet.of(JACK, QUEEN, KING); - valueOf — Một cách đặt tên dài hơn và tường minh hơn cho from hoặc of. Ví dụ:
BigInteger prime = BigInteger.valueOf(Integer.MAX_VALUE); - instance hoặc getInstance — Trả về một instance được mô tả bởi các tham số truyền vào (nếu có), nhưng không nhất thiết phải có cùng giá trị với các tham số đó. Ví dụ:
StackWalker luke = StackWalker.getInstance(options); - create hoặc newInstance — Tương tự instance hoặc getInstance, nhưng đảm bảo rằng mỗi lần gọi đều trả về một instance mới. Ví dụ:
Object newArray = Array.newInstance(classObject, arrayLen); - getType — Tương tự getInstance, nhưng được sử dụng khi factory method nằm trong một class khác. Type là kiểu object được trả về bởi factory method. Ví dụ:
FileStore fs = Files.getFileStore(path); - newType — Tương tự newInstance, nhưng được sử dụng khi factory method nằm trong một class khác. Type là kiểu object được trả về bởi factory method. Ví dụ:
BufferedReader br = Files.newBufferedReader(path); - type — Một phiên bản ngắn gọn hơn của getType và newType. Ví dụ:
List<Complaint> litany = Collections.list(legacyLitany);
Tổng kết
Theo cá nhân mình thấy, những tiện ích mà static factory method đem lại đến từ việc sử dụng gần như một hàm thông thường, bởi constructor method được tạo ra nhằm mục đích gốc là tạo ra một instance của class do đó chúng thiết kế chỉ để làm chuyện này khiến cho nó có những giới hạn nhất định. Nhưng không phải vì số lợi ích mà chúng đem lại khiến chúng ta mặc định phải sử dụng static factory method thay vì constructor, trong nhiều trường hợp thì constructor mang lại lợi ích nhiều hơn, điều này cần phải xem xét dưới góc độ người sử dụng bởi nói gì thì nói, constructor vẫn là thứ gọn nhẹ và chứa ít logic hơn static factory method.
All rights reserved