Ole-Johan Dahl và Kristen Nygaard được cho là cha đẻ của lập trình hướng đối tượng khi ông cho ra đời của ngôn ngữ lập trình Simula vào năm 1960 . Cho tới năm 1970, Alan Kay tiếp tục đưa ra các khái niệm nền móng cho lập trình hướng đối tượng khi đề xuất định nghĩa lớp, kế thừa và đa hình trong ngôn ngữ Smalltalk.
Quá trình phát triển của lập trình hướng đối tượng tiếp tục được hoàn thiện dưới sụ đóng góp của cộng đồng các nhà khoa học máy tính, với sự ra đời của OOP trong ngôn ngữ C++ và Ada trong nhưng năm 1980, JAVA năm 1995.
Cột mốc đánh dấu sự hoàn thiện OOP là năm 2000, khi Microsoft cho ra đời ngôn ngữ C#, ngôn ngữ tập trung vào hướng đối tượng. Cùng năm đó, kỹ sư phần mềm Robert C. Martin, còn được biết tới với biệt danh "Uncle Bob", đã cho ra đời cuốn sách "Design Principles and Design Patterns". Trong cuốn sách này ông nhấn mạnh việc lập trình hướng đối tượng phải đáp ứng được khả năng bảo trì và mở rộng bằng cách đề xuất ra 5 nguyên tắc cơ bản gọi là SOLID - viết tắt của của 5 chữ cái đầu của mỗi nguyên tắc:
- S - Single Responsibility Principle (Nguyên tắc Đơn Nhiệm)
- 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 Chia Interface)
- D - Dependency Inversion Principle (Nguyên tắc Đảo Ngược Phụ Thuộc)
Các phần định nghĩa dưới đây được trích trong sách "Design Principles and Design Patterns" và tham khảo Wikipedia.
1. Single Responsibility Principle (SRP)
"A class should have one and only one reason to change, meaning that a class should have only one job"
Nguyên tắc SRP được hiểu là khi cần thay đổi một Class thì chỉ có duy nhất một lý do. Nghĩa là mỗi Class chỉ nên đảm nhiệm một nhiệm vụ duy nhất mà thôi. Nếu một Class có nhiều hơn một nhiệm vụ trong chương trình, thì cách tốt nhất là nên tách nó thành nhiều Class khác nhau. Khi cần chỉnh sửa một chức năng nào đó thì bạn chỉ cần sửa trong chính Class đảm nhiệm chức năng đó mà không ảnh hưởng tới Class khác hoặc chương trình.
Ví dụ về không tuân thủ nguyên tắc SRP:
// Không tuân thủ SRP
class Order {
private int orderId;
private String customer;
private List<Item> items;
public Order(int orderId, String customer, List<Item> items) {
this.orderId = orderId;
this.customer = customer;
this.items = items;
}
public double calculateTotal() {
// Tính tổng số tiền đơn hàng
double total = 0;
for (Item item : items) {
total += item.getPrice() * item.getQuantity();
}
return total;
}
public String generateInvoice() {
// Tạo hóa đơn cho đơn hàng
return String.format("Order ID: %d\nCustomer: %s\nTotal: %.2f", orderId, customer, calculateTotal());
}
}
Hàm tạo hoá đơn generateInvoice
đảm nhiệm chức năng tạo hoá đớn - chức năng hoàn toàn khác với Order
nên cần tách nó ra thành một Class mới, như sau:
class InvoiceGenerator {
public static String generateInvoice(Order order) {
// Tạo hóa đơn cho đơn hàng
return String.format("Order ID: %d\nCustomer: %s\nTotal: %.2f", order.getOrderId(), order.getCustomer(), order.calculateTotal());
}
}
Lưu ý rằng, việc tách một Class có nhiều nhiệm vụ thành nhiều Class khác nhau không đồng nghĩa với việc tách các phương thức (method) thành từng Class riêng biệt. Mỗi Class đảm trách một nhiệm vụ có thể có nhiều phương thức khác nhau. Việc nhóm các phương thức cùng giải quyết một nhiệm vụ về một Class đòi hỏi kiến thức và kinh nghiệm.
2. Open/Closed Principle (OCP)
"Objects or entities should be open for extension but closed for modification"
Các đối tượng hoặc thực thể nên được "mở" để mở rộng nhưng phải "đóng" khi cần chỉnh sửa.
Có thể hiểu là một lớp phải có khả năng mở rộng mà không cẩn chỉnh sửa chính lớp đó. Để làm được việc này chúng ta có thể sử dụng kế thừa, interface, hoặc composition.
Ví dụ:
// Nguyên tắc Open/Closed Principle
// Interface cho các hình học
interface Shape {
calculateArea(): number;
}
// Lớp hiện thực cho hình tròn
class Circle implements Shape {
private radius: number;
constructor(radius: number) {
this.radius = radius;
}
calculateArea(): number {
return Math.PI * this.radius * this.radius;
}
}
// Lớp hiện thực cho hình chữ nhật
class Rectangle implements Shape {
private width: number;
private height: number;
constructor(width: number, height: number) {
this.width = width;
this.height = height;
}
calculateArea(): number {
return this.width * this.height;
}
}
Trong ví dụ này, Shape
là một Interface đại diện cho các hình học và có một phương thức calculateArea
để tính diện tích của hình. Cả hai lớp Circle
và Rectangle
đều thực hiện Interface Shape
, mỗi lớp cung cấp cách tính diện tích của hình mình.
Nếu cần thiết chúng ta có thể tiếp tục mở rộng thêm cách tính diện tích các hình khối khác (tam giác, tứ giác...) bằng cách tiếp tục Implement Interface Shape mà không ảnh hưởng tới các lớp hình học khác. Khi cần chỉnh sửa một lớp cụ thể nào đó thì không cần sửa trong Interface mà chỉ cần sửa trong chính lớp đó.
3. Liskov Substitution Principle (LSP)
Nguyên tắc này do Barbara Liskov đề xuất năm 1987.
Nội dung của LSP:
"Let q(x) be a property provable about objects of x of type T. Then q(y) should be provable for objects y of type S where S is a subtype of T."
Nghĩa là: Giả sử q(x) là một tính chất có thể chứng minh được về các đối tượng x thuộc loại T. Khi đó q(y) phải chứng minh được đối với các đối tượng y thuộc loại S trong đó S là kiểu con của T.
Bỏ qua hình thức toán học của nó, trong lập trình hướng đối tượng thì nguyên tắc này được hiểu là:
"Objects of a superclass should be replaceable with objects of its subclasses without affecting the correctness of the program".
Nghĩa là các đối tượng của lớp cơ sở có thể được thay thế bằng các đối tượng của lớp con của nó mà không ảnh hưởng tới tính đúng đắn của chương trình.
Chúng ta có thể xem xét ví dụ sau:
Giả sử có 2 Class, một là Class hình chữ nhật (Rectangular), hai là Class hình vuông (Square). Về cơ bản, chúng ta đều biết rằng, hình vuông là hình chữ nhật có chiều dài và chiều rộng bằng nhau nên có thể cho hình vuông kế thừa từ hình chữ nhật. Tuy nhiên nếu hành vi của đối tượng hình vuông(lớp con) vi phạm hành vi của hình chữ nhật (lớp cha) thì nguyên tắc Liskov bị vi phạm.
// Nguyên tắc Liskov Substitution Principle - Vi phạm
// Lớp cơ sở
class Rectangle {
protected width: number;
protected height: number;
constructor(width: number, height: number) {
this.width = width;
this.height = height;
}
getWidth(): number {
return this.width;
}
getHeight(): number {
return this.height;
}
setWidth(width: number): void {
this.width = width;
}
setHeight(height: number): void {
this.height = height;
}
getArea(): number {
return this.width * this.height;
}
}
// Lớp kế thừa từ Rectangle - Hình vuông (Vi phạm)
class Square extends Rectangle {
constructor(side: number) {
super(side, side);
}
setWidth(width: number): void {
super.setWidth(width);
super.setHeight(width); // Vi phạm nguyên tắc, làm thay đổi chiều cao
}
setHeight(height: number): void {
super.setWidth(height); // Vi phạm nguyên tắc, làm thay đổi chiều rộng
super.setHeight(height);
}
}
// Hàm sử dụng đối tượng Rectangle
function printArea(rectangle: Rectangle): void {
console.log(`Area: ${rectangle.getArea()}`);
}
Trong ví dụ này, lớp Square
thừa kế từ Rectangle
, nhưng khi nó triển khai các phương thức setWidth
và setHeight
, nó vi phạm nguyên tắc LSP bằng cách làm thay đổi chiều rộng và chiều cao cùng một lúc.
Trong trường hợp này, để chương trình không vi phạm nguyên tắc LSP, ta phải tạo một class cha là class Shape, sau đó cho Square và Rectangle kế thừa class Shape này.
4. Interface Segregation Principle (ISP)
"A client should never be forced to implement an interface that it doesn’t use, or clients shouldn’t be forced to depend on methods they do not use."
Không nên buộc một Client triển khai một Interface mà nó không sử dụng hoặc bắt nó phụ thuộc vào một Method mà nó không dùng đến.
Như vậy nguyên tắc Interface Segregation Principle (ISP) khuyến khích chia nhỏ các interface thành các phần nhỏ để không buộc các lớp phải triển khai các phương thức không liên quan đến nhiệm vụ của chúng.
Việc này làm giảm sự phụ thuộc vào các Method không cần thiết và làm cho mã nguồn linh hoạt, dễ mở rộng và bảo trì hơn.
Ví dụ:
// Nguyên tắc Interface Segregation Principle
// Interface cơ bản
interface Order {
processOrder(): void;
calculateTotal(): number;
sendConfirmationEmail(): void;
}
// Interface cho việc xử lý đơn hàng
interface OrderProcessor {
processOrder(): void;
}
// Interface cho việc tính tổng số tiền
interface TotalCalculator {
calculateTotal(): number;
}
// Interface cho việc gửi email xác nhận
interface EmailSender {
sendConfirmationEmail(): void;
}
// Lớp implement tất cả các Interface (Tuân thủ ISP)
class OrderHandler implements OrderProcessor, TotalCalculator, EmailSender {
processOrder(): void {
console.log("Processing order...");
}
calculateTotal(): number {
console.log("Calculating total...");
return 100; // Giả sử là giá trị total
}
sendConfirmationEmail(): void {
console.log("Sending confirmation email...");
}
}
// Sử dụng
const orderHandler: OrderProcessor = new OrderHandler();
orderHandler.processOrder();
Trong ví dụ này, chúng ta đã chia nhỏ Order
interface thành các interface con (OrderProcessor
, TotalCalculator
, và EmailSender
). Mỗi interface con đại diện cho một phần cụ thể của nhiệm vụ đặt hàng.
OrderHandler
là một lớp thực hiện tất cả các interface con. Điều này cho phép chọn lựa các tính năng cần thiết và tránh triển khai các phương thức không liên quan.
5. Dependency Inversion Principle (DIP)
"Entities must depend on abstractions, not on concretions. It states that the high-level module must not depend on the low-level module, but they should depend on abstractions."
Có thể hiểu là:
- Các module cấp cao không nên phụ thuộc vào các module cấp thấp mà cả hai nên phụ thuộc vào abtraction.
- Abstraction không nên phụ thuộc vào Detail, mà Detail nên phụ thuộc vào abstraction.
Ví dụ
// Interface định nghĩa định dạng xuất
interface OutputFormatter {
format(data: any): string;
}
// Các lớp thực hiện định dạng xuất cụ thể
class HtmlFormatter implements OutputFormatter {
format(data: any): string {
// Chuyển đổi data thành định dạng HTML
return "<html>" + JSON.stringify(data) + "</html>";
}
}
class JsonFormatter implements OutputFormatter {
format(data: any): string {
// Chuyển đổi data thành định dạng JSON
return JSON.stringify(data);
}
}
// Lớp sử dụng OutputFormatter để xuất thông tin sách
class BookExporter {
private formatter: OutputFormatter;
constructor(formatter: OutputFormatter) {
this.formatter = formatter;
}
export(book: any): void {
const data = this.getBookData(book);
const formattedData = this.formatter.format(data);
// Logic để xuất formattedData
console.log(formattedData);
}
private getBookData(book: any): any {
// Logic để lấy thông tin của sách
return { title: book.title, author: book.author, /* ... other book data */ };
}
}
// Sử dụng
const htmlFormatter = new HtmlFormatter();
const jsonFormatter = new JsonFormatter();
const bookExporterHTML = new BookExporter(htmlFormatter);
bookExporterHTML.export({ title: "The Book", author: "John Doe" });
const bookExporterJSON = new BookExporter(jsonFormatter);
bookExporterJSON.export({ title: "The Book", author: "John Doe" });
Trong ví dụ này, BookExporter
không phụ thuộc trực tiếp vào các định dạng xuất cụ thể như HtmlFormatter
hoặc JsonFormatter
, mà thay vào đó, nó phụ thuộc vào một interface chung là OutputFormatter
. Điều này giúp tăng tính linh hoạt và tái sử dụng mã, vì bạn có thể dễ dàng thêm hoặc thay đổi định dạng xuất mà không ảnh hưởng đến BookExporter
.
6. Kết luận
SOLID hay bộ 5 nguyên tắc trong lập trình hướng đối tượng do Robert C. Martin đề xuất không những đã chứng minh được tính đúng đắn trong thực tế, mà những nguyên tắc này còn tạo ra một cơ sở cho việc phát triển mã nguồn dễ hiểu, linh hoạt, và dễ bảo trì trong quá trình phát triển phần mềm. Chúng cũng thúc đẩy sự tái sử dụng mã nguồn và giảm nguy cơ xuất hiện lỗi khi thay đổi hoặc mở rộng hệ thống.
Nếu bạn quan tâm các chủ đề về lập trình frontend, backend, mời bạn tham khảo thêm các bài viết khác từ blog 200Lab: