Càng đọc càng mung lung

Cái khái niệm đảo ngược điều kiển (Inversion of control - IoC) này thực sự mung lung, cho đến ngày mình đi vào phát triển ezyfox-server, với kiến trúc như thế này:

Câu hỏi đặt ra là làm thế nào để người dùng có thể tự tạo ra được các zone, các app, các plugin và làm thế nào để khởi động được tất cả các zone, các app và plugin này lên? Trong khi đó thì cả chương trình sẽ chỉ có 1 hàm main mà thôi, chúng ta không thể nào khởi động được hơn 1 hàm main trong một chương trình.

Định nghĩa

IoC là một nguyên tắc lập trình mà ở đó việc điều khiển và kiểm soát chương trình thuộc về các framework (như spring boot hay ezyfox-server) chứ không còn phụ thuộc vào chương trình do chúng ta viết nữa (hay nói cách khác là hàm main có thể sẽ nằm ở phía thư viện). Đã bao giờ bạn thắc mắc là khi viết các web service với tomcat, hay viết các ứng dụng android hay ios ở phía client chúng ta lại chẳng bao giờ có hàm main chưa? Đó chính là một trong những ứng dụng phổ biến nhất của IoC. Thông qua các design pattern như template method, observer hay event driven, kết hợp với Dependency Injection đã đưa IoC đến với chúng ta hết sức tự nhiên mà chính chúng ta cũng không nhận thấy.

Ứng dụng truyền thống

Trong một ứng dụng truyền thống thì một chương trình sẽ phải bắt đầu từ hàm main, tất cả mọi thứ sẽ do người viết ứng dụng kiểm soát, từ việc khởi tạo các đối tượng, xử lý nghiệp vụ, xử lý exception cho đến các phương án mở rộng về sau này. Chúng ta cùng quay trở lại bài toán khởi tạo server nhé, giả sử bạn đầu server phiên bản 1.0 chỉ có một ứng dụng tên là freechat, với cách viết truyền thống chúng ta sẽ làm kiểu này:

public class ServerStartup {
    public static void main(String[] args) {
        FreeChatApp freeChatApp = new FreeChatApp();
        freeChatApp.start();
    }
}

class FreeChatApp {
    public void start() {}
}

Tiếp theo ở phiên bản 2.0 chúng ta cần phải thêm 1 game vòng quay may mắn, chúng ta sẽ cần làm thế này:

public class ServerStartup {
    public static void main(String[] args) {
        FreeChatApp freeChatApp = new FreeChatApp();
        freeChatApp.start();
        LuckyWheelGame luckyWheelGame = new LuckyWheelGame();
        luckyWheelGame.start();
    }
}

class LuckyWheelGame {
    public void start() {}
}

Vậy ở phiên bản 3.0 chúng ta sẽ cần thêm 10 app, 10 game nữa, hoặc bỏ game, bỏ app thì sao? rõ ràng là chúng ta sẽ tác động rất lớn đến hàm main, và nếu làm vậy chúng ta sẽ vi phạm nguyên tắc Open/Close (đóng với thay đổi, mở với mở rộng), nghĩa là chúng ta đang đặt một chương trình đang hoạt động ổn định vào vòng nguy hiểm. Thêm vào nữa, nếu chúng ta muốn đóng gói mọi thứ để trở thành thư viện thì sao? Rõ ràng là không thể được.

Mục tiêu ra đời

Để giải quyết được những vấn đề trên thì IoC đã ra đời với các mục tiêu:

  1. Loại bỏ sự phụ thuộc vào các lớp cài đặt cụ thể: ví dụ ServerStartup startup sẽ khởi động mà không cần phụ thuộc vào FreeChatApp hay LuckyWheelGame, nó sẽ chỉ cần quan tâm rằng nó chỉ cần khởi động lên 1 danh sách các ứng dụng mà thôi
  2. Cho phép mỗi module thực hiện độc lập các công việc của mình mà không làm ảnh hưởng đến các module khác: ví dụ việc khởi động FreeChatApp có thành công hay không cũng sẽ không làm ảnh hưởng gì đến LuckyWheelGame cả
  3. Có thể dễ dàng mở rộng (thêm mới hoặc bỏ đi) các module mà không làm ảnh hướng đến source code đang chạy: ví dụ chúng ta có thể thêm mới các app, các game, hoặc bỏ đi FreeChatApp mà không cần phải thêm hoặc xoá bất cứ thứ gì ở hàm main
  4. Giúp cho lập trình viên chỉ cần tập trung vào xử lý nghiệp vụ, các công việc khác sẽ được tự động tương thích: ví dụ chúng ta có thể tạo ra bao nhiêu app tuỳ thích, việc khởi tạo, huỷ app hãy để IoC lo

Giải quyết bài toán

Để giải quyết bài toán server, chúng ta sẽ sử dụng thư viện ezyfox-bean nhé, chỉ với một vài dòng code, chúng ta sẽ làm cho mọi thứ trở nên mềm dẻo và kì diệu hơn rất nhiều:

public class ServerStartup {
    public static void main(String[] args) throws Exception {
        EzyBeanContext beanContext = EzyBeanContext.builder()
            .scan("com.tvd12.ezyfox.example.ioc")
            .build();
        List beans = beanContext.getSingletons(EzySingleton.class);
        for(Object bean : beans) {
            if(bean instanceof EzyStartable) {
                ((EzyStartable) bean).start();
            }
        }
    }
}

@EzySingleton
public class LuckyWheelGame implements EzyStartable {
    public void start() {
        System.out.println("start lucky wheel game");
    }
}

@EzySingleton
public class FreeChatApp implements EzyStartable  {
    public void start() {
        System.out.println("start freechat app");
    }
}

Khi chạy chương trình, chúng ta sẽ nhận được kết quả:

start freechat app
start lucky wheel game

Từ giờ trở đi, cho dù bạn có thêm bao nhiêu app, bao nhiêu game, hay bỏ đi bao nhiêu app, bao nhiêu game thì hàm main cũng sẽ không thay đổi, chúng ta sẽ chỉ cần thực hiện việc duy nhất là code nghiệp vụ mà thôi. Đối với các framework có tính đóng gói cao như tomcat hay ezyfox-server, hay các framework ở phía client đều đã đóng gói hàm main ở bên trong, và chỉ để hở ra các lớp entry point như HttpServlet, EzyAppEntryLoader hay Activity cho chúng ta cài đặt, chính vì vậy mà chúng ta sẽ không thấy hàm main ở đâu.

Tổng kết

IoC có thể hiểu là sự kết hợp nhuần nhuyễn giữa các design pattern và Dependency Injection. Nó tạo ra khả năng mở rộng bất tận cho hệ thống của chúng ta. Nếu không có IoC có lẽ chúng ta đã không có các framework server hay các framework ở phía client như bây giờ. Hãy nhớ rằng đừng nhồi nhét quá nhiều thứ vào hàm main, hãy để những công việc xử lý phức tạp cho các framework, còn chúng ta, chỉ cần quan tâm đến code nghiệp vụ mà thôi.