Trở lại với cơ bản: OOP, Dependency Injection và Cake Pattern

Ví dụ trong bài viết bằng Scala nhưng đã tối giản hết mức, vì vậy nếu bạn độc giả không quen với Scala thì cũng có thể đọc code như mã giả và hình dung theo tư tưởng nhé :smile:

Dependency Injection

Version 1: Code kết dính

Trước hết chúng ta hãy bắt đầu với một class đơn giản. Giả sử hệ thống có một class User, định nghĩa đối tượng người dùng. Đây là một đối tượng quá quen thuộc phải không nào :)

class User(val name: String)

Khi lưu thông tin người dùng vào database, mình giả sử lại có một DAO (Data Access Object) class tên là UserRepository và một Service class UserService sử dụng DAO.

class UserRepository {
  // Maybe DB initialization here
  def save(user: User) = {
    // Can use real DB access here
    print("Saved " + user.name)
  }
}

class UserService {
  val userRepository = new UserRepository
  def register(name: String) = {
    val someone = new User(name)
    userRepository.save(someone)
  }
}

Cuối cùng là đối tượng ApplicationLive là app chính của chúng ta.

object ApplicationLive extends UserService
ApplicationLive.register("dtvd") // Saved dtvd

Trong thực tế UserRepository có thể tạo ra kết nối đến database và thực sự gọi câu query để insert/update, phát sinh rất nhiều overhead. Class UserRepository trên đây chỉ in ra màn hình mang tính chất minh họa.

Code kết dính không tốt như thế nào

Đoạn code trên có gì không ổn ?

  val userRepository = new UserRepository

Class UserService mang một biến có kiểu UserRepository, nói cách khác là UserService bị phụ thuộc vào UserRepository.

Phụ thuộc thì có làm sao không ?

Phụ thuộc thì ... rất làm sao, rất vấn đề. new UserRepository là một implement của class UserRepository. Khi chúng ta muốn thay đổi implement của UserRepository thì bắt buộc phải động đến UserService, hay tệ hơn nữa là phải duplicate sang một implement mới.

Khi nào mà lại cần phải thay đổi implement của UserRepository thế ?

Câu hỏi này rất hay. Hãy tưởng tượng, bây giờ mình không muốn dùng kết nối đến MySQL để lấy data nữa, mà muốn lấy từ ...MongoDB, hay là từ Redis, mình sẽ có một vài MongoUserRepository hay RedisUserRepository nữa, nhưng không thể dùng trực tiếp với UserService bên trên.

Một ví dụ dễ thấy hơn là khi muốn test UserService, chúng ta không thể dùng UserRepository tạo ra kết nối thật đến DB, mà phải mock thành object ảo, và lại không biết làm thế nào để dùng với UserService.

Dependency Injection

Để giải quyết vấn đề phụ thuộc, chúng ta sẽ ứng dụng Dependency Injection (DI). Theo Wikipedia thì

  • Dependency là đối tượng được sử dụng trong một service. dependency ở đây là userRepository, service ở đây là UserService
  • Injection có nghĩa là khả năng thay thế dependency nói trên bằng một đối tượng tùy ý khác.

Đến năm 2016 mà nhắc đến Dependency Injection hay Inversion Of Control thì cũng không có gì là mới mẻ nữa. Trong thế giới của Java thì khái niệm đã tồn tại hơn 10 năm tuổi. Martin Fowler đã viết về tư tưởng này từ năm 2004. Hay thậm chí, Inversion Of Control còn được đưa ra từ những năm 1980. Tuy vậy đến gần đây những ngôn ngữ như PHP hay Javascript mới xuất hiện những framework sử dụng DI, và việc nắm rõ khái niệm cùng với áp dụng trong code base vẫn là một kỹ năng quan trọng và không thể thiếu với các senior dev ngay tại thời điểm hiện tại.

Cake Pattern

Để thực hiện DI trong Scala có 3 phương pháp chính

  1. Dùng DI Container trong các framework hỗ trợ DI như Spring DI Container hay Google Guice
  2. Áp dụng Cake Pattern bằng pure code
  3. Dùng kỹ thuật Reader Monad của lập trình hàm

Phương pháp 3 mình sẽ trình bày ở cuối cùng. Phương pháp 2: Cake pattern là đối lập với phương pháp 1 khi sử dụng Spring DI Container hay Google Guice.

Spring DI Container nhận rất nhiều chỉ trích rằng đã áp dụng DI quá phức tạp. Để sử dụng DI Container bạn phải tạo ra Enterprise JavaBean (EJB). Từ EJB mới xuất hiện một khái niệm hoàn toàn đối lập là Plain Old Java Object (POJO), đề cao tính dễ hiểu và nguyên bản của ngôn ngữ. Đối lập lại, Google Guice đã có những bước tiến vượt bậc và gần đây đã được tích hợp vào Play framework phiên bản 2.4. Tuy vậy đây vẫn là một ngạc nhiên lớn vì từ trước đến nay trong Scala, "dân tình" vẫn dùng Cake Pattern để implement DI và nghiễm nhiên coi là phương pháp phổ biến.

DI Container của Google Guice có khả năng làm biến mất hoàn toàn trong code tham chiếu giữa client và service (bên sử dụng và bên được sử dụng). Mỗi bên có thể compile riêng rẽ, client code rất sạch và đơn giản. Tuy vậy điều này cũng dẫn đến chuyện bug sẽ chỉ xuất hiện tại thời điểm runtime chứ không phải tại thời điểm compile, cùng với việc mở ra khả năng dễ dàng tạo ra "ma trận quan hệ" phức tạp ngoài ý thức của người lập trình. DI Container sử dụng Inversion Of Control(IOC), là khái niệm mà Martin Fowler cũng chùn tay khi sử dụng

Inversion of control is a common feature of frameworks, but it's something that comes at a price. It tends to be hard to understand and leads to problems when you are trying to debug. So on the whole I prefer to avoid it unless I need it. This isn't to say it's a bad thing, just that I think it needs to justify itself over the more straightforward alternative.

Tạm để Google Guice lại một bên, lần này chúng ta sẽ tìm hiểu thực tiễn về Cake Pattern và ứng dụng vào ví dụ đầu bài. Tư tưởng về Cake Pattern được giới thiệu lần đầu bởi Jonas Boner, CTO của TypeSafe và tác giả của bộ Akka.

Cake pattern

Version 2: Gói lại trong Component

Trước hết mỗi class UserRepositoryUserService sẽ được gói lại trong một trait mới vói tên là ...Component. Trong mỗi trait này tồn tại một biến là kiểu UserRepository hoặc UserService cũ.

trait UserRepositoryComponent {
  val userRepository = new UserRepository // <-- Here
  class UserRepository {
    def save(user: User) = {
      print("Saved " + user.name)
    }
  }
}

trait UserServiceComponent extends UserRepositoryComponent {
  val userService = new UserService // <-- Here
  class UserService {
    def register(name: String) = {
      val someone = new User(name)
      userRepository.save(someone)
    }
  }
}

Sau đó là "nỗi tất cả context" lại vào nhau

object ApplicationLive 
extends UserServiceComponent 

ApplicationLive.userService.register("dtvd")

Bạn có thể thấy trait của Service đã extend trait của Repository.

Trait là cái gì thế ?

Trait là abstract class nhưng cho phép một class mới có thể extend nhiều Trait, thay vì chỉ có thể extend 1 superclass - tính chất multiple inheritance (đa kế thừa) của Scala. Bạn có thể dễ dàng bắt gặp trong Scala những đoạn code kiểu như sau

class Child extends Parent with Trait1 with Trait2 with Trait3 with Trait4 with Trait5

Viết thế này đã tốt hơn ở điểm nào?

Bây giờ UserServiceComponent chỉ cần extends UserRepositoryComponent là đã có thể dùng được biến userRepository như bình thường. Và tất nhiên ApplicationLive thì đã sở hữu cả 2 biến userRepository, userService và dùng được luôn.

Kế thừa hay là tự nhận thức ?

Trong bài viết về Cake Pattern của Jonas Boner, anh đã dùng Self Type Annotion (kiểu tự nhận thức) mà trên đây mình mới chỉ viết bằng Inheritance (kế thừa).

// trait UserServiceComponent extends UserRepositoryComponent // Inheritance
trait UserServiceComponent { this: UserRepositoryComponent => } // Self Type Annotion

Ở đây dùng tự nhận thức hay kế thừa cũng sẽ tạo được hiệu quả như nhau. Tuy vậy sẽ là thiếu sót nếu không đề cập đến lý do tại sao tự nhận thức lại tốt hơn kế thừa.

// Self Type Annotion
trait A
trait B { this: A => }
// Inheritance
trait A
trait B extends A

Trong trường hợp này không có gì khác nhau cả, mình có thể nói mình viết kiểu tự nhận thức vì ... mình thích thế. Tuy vậy, khi xuất hiện một trait con nữa thì vấn đề sẽ khác.

// Self Type Annotion
trait A { def foo = "foo" }
trait B { this: A => def foobar = foo + "bar" }
trait C { this: B => def fooNope = foo + "Nope" } //not found: value foo
// Inheritance
trait A { def foo = "foo" }
trait B extends A { def foobar = foo + "bar" }
trait C extends B { def fooNope = foo + "Nope" } // fooNope

Như vậy tự nhận thức không cho phép C biết được A mang method gì, nói cách khác, khi C một khi đã tự nhận thức mình là B thì sẽ chỉ biết B mà thôi ! Điều này dùng để phòng chống việc để lọt những tính năng không cần thiết từ các tầng cao xuống tầng thấp. Mỗi tầng chỉ cần biết giao tiếp với tầng kế trên là đủ.

Favor 'object composition' over 'class inheritance' - Design Pattern

Ở bài toán cụ thể của chúng ta, sau khi dùng tự nhận thức thì ApplicationLive bắt buộc phải extends cả 2 trait

object ApplicationLive 
extends UserServiceComponent 
with UserRepositoryComponent

Version 3: Cake Pattern hoàn chỉnh

Nếu bạn để ý thì trong nội dung các trait vừa mới tạo đã khởi tạo biến userRepositoryuserService tại chỗ bằng từ khoá val. Điều này dẫn đến chuyện khi khởi tạo ApplicationLive thì 2 biến trên là có sẵn và không động vào được nữa. Muốn gọi 2 biến trên ra cần phải

ApplicationLive.userService
ApplicationLive.userRepository

Các câu khởi tạo trải đều trong code base, khởi tạo bị dính chặt với định nghĩa. Để mang tất cả khởi tạo về một chỗ, mình sẽ thay thế bằng định nghĩa kiểu duy nhất

trait UserRepositoryComponent {
  val userRepository: UserRepository
  //...
}

trait UserServiceComponent { this: UserRepositoryComponent =>
  val userService: UserService
  //...
}

Và chuyển phần khởi tạo về ApplicationLive

object ApplicationLive extends UserServiceComponent with UserRepositoryComponent {
  val userRepository = new UserRepository
  val userService = new UserService
}
ApplicationLive.userService.register("dtvd")

Vậy là xong. Đến bây giờ thì inject đã quá dễ dàng. Mình sẽ inject userRepository thành một mock object, không làm xử lý gì trong xử lý mà chỉ in ra "Do nothing!!!"

object Test extends UserServiceComponent with UserRepositoryComponent {

  class MockUserRepository extends UserRepository{
    override def save(user: User) = {
      print("Do nothing!!!")
    }
  }
  val userRepository = new MockUserRepository
  val userService = new UserService
}
Test.userService.register("dtvd") // Do nothing!!!

DI bằng Reader Monad

Reader Monad

Phần này dành riêng cho các bạn hứng thú với functional programming (lập trình hàm). Scala còn có thể thực hiện DI bằng Reader Monad. Reader hay được dùng với Reader class của Scalaz, nhưng mình sẽ demo bằng Function1 của Scala.

Hàm trong Scala có thể dùng làm kiểu trả về của một hàm khác. Sau đây là một ví dụ đơn giản:

val square = (i: Int) => i * i
square(3) // 9

square mang kiểu hàm (function type), nhận vào giá trị Int và cho ra bình phương của giá trị đó. Kiểu hàm nhận 1 tham số duy nhất gọi là kiểu unary function (hàm đơn phân), mà thực chất là đối tượng của kiểu Function1 trong Scala. Kiểu hàm có thể "nối" bằng andThen

val z = square andThen ( i => i + 3 )
z(3) //12

Reader Monad là monad định nghĩa cho hàm đơn phân, dùng andThen và map. Reader Monad thực tế chỉ là một Function1. Mình sẽ viết lại ví dụ bên trên như sau

class UserRepository {
  def save(user: User) = {
    print("Saved " + user.name)
  }
}

class UserService {
  def register(name: String) = (r: UserRepository) => r.save(new User(name))
}

Điều gì khác biệt ở đây ? Hàm register trong UserService không còn trả về giá trị thực hiện của UserRepository.save nữa, mà giờ trả về giá trị là một hàm đơn phân, mà hàm này nhận tham số là kiểu UserRepository và đến lượt mình mới trả về giá trị của UserRepository.save :smile:

Và sử dụng UserService bên trên bằng cách gọi curry function

object ApplicationLive extends UserService
ApplicationLive.register("dtvd")(new UserRepository)

Nhìn có vẻ hay đấy, nhưng DI thế nào với quả này

class MockUserRepository extends UserRepository{
  override def save(user: User) = {
    print("Do nothing!!!")
  }
}

ApplicationLive.register("dtvd")(new MockUserRepository)

Kết luận

Bài viết đã trình bày cách sử dụng Cake Pattern với pure code để thực hiện DI thông qua một ví dụ đơn giản và gần với thực tế. Khi thực hiện Cake Pattern, bạn nên sử dụng kiểu tự nhận thức thay vì kế thừa trong code của service. Hi vọng sau khi đọc bài này các bạn viết code linh hoạt và dễ thay đổi, dễ test hơn.

Bài viết có sử dụng nguồn từ những tài liệu sau

Phần tiếp: DI bằng "Minimal Cake Pattern".

Bình luận


White
{{ comment.user.name }}
Bỏ hay Hay
{{comment.like_count}}
Male avatar
{{ comment_error }}
Hủy
   

Hiển thị thử

Chỉnh sửa

White

Vu Nhat Minh

54 bài viết.
674 người follow
Kipalog
{{userFollowed ? 'Following' : 'Follow'}}
Cùng một tác giả
White
113 27
Nếu bạn thường vào trang mua sắm của amazon, chắc sẽ chẳng lạ gì với menu Shop by Department. Tốc độ hiển thị nội dung của menu là tức thì so với d...
Vu Nhat Minh viết 2 năm trước
113 27
White
86 4
Lời người dịch Người dịch là một developer , sau khi tìm đọc được bài viết này bằng bản gốc tiếng Anh đã cảm thấy như được "khai sáng" về khả năng...
Vu Nhat Minh viết hơn 2 năm trước
86 4
White
55 5
Đây là phần cuối của một series chuyên về thiết kế UI. Bạn nên đọc (Link) trước khi bắt đầu đọc phần này. Luật số 7: "Ăn trộm" như là một nghệ sỹ...
Vu Nhat Minh viết hơn 2 năm trước
55 5
Bài viết liên quan
White
14 2
Dependency Injection và Inversion of Control – Phần 1: Định nghĩa Series bài viết Dependency Injection và Inversion of Control gồm 3 phần: 1. Địn...
Huy Hoàng Phạm viết gần 2 năm trước
14 2
White
26 6
Cuối tuần mình có thư giãn bằng cách đọc hiểu và ứng dụng một chút về dependency injection. Cảm thấy khá thấm nên muốn chia sẻ cho các bạn về những...
Kiên Trung Đặng viết gần 2 năm trước
26 6
{{like_count}}

kipalog

{{ comment_count }}

bình luận

{{liked ? "Đã kipalog" : "Kipalog"}}


White
{{userFollowed ? 'Following' : 'Follow'}}
54 bài viết.
674 người follow

 Đầu mục bài viết

Vẫn còn nữa! x

Kipalog vẫn còn rất nhiều bài viết hay và chủ đề thú vị chờ bạn khám phá!