Dependency Injection

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:

  1. 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.
  2. 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

 

You May Also Like

About the Author: Nguyen Dinh Thuc

Leave a Reply

Your email address will not be published.