SOLID
Trang này chứa các mẹo và tài nguyên để chuẩn bị cho các cuộc phỏng vấn SOLID.
Last updated
Trang này chứa các mẹo và tài nguyên để chuẩn bị cho các cuộc phỏng vấn SOLID.
Last updated
Giải thích:
Phần mềm được xem là tốt khi khi nó có kiến trúc tốt. Kiến trúc phần mềm tương tự như móng nhà, móng yếu nhà sẽ không vững. Để viết được phần mềm tốt bạn phải học rất nhiều, điều đầu tiên bạn cần biết là SOLID.
SOLID là viết tắt của 5 chữ cái đầu trong 5 nguyên tắc thiết kế hướng đối tượng. Giúp cho lập trình viên viết ra những đoạn code dễ đọc, dễ hiểu, dễ maintain. Nó được đưa ra bởi và Michael Feathers. 5 nguyên tắc đó bao gồm:
Single responsibility priciple (SRP)
Open/Closed principle (OCP)
Liskov substitution principe (LSP)
Interface segregation principle (ISP)
Dependency inversion principle (DIP)
Trong OOP bao gồm:
Tính trừu tượng (abstraction): Tạo ra các lớp trừu tượng mô hình hoá các đối tượng trong thế giới thực.
Tính đóng gói (Encapsulation): Các thực thể của lớp trừu tượng có các giá trị thuộc tính riêng biệt.
Tính kế thừa (Inheritance): Các đối tượng có thể dễ dàng kế thừa và mở rộng lẫn nhau.
Tính đa hình (Polymorphism): Có thể thực hiện một hành động đơn theo nhiều cách thức khác nhau tuỳ theo loại đối tượng cụ thể đang được gọi.
Vậy tóm lại SOLID liên quan gì đến OOP ?
-> Hầu hết lập trình viên đều đã biết các tính chất này của OOP, nhưng cách thức để phối hợp các tính chất này với nhau để tăng hiệu quả của ứng dụng thì không phải ai cũng nắm được. Một trong những chỉ dẫn để giúp chúng ta sử dụng được OOP hiệu quả hơn đó là nguyên tắc SOLID.
Mỗi lớp chỉ nên chịu trách nhiệm về một nhiệm vụ cụ thể.
Có ý nghĩa là một class chỉ nên giữ một trách nhiệm duy nhất. Một class có quá nhiều chức năng sẽ trở nên cồng kềnh và trở nên khó đọc, khó maintain. Mà đối với ngành IT việc requirement thay đổi, cần thêm sửa chức năng là rất bình thường, nên việc code trong sáng, dễ đọc dễ hiểu là rất cần thiết.
Không được sửa đổi một Class có sẵn, nhưng có thể mở rộng bằng kế thừa.
Theo nguyên lý này, mỗi khi ta muốn thêm chức năng cho chương trình, chúng ta nên viết class mới mở rộng class cũ (bằng cách kế thừa hoặc sở hữu class cũ) chứ không nên sửa đổi class cũ. Việc này dẫn đến tình trạng phát sinh nhiều class, nhưng chúng ta sẽ không cần phải test lại các class cũ nữa, mà chỉ tập trung vào test các class mới, nơi chứa các chức năng mới.
Các đối tượng (instance) kiểu class con có thể thay thế các đối tượng kiểu class cha mà không gây lỗi.
Nguyên tắc này nói về quan hệ kế thừa giữa các lớp trong lập trình hướng đối tượng và quy định các điều kiện mà các lớp con cần tuân thủ để có thể thay thế cho lớp cha mà không làm thay đổi tính đúng đắn của chương trình.
Thay vì dùng 1 interface lớn, ta nên tách thành nhiều interface nhỏ, với nhiều mục đích cụ thể.
Hãy tưởng tượng chúng ta có 1 interface lớn, khoảng 100 methods. Việc implements sẽ rất vất vả vì các class impliment interface này sẽ bắt buộc phải phải thực thi toàn bộ các method của interface. Ngoài ra còn có thể dư thừa vì 1 class không cần dùng hết 100 method. Khi tách interface ra thành nhiều interface nhỏ, gồm các method liên quan tới nhau, việc implement và quản lý sẽ dễ hơn.
Có thể hiểu nguyên lí này như sau: những thành phần trong 1 chương trình chỉ nên phụ thuộc vào những cái trừu tượng (abstraction). Những thành phần trừu tượng không nên phụ thuộc vào các thành phần mang tính cụ thể mà nên ngược lại.
Những cái trừu tượng (abstraction) là những cái ít thay đổi và biến động, nó tập hợp những đặc tính chung nhất của những cái cụ thể. Những cái cụ thể dù khác nhau thế nào đi nữa đều tuân theo các quy tắc chung mà cái trừu tượng đã định ra. Việc phụ thuộc vào cái trừu tượng sẽ giúp chương trình linh động và thích ứng tốt với các sự thay đổi diễn ra liên tục.
SOLID Giúp lập trình viên:
Rõ ràng dễ hiểu
Dễ thay đổi
Tái sử dụng
Dưới đây là 10 câu hỏi phỏng vấn về nguyên tắc SOLID trong lập trình hướng đối tượng (OOP), kèm theo câu trả lời chuẩn được diễn đạt chính xác theo chuyên ngành IT và dịch sang tiếng Việt một cách tự nhiên.
Câu hỏi: SOLID là viết tắt của gì và tại sao nó quan trọng trong lập trình hướng đối tượng? Câu trả lời: SOLID là tập hợp 5 nguyên tắc thiết kế trong lập trình hướng đối tượng, bao gồm:
S: Single Responsibility Principle (Nguyên tắc trách nhiệm duy nhất)
O: Open/Closed Principle (Nguyên tắc mở/đóng)
L: Liskov Substitution Principle (Nguyên tắc thay thế Liskov)
I: Interface Segregation Principle (Nguyên tắc phân tách giao diện)
D: Dependency Inversion Principle (Nguyên tắc đảo ngược phụ thuộc) Nó quan trọng vì giúp mã nguồn dễ bảo trì, mở rộng, tái sử dụng và giảm thiểu lỗi khi thay đổi hoặc phát triển phần mềm.
Câu hỏi: Giải thích SRP và đưa ra ví dụ khi nó bị vi phạm.
Câu trả lời:
SRP quy định rằng một lớp chỉ nên có một lý do duy nhất để thay đổi, tức là chỉ chịu trách nhiệm cho một chức năng cụ thể.
Ví dụ vi phạm: Một lớp UserManager
vừa xử lý việc lưu thông tin người dùng vào cơ sở dữ liệu, vừa gửi email thông báo. Nếu logic gửi email thay đổi, lớp này cũng phải thay đổi, vi phạm SRP.
Giải pháp: Tách thành hai lớp: UserRepository
(lưu trữ dữ liệu) và EmailService
(gửi email).
Câu hỏi: OCP là gì và làm thế nào để áp dụng nó trong mã nguồn?
Câu trả lời:
OCP yêu cầu các thực thể phần mềm (lớp, module) phải mở để mở rộng nhưng đóng để sửa đổi. Nghĩa là bạn có thể thêm chức năng mới mà không cần thay đổi mã hiện có.
Ví dụ: Một lớp PaymentProcessor
xử lý thanh toán bằng thẻ tín dụng. Nếu muốn thêm thanh toán qua ví điện tử, thay vì sửa lớp này, bạn có thể tạo giao diện PaymentMethod
và triển khai các lớp như CreditCardPayment
, WalletPayment
.
Cách áp dụng: Sử dụng tính trừu tượng (interface/abstract class) và đa hình (polymorphism).
Câu hỏi: LSP là gì và tại sao cần tuân theo nó?
Câu trả lời:
LSP quy định rằng các đối tượng của lớp con phải có thể thay thế cho đối tượng của lớp cha mà không làm thay đổi tính đúng đắn của chương trình.
Ví dụ vi phạm: Một lớp Bird
có phương thức fly()
, nhưng lớp con Penguin
không bay được. Nếu thay Bird
bằng Penguin
trong ngữ cảnh mong đợi bay, chương trình sẽ lỗi.
Giải pháp: Tách thành các giao diện như FlyingBird
và WalkingBird
.
Ý nghĩa: Đảm bảo tính đa hình hoạt động đúng và mã nguồn dễ hiểu.
Câu hỏi: ISP là gì và đưa ra một ví dụ thực tế.
Câu trả lời:
ISP yêu cầu các lớp không nên bị ép buộc triển khai các giao diện mà chúng không sử dụng. Thay vào đó, giao diện nên được chia nhỏ để phù hợp với nhu cầu cụ thể.
Ví dụ: Giao diện Worker
có các phương thức work()
và eat()
. Một lớp RobotWorker
không cần eat()
nhưng vẫn phải triển khai nó.
Giải pháp: Tách thành Workable
(có work()
) và Eatable
(có eat()
), để RobotWorker
chỉ triển khai Workable
.
Lợi ích: Giảm mã dư thừa và tăng tính linh hoạt.
Câu hỏi: DIP là gì và cách triển khai nó trong Java?
Câu trả lời:
DIP quy định rằng các module cấp cao không nên phụ thuộc vào module cấp thấp, cả hai nên phụ thuộc vào abstraction (trừu tượng). Ngoài ra, abstraction không nên phụ thuộc vào chi tiết, mà chi tiết phụ thuộc vào abstraction.
Ví dụ: Một lớp OrderService
phụ thuộc trực tiếp vào MySQLDatabase
. Nếu đổi sang MongoDB, phải sửa mã.
Giải pháp: Tạo giao diện Database
và để OrderService
phụ thuộc vào Database
. Các lớp MySQLDatabase
, MongoDBDatabase
triển khai giao diện này.
Cách triển khai: Sử dụng Dependency Injection (ví dụ: qua Spring).
Câu hỏi: Giải thích mối liên hệ giữa SRP và khả năng bảo trì mã nguồn.
Câu trả lời:
Khi một lớp có nhiều trách nhiệm (vi phạm SRP), bất kỳ thay đổi nào trong một trách nhiệm đều có thể ảnh hưởng đến các trách nhiệm khác, làm tăng nguy cơ lỗi và phức tạp hóa việc bảo trì.
Ví dụ: Lớp ReportGenerator
vừa tạo báo cáo vừa in báo cáo. Nếu logic in thay đổi, mã tạo báo cáo cũng có thể bị ảnh hưởng không cần thiết.
Kết quả vi phạm: Mã trở nên "mong manh" (fragile) và khó kiểm thử.
Câu hỏi: OCP có thể được đánh giá như thế nào trong một dự án thực tế? Câu trả lời: Để kiểm tra OCP, hãy xem liệu bạn có thể thêm chức năng mới mà không sửa đổi mã hiện có hay không. Cách đánh giá:
Nếu thêm một tính năng mới yêu cầu chỉnh sửa lớp cũ, thiết kế vi phạm OCP.
Nếu bạn có thể mở rộng qua kế thừa hoặc triển khai giao diện mà không chạm vào mã cũ, thiết kế tuân thủ OCP. Ví dụ: Một hệ thống thêm phương thức thanh toán mới bằng cách triển khai giao diện mà không sửa lớp chính là tuân thủ OCP.
Câu hỏi: Giải thích mối quan hệ giữa LSP và tính kế thừa.
Câu trả lời:
LSP đảm bảo rằng tính kế thừa trong OOP không phá vỡ hành vi mong đợi của chương trình. Khi lớp con kế thừa lớp cha, nó phải duy trì tính tương thích về hành vi để có thể thay thế lớp cha mà không gây lỗi.
Ví dụ: Nếu lớp cha Shape
có phương thức area()
, lớp con Square
phải trả về diện tích đúng, không được ném ngoại lệ không mong đợi.
Mối quan hệ: LSP giúp kế thừa an toàn và đáng tin cậy.
Câu hỏi: So sánh ISP và DIP, đưa ra ví dụ minh họa. Câu trả lời:
ISP: Tập trung vào việc chia nhỏ giao diện để tránh ép buộc lớp triển khai các phương thức không cần thiết. Ví dụ: Tách Printable
và Scannable
thay vì dùng một giao diện lớn Device
.
DIP: Tập trung vào việc đảo ngược phụ thuộc, khiến các lớp phụ thuộc vào abstraction thay vì chi tiết cụ thể. Ví dụ: PrinterService
phụ thuộc vào giao diện Printer
, không phải HPPrinter
.
Khác biệt: ISP giải quyết cấu trúc giao diện, còn DIP giải quyết mối quan hệ phụ thuộc giữa các module.
Ví dụ minh họa: Một hệ thống in có thể dùng ISP để tách giao diện và DIP để đảm bảo PrintManager
không phụ thuộc trực tiếp vào HPPrinter
.