Về cơ bản Dependency Injection (DI) cung cấp các object mà một object cần (các phụ thuộc – dependency) thay vì nó tự tạo khởi tạo constructor. Nó là một kỹ thuật vô cùng hữu ích cho testing vì nó cho phép các dependency được mock hoặc stub. Nói cách khác, DI là một Design Pattern với nguyên tắc chính là tách rời behavior khỏi các dependency.
Có một bài viết của Martin Fowler giúp cung cấp thêm thông tin chi tiết về DI
Những cần thiết cơ bản của DI
Unit testing
Mock Data Access Layer
Mock File System
Mock Email
Thiết kế module
Implement nhiều thành phần khác nhau
Infrastructure có thể nâng cấp
Tách các phần liên quan
Trách nhiệm duy nhất
Tăng khả năng sử dụng lại code
Mô hình sạch hơn
Phát triển đồng thời hoặc độc lập
Tóm tắt lại như sau, DI là một Design Pattern trong đó bất kỳ object phụ thuộc nào cũng phải được cung cấp denpendency thay vì tạo nó bên trong object. Scala là một ngôn ngữ rất sâu sắc và phong phú cung cấp cho bạn một vài cách để dùng DI chỉ dựa trên constructor của ngôn ngữ, nhưng cũng không có rào cản nào xảy ra nếu bạn muốn sử dụng DI của Java nếu nó thực sự cần thiết.
Khi nói về DI trong Scala, có rất nhiều các lựa chọn thay thế khác nhau, sau đây là một trong những cái phổ biến:
1.Constructor injection
2.Cake pattern
3.Google guice
Constructor injection
Tên cũng đã thể hiện ý nghĩa của nó! Khai báo tất cả các dependency cho một object như các tham số constructor. Nhưng nó hoạt động như thế nào? Chúng ta hãy cùng xem một ví dụ nhỏ sau đây
UserService class dùng để lấy thông tin người dùng từ cơ sở dữ liệu và thực hiện một vài xử lý
Thế nên trước tiên, chúng ta cần lấy thông tin từ cơ sở dữ liệu, định nghĩa một class UserDAL mà có các phương thức create, update, delete với các bản ghi trong DB
class UserDAL { /* dummy data access layer */ def getUser(id: String): User = { val user = User("12334", "testUser", "test@knoldus.com") println("UserDAL: Getting user " + user) user } def create(user: User) = { println("UserDAL: creating user: " + user) } def delete(user: User) = { println("UserDAL: deleting user: " + user) } }
Định nghĩa một User
// User class chứa thông tin về User case class User(id: String, name: String, email: String)
Tiếp theo chúng ta định nghĩa một UserService class, UserService sẽ thực hiện một vài thao tác với dữ liệu được cung cấp bởi UserDAL class. UserService có dependency trên UserDAL, nên UserService khai báo UserDAL class như một tham số constructor
class UserService(userDAL: UserDAL) { def getUserInfo(id: String): User = { val user = userDAL.getUser(id) println("UserService: Getting user " + user) user } def createUser(user: User) = { userDAL.create(user) println("UserService: creating user: " + user) } def deleteUser(user: User) = { userDAL.delete(user) println("UserService: deleting user: " + user) } }
Bạn có thấy cái gì đó không hợp lý trong thiết kế này không? Nào hãy cùng xem
class UserService(userDAL: UserDAL)
Nó đã vi phạm nguyên tắc Dependency Inversion Principle
Nguyên tắc như sau:
- Module của level cao hơn không nên phụ thuộc vào module của level thấp hơn. Cả hai chỉ nên dựa trên abstraction.
- Abstraction không nên phụ thuộc vào chi tiết. Chi tiết không nên dựa vào abstraction.
Nên chúng ta cần định nghĩa một trait.
trait UserDALComponent { def getUser(id: String): User def create(user: User) def delete(user: User) } class UserDAL extends UserDALComponent { // a dummy data access layer that is not persisting anything def getUser(id: String): User = { val user = User("12334", "testUser", "test@knoldus.com") println("UserDAL: Getting user " + user) user } def create(user: User) = { println("UserDAL: creating user: " + user) } def delete(user: User) = { println("UserDAL: deleting user: " + user) } } class UserService(userDAL: UserDALComponent) { def getUserInfo(id: String): User = { val user = userDAL.getUser(id) println("UserService: Getting user " + user) user } def createUser(user: User) = { userDAL.create(user) println("UserService: creating user: " + user) } def deleteUser(user: User) = { userDAL.delete(user) println("UserService: deleting user: " + user) } }
UserService dựa trên UserDALComponent (Abstraction) không implement.
Cake Pattern
Cake Pattern lần đầu tiên được giải thích bởi tài liệu của Martin Oderskys Scalable Component Abstractions như là một cách để ông và team của mình cấu trúc trình biên dịch Scala. Nào hãy cùng xem đoạn code sau
//Data access layer trait UserDALComponent { val userDAL: UserDAL class UserDAL { // a dummy data access layer def getUser(id: String): User = { val user = User("12334", "testUser", "test@knoldus.com") println("UserDAL: Getting user " + user) user } def create(user: User) = { println("UserDAL: creating user: " + user) } def delete(user: User) = { println("UserDAL: deleting user: " + user) } } } // User service which have Data Access Layer dependency trait UserServiceComponent { this: UserDALComponent => val userService: UserService class UserService { def getUserInfo(id: String): User = { val user = userDAL.getUser(id) println("UserService: Getting user " + user) user } def createUser(user: User) = { userDAL.create(user) println("UserService: creating user: " + user) } def deleteUser(user: User) = { userDAL.delete(user) println("UserService: deleting user: " + user) } } }
Ở đây class UserDAL được định nghĩa bên trong UserDALComponent trait với abstract tham chiếu đến biến của UserDAL. Câu hỏi ở đây là tại sao tham chiếu đến biến là abstract. Bởi vì nếu bạn viết Unit Test của UserService thì bạn có thể mock UserDAL dễ dàng (dùng mock framework) và lý do thứ 2 là vi phạm Dependency Inversion Principle
// Tạo object extends UserService và UserDAL trait: object User extends UserServiceComponent with UserDALComponent { val userService = new UserService val userDAL = new UserDAL // mock UserDAL object cho unit testing }
Google Guice
Google Guice là framework mã nguồn mở cho nền tảng Java được phát hành bởi Google dưới Apache License. Nó cung cấp hỗ trợ cho DI sử dụng diễn giải cho cấu hình các object Java.
Về bản chất Google Guice framework được tạo ra cho Java nhưng chúng ta có thể sử dụng tốt nó trong Scala
Chúng ta có thể cấu hình SBT để sử dụng Google Guice
libraryDependencies +="com.google.inject" % "guice" % "3.0"
Nào hãy cùng xem ví dụ sau:
// Định nghĩa một trait cho database access layer trait UserDALComponent { def getUser(id: String): User def create(user: User) def delete(user: User) } class UserDAL extends UserDALComponent { // a dummy data access layer def getUser(id: String): User = { val user = User("12334", "testUser", "test@knoldus.com") println("UserDAL: Getting user " + user) user } def create(user: User) = { println("UserDAL: creating user: " + user) } def delete(user: User) = { println("UserDAL: deleting user: " + user) } } // User service which have Data Access Layer dependency class UserService @Inject()(userDAL: UserDALComponent) { def getUserInfo(id: String): User = { val user = userDAL.getUser(id) println("UserService: Getting user " + user) user } def createUser(user: User) = { userDAL.create(user) println("UserService: creating user: " + user) } def deleteUser(user: User) = { userDAL.delete(user) println("UserService: deleting user: " + user) } }
Google Guice yêu cầu thêm một class cấu hình để chúng ta trói buộc trait với implement class của nó
import com.google.inject.{ Inject, Module, Binder, Guice } import com.knol.di.UserDALComponent import com.knol.di.UserDAL import com.google.inject.name.Names class DependencyModule extends Module { def configure(binder: Binder) = { binder.bind(classOf[UserDALComponent]).to(classOf[UserDAL]) } }
Google Guice sẽ lấy toàn bộ Dependency sử dụng và tạo object
val injector = Guice.createInjector(new DependencyModule) val component = injector.getInstance(classOf[UserService]) //get UserService object