Các design pattern cần biết khi làm việc với NestJS - phần 2

Một số design patterns chính đang được sử dụng bởi NestJS mà bạn cần nắm rõ để làm việc với NestJS hiệu quả hơn.
Các design pattern cần biết khi làm việc với NestJS - phần 2
Các design pattern cần biết khi làm việc với NestJS - phần 2

IoC (Inversion of Controls)

IoC - dịch nôm na là đảo ngược điều khiển. Bạn cần nhớ rõ IoC không phải là một DP - nó là một nguyên lý, một nguyên tắc thiết kế trong công nghệ phần mềm nói chung.

IoC khuyến nghị việc đảo ngược các nguyên tắc controls thông thường trong OOP (lập trình hướng đối tượng), từ đó giúp giảm thiểu độ phụ thuộc lẫn nhau giữa các thành phần của ứng dụng.

Ví dụ: một service CatService có các controls đại diện cho nhiệm vụ của nó - bao gồm nhiệm vụ chính và phụ. 

  • Nhiệm vụ chính: CRUD các đối tượng Cat.
  • Nhiệm vụ phụ:
    • Kiểm soát luồng ứng dụng liên quan (ví dụ như sau khi Cat bị xóa thì sẽ có những ảnh hưởng gì)
    • Khởi tạo các object liên quan để service có thể khởi chạy (ví dụ như kêt nối database,...)
    • ...

Bạn cần nhớ một nguyên tắc khác trong lập trình phần mềm - SRP (Single Responsibility Principle) - hay nguyên tắc đơn nhiệm. Các nhiệm vụ ngoài nhiệm vụ chính sẽ làm chúng ta vi phạm nguyên tắc này. Điều đó sẽ gây ra sự rối rắm trong kiến trúc, cũng như khó khăn trong khâu bảo trì, thêm mới tính năng.

 

Dependency Inversion Principle (DIP)

DIP - nguyên lý đảo ngược thành phần phụ thuộc - là một phần trong IoC - nó tập trung vào việc giảm sự phụ thuộc lẫn nhau giữa các class. 

Modules cấp cao là những modules phụ thuộc vào các modules khác.

  1. Các modules cấp cao không nên phụ thuộc vào các module cấp thấp. Nếu có sự phụ thuộc lẫn nhau, bạn nên tạo ra một Abstraction nằm giữa chúng.
  2. Abstraction không nên phụ thuộc vào các tính năng details. Các tính năng details cần phụ thuộc vào Abstraction.
class RepositoryFactory {
  static getCatRepository(): CatRepository {
    return new CatRepository()
  }
}

class CatRepository {
  public getCatName(id: number | string): string {
    return 'Mina' // we should get it from Database in real world
  }
}

class CatBusinessLogic {
  protected readonly dataAccess: CatRepository
  
  constructor() {
    this.dataAccess = RepositoryFactory.getCatRepository()
  }
  
  getCatName(id: number | string): string {
    return this.dataAccess.getCatName(id)
  }
}

Trong ví dụ trên, chúng ta sử dụng Factory Pattern để triển khai IoC. Tuy nhiên, class CatService lại sử dụng class CatRepository cụ thể. Mặc dù chúng ta đã sử dụng Factory để đẩy việc khởi tạo class sang một nơi khác, class CatServiceCatRepository vẫn kết hợp chặt chẽ với nhau.

CatService phụ thuộc vào CatRepository, do đó CatBusinessLogic là module cấp cao. Cách triển khai ở trên vi phạm quy tắc thứ nhất của DIP. Thay vào đó, chúng ta cần triển khai một Abstraction giữa chúng.

Abstraction (tính trừu tượng) và Encapsulation (tính đóng gói) là một trong những quy tắc quan trọng trong OOP. Nếu bạn nào chưa nắm rõ OOP là gì thì có thể tham khảo trên mạng nhé, mình sẽ không đem nó vào phần này.

Ở đây mình sẽ khai báo một abstract class (bạn cũng có thể sử dụng interface nếu muốn). 

abstract class AbstractCatRepository {
  abstract getCatName(id: number | string): string;
}

Lúc này, CatRepository sẽ extends từ AbstractCatRepository, và RepositoryFactory cũng trả về AbstractCatRepository.

class RepositoryFactory {
  static getCatRepository(): AbstractCatRepository {
    return new CatRepository()
  }
}

class CatRepository extends AbstractCatRepository {
  ...
}

class CatBusinessLogic {
  protected readonly dataAccess: AbstractCatRepository

  ...
}

Lúc này, chúng ta đã loại bỏ sự phụ thuộc lẫn nhau giữa CatRepositoryCatBusinessLogic. Chúng chỉ có một mối quan hệ mờ nhạt với nhau thông qua một lớp abstract

Vì không còn phụ thuộc lẫn nhau nên nó giúp chúng ta dễ dàng thay đổi data source mà không phải sửa code quá nhiều. Hôm nay bạn dùng MySQL, mai bạn có thể chuyển sang MongoDB. Rảnh nữa thì ngày mốt bạn xài Firebase...

Mỗi lần cần thay đổi data source, bạn chỉ cần tạo thêm các class tương ứng, rồi cập nhật lại Factory là xong ^_^.

 

Dependency Injection

DI - (Dependency Injection) là một DP được sử dụng để triển khai IoC. Nó chuyển việc khởi tạo các thành phần liên quan (Service) ra bên ngoài phạm vi đối tượng cần khởi tạo (Client), sau đó cung cấp chúng cho Client.

Mô hình DI phân các đối tượng làm 3 loại như sau:

  1. Client (hay dependent class - lớp phụ thuộc) - là các class phụ thuộc vào Service.
  2. Service (hay dependency) - là các class cung cấp Service cho Client.
  3. Injector - là nơi chịu trách nhiệm xử lý và truyền (inject) các Service vào Client.
dependency-injection.png

Theo như bạn có thể thấy, Injector sẽ tạo ra các instances của Service, sau đó inject các instances này vào Client. Theo đó, nó loại bỏ các controls không cần thiết (ở đây là việc khởi tạo các Service liên quan) ra khỏi Client.

Đây là một thành phần cực kỳ quan trọng trong SOLID principle. DI đảm bảo nguyên tắc đơn nhiệm (SRP) trong lập trình phần mềm.

Có một số cách để Injector inject Service vào cho Client.

  • Constructor injection
  • Property injection
  • Method injection

Mình sẽ làm ví dụ về constructor injection (thông qua constructor của Client) thôi nhé ^_^

Theo ví dụ phía trên, mình sẽ sửa lại như sau:

class CatBusinessLogic {
  constructor(private readonly dataAccess: AbstractCatRepository) {
  }

  getCatName(id: number | string): string {
    return this.dataAccess.getCatName(id)
  }
}

Sau đó thêm vào một Service như sau

class CatService {
  private readonly catBusinessLogic: CatBusinessLogic

  constructor() {
    this.catBusinessLogic = new CatBusinessLogic(
      RepositoryFactory.getCatRepository()
    )
  }

  getCatName(id: number | string): string {
    return this.catBusinessLogic.getCatName(id)
  }
}

Một số điểm đáng chú ý:

  • CatService khởi tạo và inject instance CatRepository vào CatBusinessLogic thông qua Factory.
  • CatBusinessLogic không cần phải khởi tạo instance của CatRepository qua Factory nữa.
  • Dựa theo đó, mối liên kết trực tiếp giữa CatBusinessLogicCatRepository thông qua Factory đã bị loại bỏ hoàn toàn.

Tuy nhiên, việc đẻ thêm một class CatService cũng như khởi tạo instance CatBusinessLogic trông lại khá stupid? CatService lại bị phụ thuộc vào CatBusinessLogic =)))

Đây là lúc bạn cần đến một thứ khác - IoC Container.

 

IoC Container - (DI Container)

IoC Container khởi tạo một instance của Client, đồng thời inject toàn bộ Service vào Client một cách tự động thông qua constructor, property hoặc method trong quá trình khởi chạy.

IoC Container thực hiện DI một cách tự động, nhờ đó chúng ta không cần phải tạo và quản lý các đối tượng một cách thủ công.

Các thành phần cơ bản của IoC Container

  • Register: bạn cần hướng dẫn cho IoC Container cách khởi tạo các Service (dependency) cụ thể. Lúc này, IoC Container sẽ biết cách xử lý khi gặp các Service này thông qua type mapping (hoặc token).
  • Resolve: IoC Container sẽ dựa vào các thông tin mà bạn Register để khởi tạo các instances tương ứng với từng type được chỉ định, sau đó inject chúng vào Client.
  • Disgose: Hầu hết các IoC Container đều có các phương thức riêng để xử lý vòng đời (lifecycle) của Service, và loại bỏ chúng khi cần thiết.

 

Kết

Phần này mình chỉ giới thiệu đơn giản về concept của IoC Container cũng như DI. Phần sau mình sẽ nói chi tiết hơn về cách mà NestJS triển khai DI vào ứng dụng.

Comments

Bài viết nổi bật

Dạo gần đây đi đâu cũng nghe nói về microservices, người người nhà nhà rục rịch chuyển dịch hệ thống sang microservices. Trước khi đưa ra sự so sánh, mình sẽ khái quát một chút về Monolith Application và MicroServices một chút cho các bạn chưa biết nắm rõ hơn nhé.
PHP là ngôn ngữ được sử dụng rộng rãi nhất trên thế giới trong lập trình web. Nó cũng bị ghét nhất. Nhưng tại sao nhiều developer lại ghét nó đến vậy? Hôm nay chúng ta hãy cùng tìm hiểu lý do xem chúng có thuyết phục không nhé ^_^
Lúc trước mình hay sử dụng cách này trên laptop phụ của mình, giờ mua license luôn rồi. Hôm nay mình xin chia sẻ cho bạn nào cần nhé.
JWT Tokens là một cách thức lưu trữ thông tin xác thực hiệu quả, nhưng làm cách nào để chúng ta có thể giúp chúng an toàn hơn? Có 2 cách thường dùng để lưu trữ JWT Tokens là LocalStorage và Cookies. Bây giờ chúng ta sẽ bắt đầu "mổ xẻ" các ưu - nhược điểm của mỗi loại nhé.
Có khá nhiều bạn đã yêu cầu mình một bài viết về Repository Design Pattern. Vậy mục đích của nó là gì? Nó có thực sự cần thiết cho ứng dụng của bạn hay không? Những điểm mạnh, điểm yếu của nó là gì? Chúng ta cùng đi sâu tìm hiểu qua bài viết này nhé.

Mục lục

Related posts

Triển khai Saga Pattern trong microservices với NodeJS và Choreography-Based Saga
Mô hình Saga đưa ra một giải pháp có cấu trúc để giải quyết thách thức này. Nó cung cấp một phương pháp có hệ thống để quản lý transaction qua nhiều microservices. Điều này giải quyết những phức tạp của các transaction phân tán và hoàn toàn tương thích với các nguyên tắc của kiến trúc microservices, được đặc trưng bởi sự kết nối lỏng lẻo và khả năng triển khai độc lập của các service.
Một API cho phép giao tiếp hai chiều giữa các ứng dụng phần mềm thông qua các requests. Một Webhook là một API nhẹ, hỗ trợ chia sẻ dữ liệu một chiều được kích hoạt bởi các events.
Một trong những câu hỏi được đặt thường xuyên nhất về TypeScript là liệu chúng ta nên sử dụng interface hay type. Câu trả lời cho câu hỏi này, giống như nhiều câu hỏi lập trình khác, là nó phụ thuộc vào tình hình cụ thể. Trong một số trường hợp, một cái có lợi thế rõ rệt hơn cái kia, nhưng trong nhiều trường hợp, chúng có thể thay thế cho nhau.
Đây là các types cơ bản nhưng cũng phổ biến nhất trong Typescript. Một số types khác phức tạp hơn cũng được xây dựng dựa trên những types cơ bản này.
Trong thế giới lập trình, trách nhiệm lớn nhất của chúng ta không phải chỉ làm cho code chạy được, mà còn phải đảm bảo rằng các đoạn code mà chúng ta viết có thể dễ dàng kiểm tra và bảo trì trong một khoảng thời gian dài.
Phân trang - một thành phần không thể thiếu trong các ứng dụng có lượng dữ liệu lớn. Tuy nhiên, bạn hiểu được bao nhiêu về nó?
Javascript là một thành phần không thể thiếu đối với frontend developers. Tuy nhiên, ngay từ lúc ra đời, nó đã tồn tại khá nhiều vấn đề cần khắc phục. Đó là lý do tại sao từ 2015 (ES6) tới 2021 (ES12) ra đời nhằm giúp Javascript trở nên tốt hơn.
Dạo này mình làm việc với mấy bạn trên github, thấy hay xài mấy từ viết tắt mà mình không hiểu lắm. Thôi thì tổng hợp lại một list các từ viết tắt hay dùng trong github luôn cho ai cần :D
Dạo gần đây đi đâu cũng nghe nói về microservices, người người nhà nhà rục rịch chuyển dịch hệ thống sang microservices. Trước khi đưa ra sự so sánh, mình sẽ khái quát một chút về Monolith Application và MicroServices một chút cho các bạn chưa biết nắm rõ hơn nhé.
Cách bỏ qua câu lệnh --set-upstream quen thuộc cho các con lười
Mình sẽ giới thiệu 2 cách để xóa một property trong Javascript Object. Một cách sử dụng mutable - toán tử delete, một cách còn lại là immutable - tính năng Object Restructuring.

Tin mới nhất

Triển khai Saga Pattern trong microservices với NodeJS và Choreography-Based Saga
Mô hình Saga đưa ra một giải pháp có cấu trúc để giải quyết thách thức này. Nó cung cấp một phương pháp có hệ thống để quản lý transaction qua nhiều microservices. Điều này giải quyết những phức tạp của các transaction phân tán và hoàn toàn tương thích với các nguyên tắc của kiến trúc microservices, được đặc trưng bởi sự kết nối lỏng lẻo và khả năng triển khai độc lập của các service.
Một API cho phép giao tiếp hai chiều giữa các ứng dụng phần mềm thông qua các requests. Một Webhook là một API nhẹ, hỗ trợ chia sẻ dữ liệu một chiều được kích hoạt bởi các events.
Một trong những câu hỏi được đặt thường xuyên nhất về TypeScript là liệu chúng ta nên sử dụng interface hay type. Câu trả lời cho câu hỏi này, giống như nhiều câu hỏi lập trình khác, là nó phụ thuộc vào tình hình cụ thể. Trong một số trường hợp, một cái có lợi thế rõ rệt hơn cái kia, nhưng trong nhiều trường hợp, chúng có thể thay thế cho nhau.
Trong phần này, chúng ta sẽ tìm hiểu một số khái niệm cơ bản nhất về AWS là gì và một số lợi ích khi sử dụng AWS.
Trở thành một software developer hiệu suất cao không phải là điều dễ dàng. Điều này đòi hỏi bạn phải có kỹ năng và kiến thức về lập trình, cũng như cách tiếp cận và giải quyết các vấn đề phức tạp. Tuy nhiên, nếu bạn có chút kiên nhẫn và sự nỗ lực, bạn hoàn toàn có thể trở thành một developer tài năng và thành công.
Đây là các types cơ bản nhưng cũng phổ biến nhất trong Typescript. Một số types khác phức tạp hơn cũng được xây dựng dựa trên những types cơ bản này.
Trong thế giới lập trình, trách nhiệm lớn nhất của chúng ta không phải chỉ làm cho code chạy được, mà còn phải đảm bảo rằng các đoạn code mà chúng ta viết có thể dễ dàng kiểm tra và bảo trì trong một khoảng thời gian dài.
Thông tin được định nghĩa dưới dạng dữ liệu, kiến thức về thông tin, và trí tuệ về tri thức.
Phân trang - một thành phần không thể thiếu trong các ứng dụng có lượng dữ liệu lớn. Tuy nhiên, bạn hiểu được bao nhiêu về nó?
Javascript là một thành phần không thể thiếu đối với frontend developers. Tuy nhiên, ngay từ lúc ra đời, nó đã tồn tại khá nhiều vấn đề cần khắc phục. Đó là lý do tại sao từ 2015 (ES6) tới 2021 (ES12) ra đời nhằm giúp Javascript trở nên tốt hơn.
Dạo này mình làm việc với mấy bạn trên github, thấy hay xài mấy từ viết tắt mà mình không hiểu lắm. Thôi thì tổng hợp lại một list các từ viết tắt hay dùng trong github luôn cho ai cần :D
Triển khai Saga Pattern trong microservices với NodeJS và Choreography-Based Saga
Một trong những câu hỏi được đặt thường xuyên nhất về TypeScript là liệu chúng ta nên sử dụng interface hay type. Câu trả lời cho câu hỏi này, giống như nhiều câu hỏi lập trình khác, là nó phụ thuộc vào tình hình cụ thể. Trong một số trường hợp, một cái có lợi thế rõ rệt hơn cái kia, nhưng trong nhiều trường hợp, chúng có thể thay thế cho nhau.
Đây là các types cơ bản nhưng cũng phổ biến nhất trong Typescript. Một số types khác phức tạp hơn cũng được xây dựng dựa trên những types cơ bản này.
Trong thế giới lập trình, trách nhiệm lớn nhất của chúng ta không phải chỉ làm cho code chạy được, mà còn phải đảm bảo rằng các đoạn code mà chúng ta viết có thể dễ dàng kiểm tra và bảo trì trong một khoảng thời gian dài.
Javascript là một thành phần không thể thiếu đối với frontend developers. Tuy nhiên, ngay từ lúc ra đời, nó đã tồn tại khá nhiều vấn đề cần khắc phục. Đó là lý do tại sao từ 2015 (ES6) tới 2021 (ES12) ra đời nhằm giúp Javascript trở nên tốt hơn.