Lập trình hướng đối tượng nâng cao DI bằng "Minimal Cake Pattern"

1035
13-03-2018
Lập trình hướng đối tượng nâng cao DI bằng "Minimal Cake Pattern"

Như ở bài trước đã trình bày, Dependency Injection (từ đây sẽ gọi là DI) có thể thực hiện bằng 3 cách. Cùng Bizfly Cloud tìm hiểu 3 cách này ngay tại bài viết này 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

Lần này mình sẽ trình bày lại một cách làm gọn gàng và nhẹ nhàng hơn của cách 2 tên đơn giản là Minimal Cake Pattern.

Hiện tại chúng ta có UserService phụ thuộc vào UserRepository, ApplicationLive phụ thuộc vào UserService. UserRepository là một class nguyên thủy rất đơn giản.

class User(val name: String)

class UserRepository {

def save(user: User) = {

print("Saved " user.name)

}

}

Ảnh 2.

Nhiệm vụ của chúng ta là tạo mối quan hệ có thể DI được cho 3 class nói trên.

Cake Pattern hoàn chỉnh của Jonas Boner gói userRepository vào trong một trait mới, và gói tiếp userService vào một trait khác, sử dụng kiểu tự nhận thức và cuối cùng Extends tất cả ở ApplicationLive. Logic của phương pháp này khá phức tạp và vẫn là rào cản lớn đối với những bạn chưa quen lập trình hướng đối tượng nâng cao. Trong thực tế, áp dụng Cake Pattern ở những dự án lớn cũng rất tốn cost vận hành và phát triển. Khi đó một anh engineer người Nhật đã "phát minh" ra một phương pháp giản lược hơn, khi tách riêng ý nghĩa về interface và implement đối với từng class.

Interface vs implement

Mỗi class sẽ luôn được định nghĩa đi kèm với 2 trait, 1 trait dùng để làm interface và một trait dùng để implement. Giả sử class tên là XXX, ta sẽ định nghĩa UsesXXX làm interface và MixInXXX làm implement

abtract class XXX {}

// Interface

trait UsesXXX {

val x: XXX

}

// Implement

trait MixInXXX {

val x = new XXX

}

Interface dùng cho định nghĩa và kế thừa, còn Implement dùng để dùng nghiệp vụ trực tiếp.

Khi này, khi nào xuất hiện class YYY mới phụ thuộc vào XXX, chúng ta sẽ định nghĩa bằng cách extend interface trait UsesXXX, và viết implement bằng cách sử dụng implement trait của MixInXXX

abstract class YYY extends UsesXXX {}

// Interface

trait UsesYYY {

val x: YYY

}

// Implement

trait MixInYYY {

val x = new YYY with MixInXXX

}

Bạn có thể thấy YYY cũng có tiếp 2 trait chuẩn bị sẵn, đến khi xuất hiện class nào mới muốn dùng thì lại lấy 2 trait dùng tương tự. Cứ thế class có thể kế thừa đến khi nào tùy thích, và luôn đảm bảo có 2 trait đi kèm. 2 trait này sẽ giúp cho DI dễ dàng hơn mà sẽ được trình bày ở phần sau.

Ảnh 5.

Mình sẽ viết lại userRepository và userService theo cách này. Đầu tiên là userRepository

trait UserRepository {

def save(user: User): Unit

}

object UserRepository extends UserRepository {

def save(user: User) = {

print("Saved " user.name)

}

}

// Interface

trait UsesUserRepository {

val userRepository: UserRepository

}

// Implement

trait MixInUserRepository {

val userRepository = UserRepository

}

// userService

abstract class UserService extends UsesUserRepository {

def register(name: String) = {

val someone = new User(name)

userRepository.save(someone)

}

}

// Interface

trait UsesUserService {

val userService: UserService

}

// Implementv

trait MixInUserService {

val userService = new UserService with MixInUserRepository

}

cuối cùng là ApplicationLive

object ApplicationLive extends MixInUserService

ApplicationLive.userService.register("dtvd")

DI for testing

Thực tế và hay gặp nhất là yêu cầu DI trong UnitTest. Chúng ta muốn test hàm Register của UserService mà không muốn tạo ra một UserRepository thực tế, hay nói cách khác, chúng ta chỉ muốn test logic mà không muốn phát sinh kết nối đến Database, để test chạy nhanh và không bị phụ thuộc tài nguyên Server.

Việc cần làm rất đơn giản: sử dụng nhánh interface (UsesXXX) của các class bên trên. Mình sẽ tạo mock cho UserRepository và nhúng vào một class kế thừa UserService, do đặc tính UserService chỉ kế thừa UsesUserRepository nên nhúng là một chuyện dễ dàng.

DI bằng cách nhúng Mock Object vào Interface:

class MockUserRepository extends UserRepository {

override def save(user: User) = print("Do nothing !!!")

}

class UserServiceForTest extends UserService {

val userRepository = new MockUserRepository

}

val test = new UserServiceForTest

test.register("hehe") // Do nothing !!!

Ảnh 14.

Khi hệ thống phình to, logic trở nên phức tạp và tính kế thừa bắt đầu chồng chéo thì một kiến trúc kế thừa tốt luôn đem lại độ linh hoạt cao và khả năng bóc tách để viết unit test dễ dàng.

Tại sao Minimal Cake Pattern lại là một phát kiến lớn

Khi đối mặt với một hệ thống khổng lồ, khoảng 400 module, trước hết không có DI thì dev sẽ dẹo, và DI bằng Cake Pattern thì dev cũng dẹo luôn. Lúc này bắt buộc phải có một phương pháp đơn giản hết mức, loại bỏ các tầng trừu tượng và lồng chéo không cần thiết.

Nếu so sánh với các thư viện DI Container thì bản thân Trait trong Scala là một phát minh của ngôn ngữ này, đại diện luôn cho khả năng "Inject" của các class kế thừa. Martin Flower từng nói, nếu có phép thay thế bằng những đối tượng đơn giản hơn thì không có lý gì lại không sử dụng. Khi ngôn ngữ đã tiến hóa lên thì phần xây dựng sẵn từ xưa của thư viện hay design pattern trở thành thừa và không cần thiết cũng là điều dễ hiểu.

Khi một hệ thống không chỉ lớn và còn khổng lồ về số lượng dòng code, khả năng check lỗi bởi theo compile time chứ không phải theo run time là điều tối cần thiết, và check lỗi bằng compiler của Scala chắc chắn sẽ dễ hiểu hơn check lỗi của các thư viện DI Container.

Lời kết

Qua 2 bài mình đã điểm qua 3 cách DI phổ biến trong Scala

• Áp dụng Cake Pattern bằng pure code

• Dùng kỹ thuật Reader Monad của lập trình hàm

• Sử dụng Minimal Cake Pattern bằng pure code

Trong thực tế thì Reader Monad và Minimal Cake Pattern là hữu dụng hơn cả, tuy nhiên Reader Monad đòi hỏi phải có tư duy về lập trình hàm, và nếu đã quyết định dùng thì phải chấp chận dùng lập trình hàm ở mọi nơi. Trong khi đó Minimal Cake Pattern là sáng sủa và gọn gàng, có thể áp dụng trong cả những hệ thống lớn.

Hi vọng các bạn đang làm Scala sẽ chọn được phương pháp phù hợp cho mình và tốt cho cả tương lai hậu bối của các bạn 

Nguồn: Kipalog

>> Tìm hiểu thêm: Những nguyên tắc, định luật cần biết trong lập trình

SHARE