Trang chủ / Thiết Kế & Kiến Trúc Phần Mềm
Bài học lập trình từ tệp trống: Các nguyên tắc thiết kế hướng đối tượng được giải thích rõ ràng
Mọi chương trình lớn đều bắt đầu theo cách tương tự: từ một tệp trống. Không có lớp, không có hàm, không có phụ thuộc. Chỉ có con trỏ nhấp nháy trên màn hình trống. Sự trống rỗng đó vừa là tự do vừa là trách nhiệm, bởi những quyết định đầu tiên bạn đưa ra sẽ định hình mức độ dễ đọc, dễ kiểm thử và dễ thay đổi của mã nguồn nhiều tháng sau đó. Đây là lúc nguyên tắc thiết kế hướng đối tượng SOLID giải thích đơn giản thực sự tạo ra khác biệt. Thay vì lý thuyết trừu tượng, SOLID mang đến cho bạn năm quy tắc thực tế để xây dựng phần mềm luôn gọn gàng và có thể tái sử dụng, kể cả khi nó phát triển từ tệp trống đầu tiên thành một hệ thống hoàn chỉnh.
Trong lập trình, thiết kế hướng đối tượng không có nghĩa là dùng lớp cho mọi thứ. Đó là mô hình hóa trách nhiệm, xác định ranh giới rõ ràng và quản lý phụ thuộc để mỗi phần của hệ thống làm tốt một việc và phối hợp mạch lạc với các phần còn lại. SOLID là cụm từ viết tắt của năm nguyên tắc hướng dẫn những quyết định đó. Khi được áp dụng nhất quán, chúng làm giảm độ gắn kết giữa các thành phần, tăng tính gắn kết nội tại và giúp bạn dễ hiểu mã nguồn hơn mà không tạo thêm độ phức tạp không cần thiết.
Vì sao nên bắt đầu tư duy về thiết kế từ một tệp trống?
Khi bắt đầu với một tệp trống, bạn dễ chỉ tập trung vào việc làm cho mọi thứ hoạt động. Bạn viết một lớp vừa xử lý dữ liệu, vừa lưu vào cơ sở dữ liệu, vừa định dạng đầu ra và gửi thông báo, chỉ vì điều đó nhanh hơn ở thời điểm đó. Cách này hoạt động nhưng tạo ra m���t chi phí ẩn. Mỗi yêu cầu mới đều buộc bạn sửa cùng một lớp, làm tăng nguy cơ làm hỏng thứ gì đó không liên quan.
Thiết kế tốt đảo ngược khuôn mẫu đó. Nó đặt câu hỏi mỗi thành phần chịu trách nhiệm về điều gì và phần nào nên có thể thay đổi độc lập. Trả lời những câu hỏi này sớm, khi tệp vẫn còn trống, rẻ hơn nhiều so với tái cấu trúc một lớp rối rắm sau này. SOLID giúp bạn trả lời chúng một cách hệ thống, không phải bằng cách áp đặt các quy tắc cứng nhắc, mà bằng cách đưa ra những gợi ý bạn có thể kiểm tra trong khi viết mã.
Năm nguyên tắc SOLID qua cái nhìn tổng quan
SOLID là viết tắt của năm nguyên tắc thiết kế do Robert C. Martin lần đầu mô tả. Together, chúng tạo thành nền tảng cho phần mềm hướng đối tượng dễ bảo trì. Mỗi nguyên tắc giải quyết một loại vấn đề thiết kế khác nhau, từ kích thước lớp đến kế thừa và phụ thuộc.
- S – Nguyên tắc trách nhiệm đơn lẻ
- O – Nguyên tắc mở-khóa
- L – Nguyên tắc thay thế Liskov
- I – Nguyên tắc phân tách giao diện
- D – Nguyên tắc đảo ngược phụ thuộc
1. Nguyên tắc trách nhiệm đơn lẻ: Một lý do để thay đổi
Một lớp chỉ nên có một trách nhiệm và do đó chỉ có một lý do để thay đổi. Điều này không có nghĩa là lớp chỉ nên có một phương thức. Nó có nghĩa là lớp nên đảm nhận một công việc mạch lạc. Ví dụ, lớp đại diện cho hóa đơn nên xử lý dữ liệu và phép tính của hóa đơn, nhưng không nên đồng thời xử lý lưu vào cơ sở dữ liệu và gửi email.
Khi tách trách nhiệm, bạn thu được các lớp nhỏ hơn, tập trung hơn và dễ hiểu, dễ kiểm thử hơn. Nếu cách lưu dữ liệu thay đổi, bạn chỉ sửa lớp lưu trữ. Nếu định dạng email thay đổi, bạn chỉ sửa lớp thông báo. Một cách kiểm tra tốt cho nguyên tắc này là mô tả lớp của bạn trong một câu mà không dùng từ và. Nếu bạn cần từ và, rất có thể bạn đang có nhiều hơn một trách nhiệm.
2. Nguyên tắc mở-khóa: Mở để mở rộng, khóa để sửa đổi
Các thực thể phần mềm nên mở để mở rộng nhưng đóng để sửa đổi. Trong thực tế, điều này có nghĩa là bạn có thể thêm hành vi mới mà không cần sửa mã đã được kiểm thử. Bạn đạt được điều này thông qua trừu tượng hóa, chẳng hạn như giao diện, lớp trừu tượng hoặc tổ hợp.
Hãy xem xét một bộ xử lý thanh toán cho thẻ tín dụng. Nếu bạn thêm hỗ trợ cho phương thức thanh toán mới bằng cách thêm một câu lệnh if khác bên trong cùng phương thức, bạn có nguy cơ làm hỏng logic hiện có. Thay vào đó, định nghĩa một giao diện chung cho các phương thức thanh toán và để từng phương thức tự triển khai. Khi đó, thêm loại thanh toán mới nghĩa là thêm một lớp mới, không phải thay đổi bộ xử lý.
3. Nguyên tắc thay thế Liskov: Các kiểu con phải có thể thay thế được
Đối tượng của lớp cha phải có thể được thay thế bằng đối tượng của lớp con mà không làm hỏng chương trình. Nếu lớp con thay đổi hành vi dự kiến của lớp cha, nó vi phạm nguyên tắc này, kể cả khi mã vẫn biên dịch được.
Một ví dụ kinh điển là lớp Rectangle với chiều rộng và chiều cao, cùng lớp con Square ép chiều rộng và chiều cao phải bằng nhau. Mã mong đợi hình chữ nhật cho phép thay đổi độc lập chiều rộng và chiều cao sẽ thất bại khi nhận một hình vuông. Liskov nhắc bạn mô hình hóa kế thừa xoay quanh hành vi, không chỉ dữ liệu. Nếu lớp con không thể tuân thủ hợp đồng của lớp cha, hãy cân nhắc dùng tổ hợp hoặc một phân cấp khác.
4. Nguyên tắc phân tách giao diện: Giữ giao diện tập trung
Không khách hàng nào nên bị ép phụ thuộc vào các phương thức mà nó không dùng. Thay vì một giao diện lớn, đa năng, hãy tạo nhiều giao diện nhỏ, cụ thể. Điều này ngăn các lớp không bị rối bởi các triển khai trống hoặc không liên quan.
Hãy tưởng tượng một giao diện cho máy in đa chức năng với các phương thức in, quét và fax. Một lớp máy in đơn giản chỉ in vẫn sẽ bị buộc phải triển khai quét và fax. Bằng cách tách giao diện thành ba giao diện riêng, mỗi lớp chỉ triển khai đúng thứ nó thực sự cần. Kết quả là độ gắn kết lỏng hơn, ý định rõ hơn và hệ thống dễ tái cấu trúc, dễ kiểm thử hơn.
5. Nguyên tắc đảo ngược phụ thuộc: Phụ thuộc vào trừu tượng
Các module cấp cao không nên phụ thuộc vào các module cấp thấp. Cả hai đều nên phụ thuộc vào trừu tượng. Nói cách khác, đừng mã hóa cứng phụ thuộc vào các lớp cụ thể. Hãy phụ thuộc vào giao diện hoặc trừu tượng và để việc cung cấp triển khai cụ thể đến từ bên ngoài.
Ví dụ, một dịch vụ báo cáo không nên tự tạo trực tiếp một kết nối cơ sở dữ liệu cụ thể. Thay vào đó, nó nên phụ thuộc vào một trừu tượng về truy cập dữ liệu được tiêm vào khi ứng dụng khởi động. Sự đảo ngược này khiến logic cấp cao độc lập với chi tiết cấp thấp, dễ kiểm thử bằng mock và dễ thích nghi khi hạ tầng thay đổi. Đó chính là nguyên tắc phía sau dependency injection, một mẫu phổ biến trong các khung hiện đại.
Khởi động: Một quy trình làm việc cho tệp trống
Bạn áp dụng những ý tưởng này như thế nào khi thực sự bắt đầu viết mã? Hãy thử quy trình làm việc gọn nhẹ này trước khi viết lớp đầu tiên:
- Liệt kê trách nhiệm bằng ngôn ngữ thường. Nếu một trách nhiệm nghe như hai công việc, hãy tách nó.
- Vẽ phác thảo giao diện trước. Xác định mỗi đối tượng cần làm gì, không phải làm thế nào.
- Xác định những thứ có khả năng thay đ���i. Bọc những biến thể đó sau trừu tượng để bạn có thể mở rộng mà không cần sửa đổi.
- Kiểm tra phụ thuộc. Hỏi xem logic cấp cao của bạn có phụ thuộc vào các chi tiết cụ thể có thể được đảo ngược hay không.
- Viết nhỏ và tổ hợp. Ưu tiên nhiều lớp tập trung cùng phối hợp hơn một lớp lớn làm mọi thứ.
Cách tiếp cận này không yêu cầu thiết kế lớn ngay từ đầu. Đó là thói quen đặt những câu hỏi nhỏ, nhất quán để giữ thiết kế của bạn gọn gàng khi nó phát triển.
Những sai lầm phổ biến và cách tránh
SOLID thường bị hiểu sai là lời kêu gọi tạo thêm lớp và giao diện cho mọi tình huống. Điều đó dẫn đến kỹ thuật hóa quá mức. Mục tiêu không phải là áp dụng cả năm nguyên tắc ở khắp mọi nơi, mà là nhận ra khi nào thiết kế đang trở nên khó thay đổi và biết nguyên tắc nào hữu ích.
Sai lầm khác là coi SOLID là quy tắc về cú pháp thay vì về thiết kế. Việc tuân thủ từng chữ cái của các nguyên tắc nhưng bỏ qua ý định của chúng vẫn tạo ra mã cứng nhắc. Hãy tập trung vào các mục tiêu nền tảng: độ gắn kết thấp, tính gắn kết cao và các hợp đồng rõ ràng giữa các đối tượng. Khi một lớp dễ hiểu khi xét riêng lẻ, dễ kiểm thử mà không cần thiết lập phức tạp và an toàn khi thay đổi mà không có tác dụng phụ bất ngờ, bạn đang đi đúng hướng.
Cuối cùng, hãy nhớ rằng thiết kế gọn gàng là một quá trình lặp đi lặp lại. Ngay cả khi đã ghi nhớ SOLID, phiên bản đầu tiên của bạn sẽ không hoàn hảo. Hãy tái cấu trúc theo hướng các nguyên tắc khi bạn hiểu rõ hơn về vấn đề. Tệp trống chỉ là sự khởi đầu. Điều quan trọng là mỗi lần bổ sung đều khiến mã nguồn có thể bảo trì hơn một chút so với lúc bạn nhận nó.
