-
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ư viện mà là những nguyên tắc giúp lập trình viên đưa ra cách tổ chức code hợp lý trong quá trình xây dựng phần mềm.
SOLID đặc biệt quan trọng khi làm việc với các dự án có quy mô lớn. Khi dự án còn nhỏ, lập trình viên có thể viết code trực tiếp và mọi thứ vẫn hoạt động bình thường. Tuy nhiên, khi số lượng chức năng tăng lên, số lượng class và các thành phần trong chương trình cũng tăng theo. Nếu các thành phần phụ thuộc quá nhiều vào nhau hoặc một class đảm nhận quá nhiều công việc thì việc sửa đổi và mở rộng chương trình sẽ trở nên khó khăn. Một thay đổi nhỏ ở một nơi có thể làm ảnh hưởng đến nhiều phần khác của hệ thống.
SOLID được đưa ra nhằm hạn chế những vấn đề này. Năm nguyên tắc trong SOLID bao gồm Single Responsibility Principle, Open/Closed Principle, Liskov Substitution Principle, Interface Segregation Principle và Dependency Inversion Principle. Mỗi nguyên tắc giải quyết một vấn đề khác nhau trong việc thiết kế và tổ chức code.
Có thể hiểu đơn giản rằng SOLID giúp lập trình viên xây dựng chương trình theo hướng mỗi thành phần có trách nhiệm rõ ràng, các thành phần ít phụ thuộc trực tiếp vào nhau và hệ thống có thể mở rộng mà không cần sửa quá nhiều code đang hoạt động ổn định.
-
Single Responsibility Principle – SRP
Nguyên tắc đầu tiên trong SOLID là Single Responsibility Principle, thường được gọi là nguyên tắc trách nhiệm đơn. Nguyên tắc này nói rằng một class nên chỉ có một trách nhiệm chính và chỉ nên có một lý do chính để thay đổi.
Điều này không có nghĩa là một class chỉ được phép có một phương thức. Một class hoàn toàn có thể có nhiều phương thức nếu những phương thức đó cùng phục vụ cho một trách nhiệm chung. Điều quan trọng là không nên gom quá nhiều chức năng không liên quan vào cùng một class.
Ví dụ, trong một hệ thống bán hàng, nếu có một class UserService vừa có nhiệm vụ tạo tài khoản người dùng, vừa lưu dữ liệu vào database, vừa gửi email, vừa tạo báo cáo và vừa gửi thông báo thì class này đang đảm nhận quá nhiều trách nhiệm. Khi database thay đổi, UserService cũng có thể phải thay đổi. Khi cách gửi email thay đổi, class này cũng phải thay đổi. Điều đó làm cho code trở nên khó bảo trì.
Để áp dụng SRP, các chức năng có thể được tách thành những thành phần riêng biệt. UserService chỉ chịu trách nhiệm xử lý nghiệp vụ liên quan đến người dùng, UserRepository chịu trách nhiệm giao tiếp với database, còn EmailService chịu trách nhiệm gửi email. Khi đó mỗi class có một nhiệm vụ rõ ràng hơn.
Cách hoạt động của SRP là chia một vấn đề lớn thành nhiều trách nhiệm nhỏ hơn. Khi một chức năng cần thay đổi, lập trình viên chỉ cần tìm đến class chịu trách nhiệm cho chức năng đó thay vì phải sửa một class lớn chứa rất nhiều chức năng khác nhau.
Trong dự án Flutter, nguyên tắc này cũng rất hữu ích. Ví dụ, không nên để một Widget vừa xây dựng giao diện, vừa gọi API, vừa xử lý database và vừa xử lý toàn bộ business logic. Có thể tách các phần này thành Widget, Service, Repository và các thành phần quản lý State riêng.
Nhờ SRP, code trở nên dễ hiểu hơn và khi một chức năng thay đổi thì phạm vi ảnh hưởng cũng nhỏ hơn.
-
Open/Closed Principle – OCP
Nguyên tắc thứ hai là Open/Closed Principle, có thể hiểu là nguyên tắc mở rộng và đóng sửa đổi. Nguyên tắc này nói rằng một class hoặc module nên mở để mở rộng nhưng đóng đối với việc sửa đổi.
Ý tưởng của nguyên tắc này là khi cần thêm một chức năng mới, chúng ta nên cố gắng mở rộng hệ thống bằng cách thêm code mới thay vì liên tục sửa code cũ đang hoạt động ổn định.
Ví dụ, giả sử một hệ thống bán hàng có nhiều phương thức thanh toán như tiền mặt, Momo và ngân hàng. Nếu chúng ta viết một class duy nhất và sử dụng rất nhiều câu lệnh if-else để kiểm tra từng phương thức thanh toán thì khi công ty muốn thêm ZaloPay hoặc Visa, chúng ta lại phải mở class đó ra và sửa code bên trong.
Khi số lượng phương thức thanh toán tăng lên, class này sẽ ngày càng lớn và khó bảo trì.
Để áp dụng OCP, chúng ta có thể tạo một abstraction chung cho phương thức thanh toán, sau đó mỗi phương thức thanh toán sẽ triển khai abstraction đó. Khi muốn thêm phương thức thanh toán mới, chúng ta chỉ cần tạo một class mới thay vì sửa các class cũ.
Ví dụ có thể có một abstraction PaymentMethod, sau đó CashPayment, MomoPayment và ZaloPayPayment sẽ triển khai abstraction này. Khi cần thêm VisaPayment, chỉ cần tạo class mới VisaPayment.
Như vậy hệ thống được mở để mở rộng, nhưng những code cũ đã hoạt động ổn định không cần phải sửa lại.
Trong thực tế, nguyên tắc OCP giúp hạn chế việc một class ngày càng phình to khi hệ thống có thêm nhiều chức năng. Nó đặc biệt hữu ích trong những hệ thống thường xuyên phải bổ sung tính năng mới.
Có thể hiểu ngắn gọn nguyên tắc OCP là: khi muốn thêm chức năng, ưu tiên mở rộng hệ thống thay vì sửa trực tiếp code cũ.
-
Liskov Substitution Principle – LSP
Nguyên tắc thứ ba là Liskov Substitution Principle, viết tắt là LSP. Nguyên tắc này nói rằng đối tượng của class con phải có thể thay thế đối tượng của class cha mà không làm thay đổi tính đúng đắn của chương trình.
Đây là nguyên tắc liên quan nhiều đến tính kế thừa trong lập trình hướng đối tượng.
Ví dụ, nếu một class Dog kế thừa từ class Animal thì khi chương trình yêu cầu một đối tượng Animal, chúng ta có thể sử dụng Dog thay thế và chương trình vẫn hoạt động bình thường.
Tuy nhiên, việc một class có quan hệ kế thừa không có nghĩa là class con luôn phù hợp để thay thế class cha. Ví dụ, nếu tạo class Bird có phương thức fly() và sau đó cho Penguin kế thừa từ Bird, thì sẽ xảy ra vấn đề vì chim cánh cụt không thể bay. Nếu chương trình gọi fly() trên đối tượng Penguin, chương trình có thể xảy ra lỗi hoặc phải viết thêm xử lý đặc biệt.
Điều này cho thấy thiết kế ban đầu không phù hợp. Thay vì cho tất cả các loài chim kế thừa một class yêu cầu phải bay, chúng ta có thể tách khả năng bay thành một abstraction riêng. Khi đó đại bàng có thể triển khai khả năng bay, còn chim cánh cụt chỉ cần triển khai những hành vi phù hợp với nó.
LSP giúp đảm bảo rằng các class con thật sự phù hợp với class cha hoặc abstraction mà chúng kế thừa. Khi một class con được sử dụng thay cho class cha, chương trình vẫn phải hoạt động đúng theo những gì hệ thống mong đợi.
Trong dự án thực tế, LSP giúp hạn chế việc sử dụng kế thừa một cách không hợp lý. Thay vì cố gắng cho một class kế thừa từ class khác chỉ vì chúng có một vài đặc điểm giống nhau, lập trình viên cần xem xét liệu class con có thực sự đáp ứng được các hành vi mà class cha yêu cầu hay không.
Có thể hiểu đơn giản LSP là: class con phải thực sự có thể đóng vai trò của class cha mà không làm chương trình hoạt động sai.
-
Interface Segregation Principle – ISP
Nguyên tắc thứ tư là Interface Segregation Principle, viết tắt là ISP. Nguyên tắc này nói rằng một class không nên bị ép phải phụ thuộc vào những phương thức mà nó không sử dụng.
Trong một hệ thống lớn, nếu tạo một interface quá lớn chứa rất nhiều phương thức thì những class sử dụng interface đó có thể phải triển khai những phương thức không cần thiết.
Ví dụ, giả sử chúng ta tạo một interface Employee có các phương thức work(), eat() và sleep(). Một nhân viên bình thường có thể sử dụng cả ba phương thức này. Tuy nhiên, nếu chúng ta có một class Robot cũng triển khai interface Employee, robot có thể cần work() nhưng không cần eat() hoặc sleep().
Nếu bắt Robot phải triển khai cả eat() và sleep() thì thiết kế trở nên không hợp lý. Robot có thể phải viết những phương thức không có ý nghĩa hoặc phải để trống chúng.
Để giải quyết vấn đề này, chúng ta có thể chia interface lớn thành nhiều interface nhỏ hơn, chẳng hạn như Workable, Eatable và Sleepable. Class nào cần chức năng nào thì chỉ triển khai interface tương ứng.
Như vậy Employee có thể triển khai cả ba interface, trong khi Robot chỉ cần triển khai Workable.
Cách hoạt động của ISP là chia các interface lớn thành những interface nhỏ và chuyên biệt hơn, giúp mỗi class chỉ phụ thuộc vào những chức năng thực sự cần thiết.
Nguyên tắc này giúp code trở nên linh hoạt hơn và giảm sự phụ thuộc không cần thiết giữa các thành phần.
Có thể nhớ ISP bằng một câu đơn giản: không ép một class phải sử dụng những chức năng mà nó không cần.
-
Dependency Inversion Principle – DIP
Nguyên tắc cuối cùng là Dependency Inversion Principle, viết tắt là DIP. Đây là một nguyên tắc rất quan trọng khi xây dựng các hệ thống lớn.
DIP nói rằng các module cấp cao không nên phụ thuộc trực tiếp vào các module cấp thấp. Cả hai nên phụ thuộc vào abstraction. Đồng thời abstraction không nên phụ thuộc vào implementation cụ thể; implementation nên phụ thuộc vào abstraction.
Nói đơn giản hơn, thay vì một class phụ thuộc trực tiếp vào một class cụ thể, chúng ta nên tạo một abstraction ở giữa để giảm sự phụ thuộc.
Ví dụ, một UserService cần lưu dữ liệu người dùng vào database. Nếu UserService tạo trực tiếp một đối tượng MySQLDatabase thì UserService đang phụ thuộc cứng vào MySQL. Nếu sau này công ty chuyển từ MySQL sang PostgreSQL thì UserService có thể phải sửa lại.
Để áp dụng DIP, chúng ta tạo một abstraction chẳng hạn như Database. MySQLDatabase và PostgreSQLDatabase đều triển khai abstraction này. UserService chỉ làm việc với Database mà không cần biết database cụ thể là MySQL hay PostgreSQL.
Khi đó cấu trúc sẽ giống như:
UserService
↓
Database
↑ ↑
MySQL PostgreSQL
UserService không còn phụ thuộc trực tiếp vào MySQLDatabase. Nó chỉ phụ thuộc vào abstraction Database.
Điều này giúp hệ thống dễ thay đổi hơn. Nếu muốn chuyển sang một database khác, chúng ta chỉ cần tạo implementation mới phù hợp với abstraction mà không phải thay đổi toàn bộ logic của UserService.
DIP cũng liên quan rất nhiều đến Dependency Injection, một kỹ thuật thường được sử dụng trong các dự án Flutter. Thay vì class tự tạo dependency bên trong, dependency có thể được truyền từ bên ngoài vào. Nhờ đó các thành phần trong chương trình được tách biệt và dễ kiểm thử hơn.
Trong một dự án Flutter thực tế, tư tưởng này có thể được áp dụng khi xây dựng các lớp như Repository, Service hoặc API Client. Ví dụ, một Repository có thể phụ thuộc vào một abstraction của API hoặc Database thay vì phụ thuộc trực tiếp vào một implementation cụ thể.
Có thể hiểu ngắn gọn DIP là: đừng để code cấp cao phụ thuộc cứng vào code cấp thấp; hãy để chúng giao tiếp với nhau thông qua abstraction.
-
Tổng kết về SOLID
Sau khi tìm hiểu năm nguyên tắc SOLID, mình hiểu rằng SOLID không phải là một công nghệ hay framework mà là một tập hợp các nguyên tắc giúp lập trình viên thiết kế và tổ chức code tốt hơn.
Năm nguyên tắc SOLID giải quyết những vấn đề khác nhau. Single Responsibility Principle giúp mỗi class tập trung vào một trách nhiệm chính. Open/Closed Principle giúp hệ thống có thể mở rộng mà hạn chế phải sửa code cũ. Liskov Substitution Principle đảm bảo class con có thể thay thế class cha một cách hợp lý. Interface Segregation Principle giúp class không phải phụ thuộc vào những chức năng mà nó không sử dụng. Cuối cùng, Dependency Inversion Principle giúp giảm sự phụ thuộc trực tiếp giữa các thành phần bằng cách sử dụng abstraction.
Trong các dự án Flutter thực tế, SOLID có thể được kết hợp với các kiến trúc và kỹ thuật khác như Repository Pattern, Service, Dependency Injection, State Management và API Layer. Khi dự án ngày càng lớn, việc áp dụng những nguyên tắc này giúp code có cấu trúc rõ ràng hơn, dễ kiểm thử, dễ bảo trì và dễ mở rộng.
Qua việc tìm hiểu SOLID, mình nhận thấy rằng mục tiêu của SOLID không phải là làm cho code trở nên phức tạp hơn mà ngược lại, giúp phân chia trách nhiệm và sự phụ thuộc một cách hợp lý để khi dự án phát triển, việc thay đổi một chức năng không gây ảnh hưởng không cần thiết đến những phần còn lại của hệ thống.
PHỤ LỤC:
Nguồn tham khảo: https://www.youtube.com/watch?v=_dTJeiticT8
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ử […]
Tìm hiểu về Design Pattern
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 […]
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, […]
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 […]