Google Guice

Trong Java, khi nhắc đến IoC (Inversion of Control) và DI (Dependency Injection) hầu hết mọi người sẽ nghĩ ngay đến Spring Framework, một framework khá nổi tiếng và đang rất được yêu thích trong cộng đồng Java. Ngoài Spring, có 2 framework cũng được mọi người hay nhắc đến là PicoContainer và Google Guice. Hiện tại mình đang làm việc trên Magnolia CMS, một CMS đang phát triển và sử dụng Google Guice trong kiến trúc của nó. Magnolia sử dụng Guice cùng với một số các mẫu thiết kế hướng đối tượng (Design Patterns), tạo nên một kiến trúc module hóa rất hay. Khi nào có thời gian rảnh và lối hành văn tốt hơn ? mình sẽ nói về tầng bên dưới của Magnolia CMS.

Hiện tại Guice đang ở phiên bản 3.0, nhưng mình dịch bài user’s guide của nó là 1.0 vì bài viết khá đơn giản và dễ hiểu giúp các bạn mới tiếp xúc với DI framework nắm bắt dễ dàng hơn. Nếu các bạn muốn tìm hiểu về phiên bản hiện tại của Guice thì theo đường dẫn https://github.com/google/guice/wiki/GettingStarted, Guice đã đưa cả source và user’s guide từ Google Code sang Github.

Một số từ khóa nên biết trước: 

IoC (Inversion of Control)

DI (Dependency Injection)

Singleton Pattern

Factory Pattern

Mock – http://www.mockobjects.com/

Trong bài có khác nhiều từ mình không dịch, các bạn tham khảo giúp mình tại đây

http://toando.coffee/terminology/

Bài gốc: http://google.github.io/guice/user-docs/Guice-1.0-Users-Guide.pdf

Giới thiệu

Cộng đồng Java phải tốn nhiều công sức để kết nối (wiring) các đối tượng lại với nhau. Làm thế nào ứng dụng web của bạn truy cập vào tầng dịch vụ giữa(middle tier service), hay dịch vụ(service) của bạn để login người dùng hay quản lý giao dịch (transaction)? Bạn sẽ tìm thấy rất nhiều cách tổng quát và cụ thể để giải quyết vấn đề này. Một số dựa vào mẫu (patterns), còn lại sử dụng framework. Tất cả kết quả ở nhiều mức độ khác nhau đều phục vụ khả năng kiểm thử và một số cho boilerplate code. Bạn sẽ sớm thấy Guice có khả năng làm điều đó tốt nhất thế giới (nổ): dễ dàng cho unit test, tính mềm dẻo và khả năng dễ bảo trì cao nhất và trùng lặp mã lệnh thấp nhất.

Chúng tôi sẽ sử dụng một ví dụ đơn giản, minh họa lợi ích của Guice hơn so với một số cách giải quyết cổ điển mà bạn đã khá quen thuộc. Thực thế, các đoạn mã ví dụ thật sự đơn giản, mặc dù nó biểu thị được lợi ích ngay lập tức, chúng tôi sẽ không sử dụng Guice ngay. Chúng tôi mong muốn bạn sẽ thấy được ứng dụng của bạn sẽ phát triển thế nào, và lợi ích của Guice.

Trong ví dụ này, client phụ thuộc vào một interface Service. Nó có tên tùy ý. Chúng tôi đơn giản gọi là Service.

Chúng ta có một lớp cài đặt mặc định cho interface trên, lớp mà client không nên bị phụ phục vào. Nếu chúng ta quyết định sử dụng một cài đặt khác trong tương lai, chúng ta không muốn phải đi lại từ đầu và thay đổi mọi thứ ở client.

Chúng ta còn có một service mock để sử dụng cho unit test

Plain Old Factories

Trước khi biết đến dependency injection, phần lớn chúng ta đã sử dụng factory pattern. Ngoài interface Service, bạn có thêm một lớp Factory Service để cung cấp service cho client cũng như là cách để vượt qua kiểm thử trong Mock Service. Chúng ta sẽ tạo một singleton service vì vậy chúng ta có thể giữ cho ví dụ này đơn giản hết mức có thể.

Client sẽ đến trực tiếp Factory bất cứ lúc nào khi cần Service.

Client đơn giản vậy là đủ, nhưng việc unit test để vượt qua kiểm thử (pass) trong Mock Service và sau đó xóa tất cả. Đây không phải là một vấn đề lớn trong ví dụ đơn giản của chúng ta, nhưng nếu bạn thêm vào nhiều Client và Service, tất cả những mocking và  cleaning tạo nên sự va chạm cho việc viết unit test. Ngoài ra, nếu bạn quên xóa (clean up) tài nguyên sau mỗi test, những test khác có thể sẽ thành công hay thất bại. Ngay cả trường hợp xấu nhất, những test có thể sẽ thất bại phụ thuộc vào việc bạn sắp xếp việc thực thi các test.

Cuối cùng, ghi chú là Factory Service hiện tại đang là Singoton. Nếu thay đổi cài đặt cho Factory là non-singleton sẽ làm cho vấn đề trở nên rất phức tạp.

Tự dependency injection

Mục đích của DI pattern là để tạo unit test dễ dàng hơn. Chúng ta không cần thiết sử dụng một framework đặc biệt nào đó để thực hành DI. Bạn có thể viết nó bằng tay (tự viết).

Trong khi Client yêu cầu Factory cung cấp một Service trong ví dụ trước, thì với DI, Client mong muốn có một Service được truyền vào (Client sẽ không cần gọi Factory Service). Đừng gọi tôi, tôi sẽ gọi bạn. (Don’t call me, I’ll call you)

Điều này giúp cho unit test đơn giản hơn nhiều. Chúng ta có thể truyền Mock Service và ném ra mọi thứ chúng ta muốn khi chúng ta xong việc.

Bây giờ, làm thể nào chúng ta liên kết Client với Service? Khi tự cài đặt DI, chúng ta có thể di chuyển tất cả các logic phụ thuộc vào trong lớp Factory Client. Điều đó có nghĩa chúng ta phải tạo thêm một lớp FactoryClient

Dependency Injection với Guice

Tạo ra những lớp factories và tự tạo logic Dependency Injection cho tất cả các Service và Client trở nên rất nhàm chán. Một số DI Framework yêu cầu bạn tường minh việc ánh xạ (map) các Service tại một nơi mà bạn muốn chúng được inject (tiêm).

Guice loại bỏ bất cả các mã lệnh dư thừa (boilerplate) này mà không ảnh hưởng đến khả năng bảo trì của chương trình.

Với Guice, bạn cài đặt Module. Guice truyền mối liên kết (binder) vào trong module của bạn, và module đó sử dụng mối liên kết này để tạo ánh xạ giữa interface và cài đặt cho interface đó. Theo đó, Module sẽ báo với Guice ánh xạ Service đến ServiceImpl với tầm vực Singleton:

Module trên báo với Guice những gì bạn muốn inject(tiêm). Bây giờ, làm thế nào chúng ta báo với Guice nơi chúng ta muốn inject Service vào? Với Guice, bạn chú thích tại constructor(hàm khởi tạo), hàm (method) và thuộc tính (field) với chú thích (annotation) @Inject

Chú thích (annotation) @Inject làm vấn đề rõ ràng hơn cho các lập trình viên khi chính sửa lớp của bạn.

Để Guice inject Client, chúng ta phải vừa trực tiếp hỏi Guice để tạo đối tượng Client cho chúng ta, hay một số lớp khác mà phải có đối tượng Client được inject vào đó.

Chú thích: Chỗ này tác giả nói hơi khó hiểu, ý tác giả là nếu bạn muốn tạo đối tượng Client bạn phải nhờ Guice làm dùm, bạn không được tự tạo ( dùng new). Khi bạn tạo những lớp khác mà có inject Client, thì Guice sẽ tự tạo Client và inject vào lớp đó.

Khác nhau giữa Guice và tự viết Dependency Injection

Như bạn có thể thấy, Guice tiết kiệm cho bạn từ việc phải viết lớp Factory Client. Bạn không phải viết mã lệnh tường minh để kết nối (wiring) giữa các Client và những lớp phụ thuộc của chúng. Nếu bạn quên cung cấp sự phụ thuộc, Guice sẽ thất bại (fail) khi chạy. Guice xử lý xoay vòng những phụ thuộc một cách tự động.

Guice cho phép bạn khai báo tầm vực . Ví dụ, bạn không phải viết nhiều đoạn mã như nhau để lưu trữ một đối tượng vào trong biến HttpSession lần này qua lần khác.

Trong thế giới thực, bạn thường không biết một lớp cài đặt cho interface đến khi chương trình thực thi. Bạn cần có các định nghĩa factories hay Service Locator cho các factories của bạn. Guice sẽ giải quyết các vấn đề cho với công sức thấp nhất.

Khi bạn tự viết DI, bạn có thể dễ dàng quay trở lại những thói quen cũ và thực hiện các phụ thuộc trực tiếp, đặc biệt khi bạn là một người mới tiếp xúc với khái niệm Dependency Injection. Sử dụng Guice sẽ đem lại nhiều lợi thế cho bạn (turn the table, idiom) và làm cho mọi việc đơn giản hơn.

Các annotation khác

Khi có thể, Guice cho phép bạn sử dụng annotation thay thế cho việc binding và loại bỏ được cả những mã lệnh boilerplace. Trở lại với ví dụ của chúng ta, nếu bạn cần một interface làm đơn giản hóa unit test nhưng bạn không muốn quan tâm đến những phụ thuộc trong quá trình biên dịch, bạn có thể chỉ ra lớp cài đặt trực tiếp cho infterface đó bằng cách:

Nếu Client muốn một Service và Guice không thể tìm thấy được binding tường minh (trong lớp Module), thì Guice sẽ inject đối tượng ServiceImpl.

Mặc định, Guice inject một thực thể mới bất kỳ lúc nào khi yêu cầu. Nếu bạn muốn khai báo tầm vực khác, bạn có thể chú thích trên lớp cài đặt của interface:

Chú thích: Khi bạn cần inject interface Service vào bất cứ lớp nào, thì Guice sẽ tạo mới một đối tượng ServiceImpl, nhưng khi bạn để annotation @Singleton trên lớp ServiceImpl, thì Guice chỉ tạo lớp đó một lần, và những lúc bạn cần inject Service, nó sẽ inject đối tượng duy nhất trên.

Khái quát kiến trúc của Guice

Chúng ta có thể mường tượng kiến trúc của Guice gồm 2 giai đoạn riêng biệt: startup (khởi động) và runtime (thực thi). Bạn xây dựng một Injector trong xuốt quá trình khởi động (startup) và sử dụng nó để inject các đối tượng trong lúc runtime (thực thi).

Startup

Bạn cấu hình cho Guice bằng cách cài đặt interface Module. Guice truyền vào Module của bạn một đối tượng Binder, bạn sử dụng Binder này để cấu hình (configure) quá trình binding (ràng buộc). Một binding cơ bản bao gồm sự ánh xạ (map) giữa một interface và lớp cài đặt cụ thể cho interface đó. Ví dụ:

Việc tạo một Injector đòi hỏi phải theo các bước:

guice-1

  1. Đầu tiên, tạo ra một đối tượng Module (ở trên là MyModule) và truyền nó vào hàm tạo Injector Guice.createInjector().
  2. Guice tạo một đối tượng Binder và truyền nó vào MyModule.
  3. MyModule sử dụng đối tượng binder để định nghĩa các binding (mối liên kết giữa interface và cài đặt của interface đó).
  4. Dựa vào các bindings bạn đã chỉ định, Guice tạo một đối tượng Injector và trả về hàm gọi khởi tạo nó.
  5. Bạn sử dụng injector trên để inject đối tượng.

Runtime

Bây giờ chúng ta có thể sử dụng injector mà chúng ta đã tạo trong suốt giai đoạn đầu để inject những đối tuợng và sử dụng bên trong nội tại trong các lớp binding. Mô hình thực thi của Guice bao gồm một injector chứa một số luợng các binding.

guice-2

Một key định danh duy nhất mỗi binding. Khoá sẽ chứa kiểu dữ liệu mà Client phụ thuộc vào và một annotation tuỳ chọn. Bạn có thể sử dụng một annotation để phân biệt nhiều binding đến kiểu dữ liệu như nhau. Kiểu dữ liệu của key và annotation tuơng ứng với kiểu dữ liệu và annotation tại điểm đuợc inject.

Mỗi binding có một provider (lớp cung cấp) sẽ cung cấp những thực thể của kiểu dữ liệu cần thiết. Bạn có thể cung cấp một lớp, và Guice sẽ tạo thực thể của nó cho bạn. Bạn có thể đưa cho Guice một thực thể của kiểu dữ liệu bạn đang kết nối (bind) tới.Bạn có tự thể cài đặt (implement) một lớp Provider, và Guice có thể inject các phụ thuộc vào trong nó.

Mỗi binding còn có một sự tuỳ chọn đó là Scope (phạm vi). Mặc định các binding không có Scope, và Guice tạo một thực thể mới cho mỗi lần inject. Một scope tuỳ chỉnh giúp bạn điều khiển Guice có phải tạo ra một đối tuợng mới hay không. Ví dụ, bạn có thể tạo một thực thể trên HttpSession.

Quá trình nạp chương trình (Bootstrapping)

Ý tưởng của nạp chương trình (bootstrapping) là nguyên tắc cơ bản của dependency injection. Việc luôn luôn tường minh hỏi Injector về những phụ thuộc mà sử dụng Guice như là một service locator, đó không phải là dependency injection framework.

Mã lệnh của bạn nên tương tác trực tiếp với Injector càng ít càng tốt. Thay vào đó, bạn muốn nạp ứng dụng của bạn bằng cách inject một đối tượng root. Thùng chứa (container) có thể inject các phụ thuộc và các phụ thuộc của đối tượng root này, và tiếp tục đệ quy. Cuối cùng, lý tưởng nhất là ứng dụng của bạn nên có một lớp biết về Injector và mọi lớp khác nên mong muốn có những đối tượng đã được inject.

Ví dụ, một ứng dụng web framework như Struts 2 nạp ứng dụng bằng cách inject tất cả các actions. Bạn có thể nạp một web service framework bằng cách inject những lớp cài đặt cho service của bạn.

DI là virus. Nếu bạn đang refactor mã lệnh có trước với hàng loạt các hàm static, bạn có thể sẽ cảm thấy giống như đang kéo một sợi dây không giờ kết thúc. Đó là một điều tốt. Nghĩa là DI đang giúp tạo ra cho bạn những đoạn mã linh hoạt hơn và dễ kiểm thử hơn.

 

(cont)

Như các bạn thấy mình dịch khá tệ, dịch từng từ nên khó truyền đạt hết hàm ý của các giả. Mình chỉ cố gắng cung cấp nhiều nhất từ khóa có thể để mọi người tìm kiếm. Mình khuyên các bạn nên đọc bài gốc tiếng Anh.

5 Comments on "Google Guice"

  1. Khoa Phung says:

    Bài này quá bổ ích, cám ơn bạn nhiều nhé

  2. Khoa Kieu says:

    2 thằng về làm task nhanh. Ở đây chém gió ah.

Got something to say? Go for it!