Dependency Injection với Abstract Factory


Bài viết này, chúng ta cùng tìm hiểu ưu và nhược điểm của Dependency Injection (DI - “tiêm ” phụ thuộc) với một số cách khác nhau dùng Abstract Factory design pattern. Đặt trong ngữ cảnh của một số vấn đề như: Tạo stateful objects với tham số, điều khiển kiểm tra exceptions khi tạo object, và gắn động các objects. Mà các IoC frameworks, như Spring IoC container, PicoContainer và Guice, không đưa ra giải pháp tốt nhất cho các trường hợp này.

Abstract Factory Design Pattern cho Dependency Injection


GoF* Abstract Factory design pattern được tóm gọn theo 2 bước:
Một factory interface thay thế cho một class abstract factory (Optional),
Mọi factory method chịu trách nhiệm tạo một object và tiêm phụ thuộc vào nó.
Xét ví dụ đơn giản sau:

ComponentA phụ thuộc vào ComponentB. Để có thể unit-testing class ComponentAImpl, hiện thực của interface ComponentB phải tiêm vào ComponentAImpl. Đoạn code sau minh hoạ cách dùng Abstract Factory design pattern để tiêm phụ thuộc.

//Abstract Factory for Dependency Injection
//Factory interface
public interface Module1ServiceFactory {

ComponentA getComponentA();

ComponentB getComponentB();
}

//Concrete factory
public class Module1ServiceFactoryImpl implements Module1ServiceFactory {

private ComponentA componentA;
private ComponentB componentB;
private Module1Servicefactory instance;

private Module1ServicefactoryImpl() {
}

public static synchronized Module1ServiceFactory getInstance() {
if (null == instance) {
instance = new Module1ServicefactoryImpl();
componentA = new ComponentAImpl();
componentB = new ComponentBImpl();
componentA.setComponentB(componentB);
}
return instance;
}

public ComponentA getComponentA() {
return componentA;
}

public ComponentB getComponentB() {
return componentB;
}
}

//Client
public class Client {

public static void main(String[] args) {
Module1ServiceFactory m1sf =
Module1ServicefactoryImpl.getInstance();
ComponentA componentA = m1sf.getComponentA();
componentA.operationA1();
}
}


Lazy-instantiation


Lazy-instantiation có thể thực hiện bàng cách thay đổi phương thức để có thể xem như objects đã được tạo và chỉ lấy ra khi cần. Để làm điều này, getComponentA() có thể chỉnh lại như sau:

public synchronized ComponentA getComponentA() { if (null == componentA) { componentA = new ComponentAImpl(); } return componentA; }

Dĩ nhiên Module1ServiceFactoryImpl.getInstance() phải được sửa lại không tạo objects.
Ta có thể thêm parameter cho Module1ServiceFactoryImpl .getInstance() để cho biết nó có cần tạo objects hay không.

Non-Singleton Scope


Đoạn code trên sẽ tạo singleton objects. Nếu một object mới chỉ cần khi gọi phương thức getComponentA() và getComponentB(), thì ta sửa lại như sau:


//Concrete factory
public class Module1ServiceFactoryImpl {
private Module1ServiceFactory instance;

private Module1ServiceFactoryImpl() {}

public static synchronized Module1ServiceFactory getInstance() {
if (null == instance) {
instance = new Module1ServiceFactoryImpl();
}

return instance;
}

public ComponentA getComponentA() {
ComponentA componentA = new ComponentAImpl();
ComponentB componentB = getComponentB();
componentA.setComponentB(componentB);
return componentA;
}


public ComponentB getComponentB() {
return new ComponentBImpl();
}
}


Ta có thể tiêm một singleton object vào một non-singleton object. Ví dụ, nếu ComponentB là một singleton, và ComponentA là một non-singleton, ta có thể làm như sau:

//Concrete factory
public class Module1ServiceFactoryImpl {
private Module1ServiceFactory instance;
private ComponentB componentB;

private Module1ServicefactoryImpl() {}

public static synchronized Module1ServiceFactory getInstance() {
if (null == instance) {
instance = new Module1ServiceFactoryImpl();
componentB = new ComponentBImpl();
}

return instance;
}

public ComponentA getComponentA() {
ComponentA componentA = new ComponentAImpl();
componentA.setComponentB(componentB);
return componentA;
}

public ComponentB getComponentB() {
return componentB;
}
}

Ngược lại ta cũng có thể tiêm một non-singleton object cho một singleton, tuy nhiên trường hợp này rất ít gặp.

Tạo Local Stateful Objects với tham số cho Singletons


Đây là vấn đề đặt trưng cho tất cả các IoC frameworks. Được minh hoạ bằng phương thức cho ComponentA singleton:

public void operationA2() {
String s = aPrivateMethod();
int i = anotherMethod();
ComponentC componentC = new ComponentCImpl(s, i);
//do something else.
}

Ở đây, ComponentAImpl dùng ComponentC. Hiển nhiên là chúng ta cần tiêm cho nó một hiện thực của ComponentC là ComponentCImpl. Tuy nhiên, chúng ta không thể tạo biến componentC khi nó là một stateful object với trạng thái chỉ dùng được trên client và sẽ không thể dùng trong clients khác thông qua threads. Vậy, nó không thể tiêm dùng setter hay constructor.
Có 2 giải pháp để giải quyết vấn đề này dùng Abstract Factory pattern. In both schemes, chúng ta cần thay đổi Module1ServiceFactory và thêm một tham số cho factory method:

ComponentC getComponentC(String s, int i);

Và hiện thực phương thức này trong Module1ServiceFactoryImpl:

public ComponentC getComponentC(String s, int i) {
return new ComponentCImpl(s, i);
}

Giải pháp thứ nhất là tiêm factory object vào class có local stateful objects cần:

private Module1ServiceFactory factory;
public void setModule1ServiceFactory(Module1ServiceFactory factory) {
this.factory = factory;
}

ComponentAImpl.operationA2() trở thành:

public void operationA2() {
String s = aPrivateMethod();
int i = anotherMethod();
ComponentC componentC = factory.getComponentC(s, i);
//do something else.
}

Bất lợi của cách này là ComponentAImpl phụ thuộc vào Module1ServiceFactory và chúng ta cần cung cấp một mock hiện thực Module1ServiceFactory để unit-test ComponentAImpl. Mặc dù có nhược điểm như vậy, nhưng tiêm factory object vào một class dùng trong việc tạo local stateful objects là đơn giản nhất. Và được dùng nhiều nhất trong J2EE technology. Ví dụ, trong JPA (Java Persistence API), một entity manager factory có thể tiêm vào code, và một application-managed entity manager được tạo từ entity manager factory.
Cách thứ 2 để tạo local stateful objects là chuyển phương thức sang abstract class, có thể dùng trong unit-tested, và có objects phụ thuộc cụ thể trong subclass.

public abstract class AbstractComponentA implements ComponentA {
public void operationA2() {
String s = aPrivateMethod();
int i = anotherMethod();
ComponentC componentC = getComponentC(s, i);
//do something else.
}

public abstract ComponentC getComponentC(String s, int i) ;

}

public class ComponentAImpl extends AbstractComponentA {
public ComponentC getComponentC(String s, int i) {
return new ComponentCimpl(s, i);
}

}

Cách này được dùng nhiều trong Method Injection của Springframework chỉ hỗ trợ trường hợp không cần tham số khi tạo stateful objects. Unit-testing code không cần hiện thực mock factory. Tuy nhiên, đó là một giải pháp nguy hiểm. Giả sử chúng ta có 10 local stateful objects trong, chúng ta cần 10 abstract methods cho mục đích dùng unit-testing. Đúng là có thể dùng unit-testing và dùng rất đơn giản – at the expense of cluttered application code.
Springframework đưa ra phương thức có thể giải quyết vấn đề này dùng Java reflection. Nhưng nó là một cách rất phức tạp và không phù hợp cho một application bình thường.

Kiểm tra Exceptions Thrown khi tạo Object


Đây là vấn đề mà một IoC container thường làm không tốt. Nếu kiểm tra exceptions được tung ra trong quá trình tạo object, ứng dụng có thể muốn bắt và thu lại. Lấy ví dụ một web service client, nếu web service chưa được tạo khi client gọi, client có thể bắt exception mà nó tung ra, hiện messages phù hợp và đề nghị thực hiện lại. Trường hợp này khó cho IoC containers để tung ra lổi đã được khai báo trong ứng dụng. Factories có thể dể dàng điều khiển trạng thái – chúng ta có thể đơn giản khai báo exceptions cần kiểm tra và tung lổi trong factory methods và cho phép application code điều khiển và thu lại ngay tại đó.

Gắn linh động


Là trường hợp có nhiều hiện thực khác nhau của cùng một interface cần tiêm vào một object khác nhau. Một ví dụ hay là Strategy design pattern. Một tham số có thể dùng để cho biết class cụ thể nào cần được tiêm. Trường hợp này khó hiện thực theo kiểu tiêm phụ thuộc trong file XML hỗ trỡ trong IoC containers, chỉ đơn giản cung cấp một cách để lắp các objects theo cách tĩnh. Lập trình IoC containers có thể cho phép giải quyết cấn đề phụ thuộc động. Tuy nhiên, tiêm phụ thuộc bằng tay với Abstract Factory cung cấp một cách đơn giản nhất và và là một giải pháp đơn giản nhất trong trường hợp này, như ta thấy trong ví dụ sau:

//Concrete factory
public class Module1ServiceFactoryImpl {
...

public ComponentA getComponentA(int strategy) {
ComponentA componentA = new ComponentAImpl();
ComponentB componentB = getComponentB(strategy);
componentA.setComponentB(componentB);
return componentA;
}


public ComponentB getComponentB(int strategy) {
switch(strategy) {
case STRATEGYA:
return new StrategyA();
case STRATEGYB:
return new StrategyB();
default:
return null;
}
}
}

Trong ví dụ này, cả StrategyA và StrategyB đều là hiện thực của interface ComponentB.

Tổng kết


Các trường hợp đã được đưa ra cho vấn đề tiêm phụ thuộc với Abstract Factory design pattern bao gồm tạo local stateful objects từ tham số động, điều khiển kiểm tra exceptions trong quá trình tạo object, và gắn linh động. Bên cạnh đó, vấn đề là một cách tốt nhất để so sánh với các IoC containers từ dùng Java code và gắn cứng (XML). Bất lợi ở chổ developers cần viết nhiều code hơn để bắt đầu. Một hạn chế khác là hiện thực của factory code thay đổi đáng kể nếu chúng ta đổi giựã lazy-initialization và eager-initialization hay từ singletons đến non-singletons. Tuy nhiên, nó có thể chứng tỏ rằng it is rare that we need to make these kinds of changes.
Vấn đề chính là cho phép unit-testing là lập trình dùng interfaces để classes không dựa vào classes cụ thể trong ứng dụng. Để mock objects có thể tiêm vào một class khi test và thi hành tất cả sự phụ thuộc.
Phụ thuộc đã được giải quyết, theo một cách nào đó trong ứng dụng. Một ý tưởng thường thấy phía sau tất cả IoC containers và các giải pháp cho DI là phụ thuộc được giải quyết theo cách dể hình dung – XML configuration files (Spring IoC), đặc biệt Java classes (Google Guice), hay factory classes cụ thể. Trong trường hợp này, chúng ta tránh sự lan rộng của phụ thuộc thông qua code ứng dụng và một ít class cụ thể có thể dễ dàng. Mock objects có thể dùng để unit-testing.
Và cuối cùng, với IoC containers, chúng ta loại trừ phụ thuộc vào classes cụ thể và có thể tạo unit-testing, theo cách mà họ cung cấp – thông qua các thirty-party APIs hay cấu hình trên file XML. Khi không có một chuẩn cho IoC containers và mổi framework cung cấp các tính năng và chức năng khác nhau chung quanh vấn đề tiêm phụ thuộc, thì ta khó mà thay thế một IoC framework để dùng một cái khác.

*GoF: Gang of Four. Người ta thường gọi quyển sách Design Patterns: Elements of Reusable Object-Oriented Software với cái tên này vì có 4 tác giả viết nó.

Dịch từ TheServerSide

Cao Trong Hien

,

0 Responses to "Dependency Injection với Abstract Factory"

Đăng nhận xét