Dependency Inversion, Inversion of Control and Dependency Injection
This post hasn't been updated for 8 years
Giới thiệu
Chào mọi người, chắc hẳn trong giới lập trình phần mềm của chúng ta, ai cũng ít nhất một lần nghe đến các khái niệm như SOLID, OOP Design, Dependency Inversion hay IoC ... Tuy nhiên không hẳn ai cũng hiểu rõ và thực hành thành công. Theo nhận thức của tôi, rất nhiều kỹ sư phần mềm đã đi làm thực tế được 1-2 năm, thậm chí là 3-4 năm vẫn còn mơ hồ về các khái niệm này và dĩ nhiên ứng dụng nó trong lập trình sẽ cực kỳ khó. Bản thân tôi, trong khả năng của mình cũng chưa dám chắc đã hiểu rõ về tất cả các khái niệm đó. Tuy nhiên, trong giới hạn bài viết này, tôi sẽ chia sẻ một số hiểu biết dựa trên lý thuyết và kinh nghiệm làm việc của bản thân để làm rõ thêm những khái niệm nêu trên mà theo tôi là vô cùng quan trọng và cần thiết đối với một lập trình viên.
Dependency Inversion
-
Dám chắc rằng chúng ta đều biết đến một khái niệm vô cùng quan trọng trong lập trình, đó là OOP Design - Thiết kế hướng đối tượng. Mọi dự án phần mềm làm ra không những đòi hỏi tính đúng đắn về business, về giao diện người dùng, về chức năng ... mà còn đòi hỏi sự dễ dàng trong việc mở rộng và maintain. Thiết kế hướng đối tượng ra đời với mục đích đó. Trải qua nhiều dự án phần mềm, người ta đã rút ra được 5 nguyên tắc cơ bản trong Thiết kế hướng đối tượng đó là :
- Single responsibility principle
- Open/closed principle
- Liskov substitution principle
- Interface segregation principle
- Dependency inversion principle
Trong bài viết hôm nay, tôi sẽ làm rõ khái niệm vô cùng quan trọng trong 5 nguyên tắc thiết kế hướng đối tượng, đó là Dependency Inversion.
-
Dependency Inversion phát biểu như sau :
- Các module, class cấp cao (high-level) không nên phụ thuộc vào module, class cấp thấp hơn (low-level) mà nên phụ thuộc (giao tiếp) với nhau thông qua một abstraction (Interface).
- Abtraction không nên phụ thuộc vào chi tiết. Chi tiết nên phụ thuộc vào abtraction.
Khó hiểu quá. Chắc hẳn đọc khái niệm bao giờ cũng vậy, trừu tượng và khó hiểu, đơn giản vì nó chỉ là một nguyên lý, một phát biểu ở mức khái niệm, muốn hiểu rõ hơn tôi sẽ dẫn bạn đến một implementation.
Giả sử tôi có :
- Một class Circle (đây là một class low-level) có public cho tôi 2 method : getPerimeter() và getArea() để tính chu vi và diện tích của hình tròn.
- Một class ShapeManager (đây là một class high-level) sẽ public 2 method : calculatePerimeter() và calculateArea();
Tôi muốn in ra màn hình diện tích của hình tròn có bán kính cố định nào đó, tôi sẽ implement như sau.
public class ShapeManager {
private Circle circle;
public ShapeManager() {
}
public void setCircle(Circle circle){
this.circle = circle;
}
public float calculatePerimeter() {
return circle.getPerimeter();
}
public float calculateArea() {
return circle.getArea();
}
}
Và ở hàm main, tôi sẽ chỉ việc gọi như sau:
public static void main(String[] args) {
ShapeManager manager = new ShapeManager();
manager.setCircle(new Circle(5));
System.out.println("Circle with perimeter and area: "
+ manager.calculatePerimeter() + ":" + manager.calculateArea());
}
Code thoạt nhìn không có vấn đề gì cả và vẫn cho ra kết quả đúng. Thế nhưng, nếu tôi muốn mở rộng class ShapeManager có thể tính được nhiều hình khác nhau : hình vuông, hình chữ nhật, hình thoi ... thì tôi phải tạo thêm nhiều đối tượng trong class ShapeManager. Càng lúc, class này sẽ phình to ra tới mức tôi không thể hình dung ra được. Rõ ràng, chúng ta đang thấy class high-level ở đây là ShapeManager đang phụ thuộc vào class low-level. Dependency Inversion muốn các module này không nên phụ thuộc vào nhau và giao tiếp với nhau thông qua một abstraction (Interface). Bởi lẽ đó, Inversion of Control ra đời để làm nhiệm vụ đó.
Inversion of Control (IoC)
-
IoC là một design pattern để hiện thực hóa hay implement nguyên lý thiết kế Dependency Inversion nêu trên. Dĩ nhiên, nó sẽ tuân thủ đầy đủ những nguyên tắc là Dependency Inversion phát biểu. Ngay cả cái tên của nó cũng gợi cho ta đôi chút về ý nghĩa của pattern này - sự đảo ngược điều khiển mà với Dependency Injection, bạn sẽ hiểu rõ hơn về khái niệm này.
-
IoC sẽ không quan tâm đến việc Service được khởi tạo như thế nào mà chỉ quan tâm đến những gì mà nó cung cấp thông qua một abstraction. Điều này tuân thủ chặt chẽ nguyên lý của Dependency Inversion nêu trên, tức là các module high-level chỉ phụ thuộc hay giao tiếp với các module low-level thông qua một abstraction, và chính module high-level sẽ không cần biết module low-level sẽ được khởi tạo như thế nào mà chỉ cần biết những gì nó cung cấp. Phần này sẽ được nói chi tiết hơn ở Dependency Injection ở phần sau.
-
IoC sẽ có 1
Container
để chứa các concretion implementation của các abstraction dùng để kết nối các module với nhau trong mộtobject graph
. Khó hiểu quá nhỉ? Hiểu nôm na nó giống như nơi lưu trữ các implementation của các abtraction mà bạn muốn truyền vào high-level module. Khi nào high-level module cần dùng, nó chỉ việc tìm trong Container với instance tương ứng và inject vào high-level module. Bởi vậy mà high-level module không thể biết Service(low-level module) mình dùng được tạo nên ở đâu là vậy đó. -
Có nhiều cách để implement IoC như :
Service Locator
,Event
hayDependency Injection
... và mỗi loại đều có một ưu nhược điểm riêng mà tùy trường hợp sẽ được sử dụng cho phù hợp.Trở lại với ví dụ ở đầu bài, trong trường hợp muốn mở rộng class ShapeManager, tôi muốn hỗ trợ thêm các hình khác như hình vuông, hình chữ nhật, tôi sẽ tiến hành implement nó theo IoC.
-
Tạo một interface Shape. Interface này sẽ làm nhiệm vụ kết nối ShapeManager (high-level module) với các implementation của Shape (low-level module) hay nói cách khác ShapeManager chỉ phụ thuộc vào interface Shape mà ko cần quan tâm nó được khởi tạo ở đâu và bằng cách nào.
public interface Shape {
float getPerimeter();
float getArea();
}
- Tạo các implementation của Shape. Đây chính là các Service(low-level module) cần dùng trong high-level module.
public class Circle implements Shape {
private static final float PI = 3.1415f;
private int radius;
public Circle(int radius) {
this.radius = radius;
}
@Override
public float getPerimeter() {
return radius * 2 * PI;
}
@Override
public float getArea() {
return (float) (Math.pow(radius, 2) * PI);
}
}
public class Square implements Shape {
private int size;
public Square(int size) {
this.size = size;
}
@Override
public float getPerimeter() {
return size * 4;
}
@Override
public float getArea() {
return (float) Math.pow(size, 2);
}
}
- Modify class ShapeManager.
public class ShapeManager {
private Shape shape;
public void setShape(Shape shape) {
this.shape = shape;
}
public float calculatePerimeter() {
return this.shape.getPerimeter();
}
public float calculateArea() {
return this.shape.getArea();
}
}
- Thử gọi nó nào.
public static void main(String[] args) {
Shape circle = new Circle(5);
Shape square = new Square(5);
Shape rectangle = new Rectangle(4, 6);
ShapeManager manager = new ShapeManager();
manager.setShape(circle);
System.out.println("Circle with perimeter and area: "
+ manager.calculatePerimeter() + ":"
+ manager.calculateArea());
manager.setShape(square);
System.out.println("Square with perimeter and area: "
+ manager.calculatePerimeter() + ":"
+ manager.calculateArea());
manager.setShape(rectangle);
System.out.println("Rectangle with perimeter and area: "
+ manager.calculatePerimeter() + ":"
+ manager.calculateArea());
}
-
Rõ ràng từ ví dụ trên, ta thấy rõ : Nếu trường hợp cần mở rộng thêm các hình khác, ta chỉ cần tạo thêm class implement Shape mà không cần modify ShapeManager().
-
Nếu việc sử dụng các low-level module (trong ví dụ trên là các module Circle, Square ...) diễn ra ở nhiều nơi thì việc phải khởi tạo nó sẽ mất rất nhiều thời gian và khó khăn cho việc maintain sau này, giải pháp được sử dụng ở đây là
IoC Container
.
Dependency Injection (DI)
- Dependency Injection là một trong những pattern để implement Dependency Inversion, nó là một trong những subtype của IoC.
- Nguyên tắc cơ bản của DI là làm cho high-level module phụ thuộc vào low-level module thông qua injector, hay nói cách khác, muốn tạo instance high-level module, ta phải tạo instance của low-level module và inject nó vào high-level module thông qua injector. Injector ở đây có thể là constructor, setter hay interface.
- Nguyên tắc trên có vẻ mâu thuẫn vơi DIP (Dependency Inversion Principle), tuy nhiên nếu xem xét kỹ thì không hòan toàn vậy. Nguyên tắc của DI khác ở chỗ nó sẽ tạo ra sự phụ thuộc của high-level module và low-level module thông qua abstraction chứ ko phải một cách trực tiếp. Như vậy, high-level module sẽ sử dụng Service (low-level module abstraction) thông qua injector mà không quan tâm đến việc khởi tạo của nó. Thật khó hiểu, chính bản thân mình sau rất nhiều lần research vẫn còn chút mơ hồ về vấn đề này. Hãy đến với ví dụ sau.
public class ShapeManager {
private Shape shape;
public ShapeManager(){
this.shape = new Circle();
}
public float calculatePerimeter() {
return this.shape.getPerimeter();
}
public float calculateArea() {
return this.shape.getArea();
}
}
-
Rõ ràng ở ví dụ trên, biến
shape
là bất biến vì được khởi tạo ngay trong constructor. Điều này là không sai nhưng rất khó để mở rông. Module ShapeManager không phụ thuộc vào bất kỳ Service nào và việc tạo instance của nó cũng độc lập với các Service (low-level abstraction) khác, điều đó đi ngược lại với nguyên tắc của DI. Trong trường hợp này, ta sẽ inject Service vào high-level module thông qua injector.- Modify hàm dựng.
public ShapeManager(Shape shape){
this.shape = shape;
}
+ Sử dụng setter
public void setShape(Shape shape){
this.shape = shape;
}
+ Sử dụng interface.
public interface ShapeSetter{
void setShape(Shape shape);
}
public class ShapeManager implement ShapeSetter {
private Shape shape;
@Override
public void setShape(Shape shape){
this.shape = shape;
}
public float calculatePerimeter() {
return this.shape.getPerimeter();
}
public float calculateArea() {
return this.shape.getArea();
}
}
- Để inject Service vào high-level module, ta có thể inject manual hoặc dùng DI Container, các framework hỗ trợ rất tốt việc này.
- DI được sử dụng trong hầu hết các ngôn ngữ hướng đối tượng và có rất nhiều framework hỗ trợ cho việc implement DI. Có thể kể đến
Ninject
trong C#,Spring
trong Java,Dagger
trong Android,Laravel
trong PHP ... - DI cũng như hầu hết các pattern khác đều có ưu nhược điểm riêng biệt, tùy từng trường hợp mà ta sẽ xác định dùng nó một cách phù hợp.
- Ưu điểm : Giảm sự kết dính giữa các module với nhau, code trở nên đẹp và dễ dàng trong bảo trì, dễ dàng viết Unit Test ...
- Nhược điểm: Khó debug, có thể giảm performance vì sẽ tạo các instance ngay ở runtime (DI Container) ...
Kết luận
Dependency Inversion, Inversion of Control và Dependency Injection là các khái niệm mà mọi lập trình viên cần biết và sử dụng thuần thục. Trong khuôn khổ bài viết này, tôi chỉ chia sẻ những kiến thức và kinh nghiệm bản thân trong quá trình làm việc, có thể không tránh được những thiếu sót. Vì thế rất mong nhận được mọi góp ý từ các bạn.
All Rights Reserved