Design Pattern là gì?

Design Pattern, hay còn gọi là mẫu thiết kế, là những giải pháp mang tính tổng quát được sử dụng để giải quyết những vấn đề thường gặp trong quá trình thiết kế và xây dựng phần mềm. Design Pattern không phải là một đoạn code có sẵn để mình copy vào chương trình, cũng không phải là một thư viện hay framework. Nó giống như một cách tư duy và cách tổ chức code mà lập trình viên có thể áp dụng khi gặp một vấn đề phù hợp.

Trong quá trình phát triển phần mềm, có rất nhiều vấn đề xuất hiện lặp đi lặp lại. Ví dụ, chương trình cần đảm bảo chỉ có một đối tượng được tạo ra, cần tạo nhiều loại object khác nhau, hoặc một object thay đổi trạng thái thì nhiều object khác cũng cần được thông báo. Nếu mỗi lần gặp những vấn đề này lập trình viên đều tự nghĩ ra cách giải quyết từ đầu thì sẽ mất nhiều thời gian và có thể tạo ra những thiết kế khó bảo trì.

Design Pattern được đưa ra để giải quyết những vấn đề kiểu như vậy. Có thể hiểu đơn giản: Design Pattern -> Mẫu thiết kế / Công thức giải quyết vấn đề -> Áp dụng khi gặp bài toán tương tự.

Tuy nhiên, Design Pattern không có nghĩa là lúc nào cũng phải sử dụng pattern. Nếu chương trình nhỏ và đơn giản thì việc áp dụng quá nhiều pattern có thể khiến code phức tạp hơn. Vì vậy, quan trọng nhất vẫn là hiểu vấn đề trước rồi mới lựa chọn Design Pattern phù hợp.

Design Pattern hoạt động như thế nào?

Design Pattern hoạt động dựa trên việc xác định một vấn đề thường gặp trong thiết kế phần mềm, sau đó tổ chức các class, object và mối quan hệ giữa chúng theo một cách đã được sử dụng phổ biến để giải quyết vấn đề đó.

Ví dụ, giả sử một hệ thống có các class phụ thuộc trực tiếp vào nhau: Class A -> Class B -> Class C. Trong trường hợp này, Class A phụ thuộc vào Class B và Class B lại phụ thuộc vào Class C. Nếu Class C thay đổi cách hoạt động thì những phần phía trên có thể cũng phải thay đổi theo.

Một cách thiết kế tốt hơn là tạo ra một abstraction ở giữa: Class A -> Interface <- Class B. Lúc này Class A không cần biết chính xác implementation bên dưới là gì mà chỉ cần làm việc thông qua interface.

Design Pattern cũng hoạt động theo tư tưởng tương tự. Pattern giúp lập trình viên tổ chức các thành phần và mối quan hệ giữa chúng để code dễ thay đổi, mở rộng và bảo trì hơn.

Vì vậy, Design Pattern không đơn giản chỉ là một cách viết code. Quan trọng hơn là nó đưa ra cách tổ chức và phân chia trách nhiệm giữa các thành phần trong hệ thống.

Design Pattern ra đời để giải quyết vấn đề gì?

Khi lập trình một chương trình nhỏ, mình có thể viết code trực tiếp và thường không gặp quá nhiều vấn đề. Tuy nhiên, khi dự án phát triển lớn hơn thì số lượng class, object, chức năng và mối quan hệ giữa các thành phần cũng tăng lên.

Nếu không có cách tổ chức hợp lý, code có thể trở nên khó đọc, khó sửa và khó mở rộng. Một thay đổi nhỏ ở một class đôi khi có thể ảnh hưởng đến rất nhiều phần khác của hệ thống.

Design Pattern giúp giải quyết những vấn đề thiết kế thường xuyên xuất hiện trong quá trình phát triển phần mềm.

Ví dụ:

  • Nếu cần đảm bảo một class chỉ có một object duy nhất thì có thể sử dụng Singleton Pattern.

  • Nếu muốn tách việc tạo object ra khỏi phần code sử dụng object thì có thể sử dụng Factory Pattern.

  • Nếu hai thành phần có interface khác nhau nhưng vẫn muốn chúng làm việc được với nhau thì có thể sử dụng Adapter Pattern.

  • Nếu một object thay đổi trạng thái và cần thông báo cho nhiều object khác thì có thể sử dụng Observer Pattern.

  • Nếu một bài toán có nhiều cách xử lý khác nhau và muốn thay đổi cách xử lý linh hoạt thì có thể sử dụng Strategy Pattern.

Như vậy, Design Pattern giúp lập trình viên không phải giải quyết lại những vấn đề thiết kế tương tự từ đầu, đồng thời giúp việc tổ chức code có định hướng rõ ràng hơn.

Các nhóm Design Pattern

Design Pattern có rất nhiều loại khác nhau. Trong cách phân loại phổ biến, Design Pattern thường được chia thành 3 nhóm chính: Creational Pattern, Structural Pattern và Behavioral Pattern.

4.1. Creational Design Pattern

Creational Pattern là nhóm Design Pattern tập trung vào việc tạo object. Mục đích của nhóm này là giúp quá trình tạo object linh hoạt hơn và giảm sự phụ thuộc trực tiếp của code vào class cụ thể.

Một số pattern phổ biến trong nhóm này gồm: Singleton, Factory Method, Abstract Factory, Builder, Prototype.

Ví dụ: Nếu ứng dụng có nhiều loại object khác nhau và việc tạo object phụ thuộc vào một điều kiện nào đó, thay vì để phần code chính tự tạo từng object, mình có thể sử dụng Factory để xử lý việc tạo object.

4.2. Structural Design Pattern

Structural Pattern là nhóm Design Pattern tập trung vào cách tổ chức và kết nối các class hoặc object với nhau. Mục đích là giúp các thành phần có thể kết hợp với nhau một cách linh hoạt mà không tạo ra sự phụ thuộc quá lớn.

Một số pattern phổ biến gồm: Adapter, Decorator, Facade, Proxy, Composite.

Ví dụ: Adapter Pattern có thể được sử dụng khi hai thành phần không tương thích về interface nhưng mình vẫn muốn chúng giao tiếp được với nhau.

4.3. Behavioral Design Pattern

Behavioral Pattern tập trung vào cách các object giao tiếp và phối hợp với nhau. Nhóm này không tập trung chủ yếu vào việc object được tạo như thế nào mà quan tâm đến cách các object thực hiện hành vi và trao đổi thông tin với nhau.

Một số pattern phổ biến gồm: Observer, Strategy, Command, State, Iterator, Template Method.

Nhóm này thường hữu ích trong những hệ thống có nhiều hành vi hoặc có nhiều trạng thái khác nhau.

Singleton Pattern

Singleton Pattern là một Design Pattern thuộc nhóm Creational Pattern. Mục đích của Singleton là đảm bảo rằng một class chỉ có một instance duy nhất trong phạm vi mà pattern đó quản lý, đồng thời cung cấp một cách để các phần khác của chương trình có thể truy cập đến instance đó.

Ví dụ, trong một ứng dụng có một thành phần quản lý cấu hình hoặc một service dùng chung, mình có thể không muốn tạo nhiều object giống nhau.

Cách hoạt động của Singleton là class sẽ kiểm tra xem instance đã tồn tại hay chưa. Nếu chưa tồn tại thì tạo một instance mới. Nếu đã tồn tại thì sử dụng lại instance cũ.

Có thể hình dung:

  • Lần 1: App -> Singleton -> Tạo instance mới

  • Lần 2: App -> Singleton -> Sử dụng instance cũ

  • Lần 3: App -> Singleton -> Sử dụng instance cũ

Như vậy, các lần truy cập sau sẽ sử dụng cùng một instance thay vì tạo object mới.

Singleton có thể hữu ích trong một số trường hợp, tuy nhiên không nên lạm dụng. Nếu sử dụng quá nhiều Singleton, các thành phần trong hệ thống có thể phụ thuộc vào trạng thái dùng chung, từ đó làm việc kiểm thử và bảo trì trở nên khó khăn hơn.

Factory Pattern

Factory Pattern được sử dụng để tạo object mà phần code sử dụng object không cần biết chính xác class cụ thể nào được tạo ra.

Ví dụ, một ứng dụng có nhiều phương thức thanh toán: Payment -> (Momo / Bank / Cash).

Nếu phần code chính tự tạo từng loại object (new MomoPayment(), new BankPayment(), new CashPayment()) thì code chính sẽ phụ thuộc trực tiếp vào từng class cụ thể.

Factory Pattern có thể đưa phần tạo object ra một nơi riêng: Application -> PaymentFactory -> (Momo / Bank / Cash).

Khi cần một loại Payment, Application chỉ cần yêu cầu Factory tạo object phù hợp.

Điểm quan trọng của Factory là tách việc tạo object ra khỏi nơi sử dụng object. Nhờ đó khi có thêm một loại Payment mới, phần xử lý tạo object có thể được thay đổi tập trung hơn thay vì phải sửa rất nhiều nơi trong chương trình.

Structural Design Pattern

Structural Design Pattern tập trung vào cách các class và object được kết nối và tổ chức với nhau. Mục đích của nhóm này là giúp các thành phần trong hệ thống có thể kết hợp với nhau nhưng vẫn giữ được sự độc lập tương đối.

Ví dụ, trong một dự án có một class đang sử dụng một interface nhất định, nhưng thư viện bên ngoài lại cung cấp một interface khác. Hai thành phần này không thể sử dụng trực tiếp với nhau.

Lúc này có thể sử dụng Adapter: Hệ thống -> Adapter -> Thư viện bên ngoài.

Adapter đóng vai trò như một bộ chuyển đổi, giúp hai thành phần có cách giao tiếp khác nhau vẫn có thể làm việc cùng nhau.

Ngoài Adapter, Structural Pattern còn có những pattern khác như Decorator, Facade, Proxy và Composite. Mỗi pattern giải quyết một vấn đề về cấu trúc khác nhau.

Adapter Pattern

Adapter Pattern được sử dụng khi muốn kết nối hai thành phần có interface hoặc cách sử dụng không tương thích với nhau.

Ví dụ, hệ thống hiện tại yêu cầu phương thức send(), nhưng thư viện bên ngoài lại cung cấp sendMessage().

Nếu muốn sử dụng thư viện đó, thay vì sửa toàn bộ code hiện tại hoặc sửa trực tiếp thư viện bên ngoài, mình có thể tạo một Adapter: Application -> Adapter -> sendMessage().

Application chỉ cần gọi send(), sau đó Adapter sẽ chuyển lời gọi này thành sendMessage(). Nhờ vậy, phần code chính không cần biết chi tiết về cách hoạt động của thư viện bên ngoài.

Trong dự án thực tế, Adapter có thể hữu ích khi tích hợp thư viện bên thứ ba, API bên ngoài hoặc khi cần kết nối code cũ với code mới.

Có thể hiểu đơn giản, Adapter giống như một bộ chuyển đổi giúp hai thành phần nói hai “ngôn ngữ” khác nhau có thể giao tiếp với nhau.

Behavioral Design Pattern

Behavioral Design Pattern tập trung vào hành vi và cách giao tiếp giữa các object.

Khi một hệ thống có nhiều object cùng phối hợp để xử lý một chức năng, nếu các object phụ thuộc trực tiếp vào nhau quá nhiều thì code có thể trở nên khó quản lý. Behavioral Pattern đưa ra những cách tổ chức giúp các object giao tiếp với nhau rõ ràng hơn.

Một số pattern phổ biến gồm: Observer, Strategy, Command, State, Iterator, Template Method.

Ví dụ, Observer giúp một object thông báo cho nhiều object khác khi trạng thái thay đổi. Strategy giúp thay đổi thuật toán hoặc cách xử lý mà không cần sửa phần code sử dụng nó.

Observer Pattern

Observer Pattern được sử dụng khi một object thay đổi trạng thái và cần thông báo cho nhiều object khác biết về sự thay đổi đó.

Luồng hoạt động: Subject -> Thông báo -> (Observer A / Observer B / Observer C).

Các Observer nhận được thông báo và thực hiện hành động tương ứng.

Ví dụ, trong một ứng dụng bán hàng, khi số lượng sản phẩm trong giỏ hàng thay đổi thì nhiều thành phần trên giao diện có thể cần cập nhật lại như số lượng sản phẩm, tổng tiền hoặc icon giỏ hàng.

Tư tưởng của Observer cũng xuất hiện trong nhiều cơ chế quản lý state và reactive programming.

Điểm quan trọng của Observer là Subject không cần xử lý trực tiếp từng Observer, mà chỉ cần thông báo rằng trạng thái của nó đã thay đổi.

Strategy Pattern

Strategy Pattern cho phép định nghĩa nhiều cách xử lý khác nhau cho cùng một vấn đề và có thể thay đổi cách xử lý mà không cần thay đổi phần code chính sử dụng nó.

Ví dụ, một ứng dụng có nhiều cách tính phí vận chuyển: Shipping -> (Standard / Express / Same Day).

Nếu viết tất cả trong một class thì có thể xuất hiện rất nhiều điều kiện: if Standard … else if Express … else if Same Day. Khi số lượng cách xử lý tăng lên, code sẽ trở nên khó đọc và khó bảo trì.

Strategy Pattern có thể tách từng cách xử lý thành một Strategy riêng: ShippingStrategy <- (Standard / Express / SameDay).

Khi muốn thay đổi cách tính phí, chương trình chỉ cần thay đổi Strategy đang sử dụng. Pattern này giúp tách thuật toán hoặc cách xử lý ra khỏi phần code sử dụng nó, từ đó việc thêm hoặc thay đổi cách xử lý sẽ dễ dàng hơn.

Design Pattern có liên quan gì đến SOLID?

SOLID và Design Pattern là hai khái niệm khác nhau nhưng có mối quan hệ khá chặt chẽ. SOLID là những nguyên tắc giúp mình suy nghĩ và thiết kế code tốt hơn, còn Design Pattern là những mẫu giải pháp có thể áp dụng để giải quyết các vấn đề thiết kế cụ thể.

Có thể hiểu đơn giản:

  • SOLID -> Nguyên tắc thiết kế -> Giúp biết code nên được tổ chức như thế nào

  • Design Pattern -> Mẫu giải pháp -> Giúp biết tổ chức code theo cách phù hợp nào

Ví dụ, Dependency Inversion Principle trong SOLID khuyến khích các thành phần cấp cao không nên phụ thuộc trực tiếp vào implementation cụ thể mà nên phụ thuộc vào abstraction. Một số cách thiết kế như Factory, Strategy hoặc Dependency Injection có thể hỗ trợ việc giảm sự phụ thuộc trực tiếp giữa các thành phần.

Do đó, học SOLID và Design Pattern cùng nhau sẽ giúp mình hiểu rõ hơn về thiết kế phần mềm. SOLID giúp mình hiểu nguyên tắc, còn Design Pattern giúp mình thấy những cách áp dụng nguyên tắc đó trong các bài toán thực tế.

Design Pattern trong Flutter

Design Pattern cũng được sử dụng khá nhiều trong các dự án Flutter thực tế. Khi ứng dụng còn nhỏ thì mình có thể viết code tương đối đơn giản. Tuy nhiên, khi ứng dụng có nhiều màn hình, API, database, state và business logic thì việc tổ chức code trở nên rất quan trọng.

Một ứng dụng Flutter có thể được tổ chức theo cấu trúc tầng: UI -> State Management -> Service / Use Case -> Repository -> API / Database

Mỗi tầng sẽ có trách nhiệm khác nhau và giao tiếp với nhau thông qua những thành phần phù hợp.

Trong quá trình học Flutter, mình có thể gặp hoặc thấy những ý tưởng liên quan đến các pattern như: Repository Pattern, Factory Pattern, Singleton, Strategy, Observer, Adapter, Dependency Injection, BLoC, MVVM, Clean Architecture.

Ví dụ, Repository có thể đóng vai trò trung gian giữa phần business logic và nguồn dữ liệu. UI không cần trực tiếp xử lý chi tiết việc lấy dữ liệu từ API hoặc database mà có thể gọi thông qua Repository.

Ngoài ra, những thư viện quản lý state như BLoC, Provider hoặc Riverpod cũng sử dụng nhiều ý tưởng về việc tách state, logic và UI, giúp các thành phần trong ứng dụng dễ quản lý hơn.

Tuy nhiên, cần lưu ý rằng Design Pattern không phải cứ sử dụng càng nhiều thì code càng tốt. Việc áp dụng pattern phải dựa trên vấn đề thực tế của dự án. Nếu một chức năng đơn giản nhưng lại áp dụng quá nhiều pattern thì code có thể trở nên phức tạp và khó hiểu hơn.

Đối với mình khi mới bắt đầu làm Flutter, điều quan trọng trước tiên là hiểu pattern giải quyết vấn đề gì, sau đó mới học cách áp dụng nó vào code.

Kết luận

Sau khi tìm hiểu về Design Pattern, mình hiểu rằng Design Pattern là những mẫu thiết kế được sử dụng để giải quyết các vấn đề thường gặp trong quá trình xây dựng phần mềm. Design Pattern không phải là code có sẵn để sao chép mà là cách tư duy và tổ chức các class, object cũng như mối quan hệ giữa chúng.

Design Pattern thường được chia thành ba nhóm chính là Creational, Structural và Behavioral. Creational tập trung vào việc tạo object, Structural tập trung vào cách tổ chức và kết nối các thành phần, còn Behavioral tập trung vào cách các object giao tiếp và xử lý hành vi.

Một số Design Pattern mình đã tìm hiểu gồm Singleton, Factory, Adapter, Observer và Strategy. Mỗi pattern được sử dụng để giải quyết một loại vấn đề khác nhau. Vì vậy, mình không nên áp dụng một pattern một cách máy móc mà cần xem xét vấn đề của chương trình trước.

Qua việc tìm hiểu, mình nhận thấy Design Pattern khá quan trọng khi phát triển những dự án có quy mô lớn, vì nó giúp code được tổ chức rõ ràng hơn, giảm sự phụ thuộc giữa các thành phần và thuận tiện hơn khi mở rộng hoặc bảo trì.

Đặc biệt đối với Flutter, việc hiểu Design Pattern sẽ giúp mình đọc và hiểu cấu trúc code của các dự án thực tế tốt hơn. Khi tham gia vào một dự án của công ty, mình có thể gặp những cách tổ chức code mà trước đây chưa từng sử dụng. Nếu hiểu được tư duy phía sau Design Pattern thì mình sẽ dễ hiểu tại sao dự án lại được chia thành nhiều tầng hoặc nhiều thành phần khác nhau.

Tóm lại, mình hiểu SOLID là những nguyên tắc giúp mình biết “nên thiết kế code như thế nào”, còn Design Pattern cung cấp những “mẫu giải pháp” có thể áp dụng khi gặp những vấn đề thiết kế cụ thể. Đây là hai kiến thức có liên quan và là nền tảng quan trọng để mình hiểu cách xây dựng và tổ chức code trong các dự án phần mềm thực tế.

Phụ lục

Nguồn tham khảo: https://viblo.asia/p/mo-xe-9-design-pattern-kinh-dien-moi-lap-trinh-vien-can-biet-kNLr3DmOVgA

Bài viết khác

REST API

1. REST API là gì? REST API là một cách thiết kế API giúp các ứng dụng giao tiếp và trao đổi dữ liệu với nhau thông qua Internet. REST là viết tắt của Representational State Transfer, còn API là Application Programming Interface. Hiểu đơn giản, REST API đóng vai trò như một cầu nối […]

Tìm hiểu về ngôn ngữ lập trình Dart

  Dart là gì? Dart là một ngôn ngữ lập trình mã nguồn mở được phát triển bởi Google, được sử dụng để xây dựng các ứng dụng có thể chạy trên nhiều nền tảng khác nhau. Hiện nay, Dart được biết đến nhiều nhất vì đây là ngôn ngữ lập trình chính được sử […]

SOLID là gì?

1. Cơ bản về SOLID SOLID là 5 nguyên tắc giúp mình viết code dễ hiểu, dễ sửa, dễ mở rộng và ít gây lỗi khi thay đổi. SOLID thường được sử dụng trong lập trình hướng đối tượng. Có thể hiểu đơn giản là thay vì viết một đoạn code làm quá nhiều việc hoặc […]

Dart Fundamentals

Dart Fundamentals là các kiến thức nền tảng của ngôn ngữ lập trình Dart, bao gồm cú pháp, kiểu dữ liệu, biến, hàm, luồng điều khiển, cách làm việc với collection, null safety và các khái niệm OOP cơ bản. Những cái thường thuộc Dart Fundamentals: Biến và kiểu dữ liệu: cách khai báo biến, […]

Tìm hiểu về SOLID trong lập trình

SOLID là gì? SOLID là tập hợp gồm 5 nguyên tắc trong thiết kế phần mềm hướng đối tượng, được sử dụng để giúp mã nguồn có cấu trúc tốt hơn, dễ đọc, dễ bảo trì, dễ kiểm thử và dễ mở rộng. SOLID không phải là một ngôn ngữ lập trình, framework hay thư […]

Scrum và các khái niệm liên quan

1. Khái niệm về Scrum Scrum là một Agile Framework được sử dụng để quản lý và phát triển các sản phẩm phức tạp, đặc biệt phổ biến trong lĩnh vực Software Development. Scrum giúp đội nhóm chia một dự án lớn thành những phần công việc nhỏ và phát triển sản phẩm thông qua […]

Leave a Reply

Your email address will not be published. Required fields are marked *