Series SOLID cho thanh niên code CỨNG: Open/Closed Principle

Giới thiệu

Đây là đây là bài viết thứ 2 trong series SOLID cho thanh niên code cứng. Ở bài viết này, mình sẽ nói về Open/Closed Principle – Nguyên lý Đóng Mở.

  1. Single Responsibility Principle
  2. Open/Closed Principle
  3. Liskov Substitution Principle
  4. Interface Segregation Principle
  5. Dependency Inversion Principle

Nội dung nguyên lý:

Chiều dài váy con gái nên đủ ngắn để khơi MỞ tính tò mò của con trai, nhưng nên đủ dài để ĐÓNG lại những suy nghĩ đen tối của bọn nó.

À xin lỗi, vừa viết bài vừa học tiếng Nhật thông qua phim ảnh nên mình phát biểu nguyên lý hơi nhầm 1 tí. Nội dung nguyên lý đây:

Có thể thoải mái mở rộng 1 module, nhưng hạn chế sửa đổi bên trong module đó (open for extension but closed for modification).

SOLID-small

Giải thích nguyên lý

Theo nguyên lý này, một module cần đáp ứng 2 điều kiện sau:

  • Dễ mở rộng: Có thể dễ dàng nâng cấp, mở rộng, thêm tính năng mới cho một module khi có yêu cầu.
  • Khó sửa đổi: Hạn chế hoặc cấm việc sửa đổi source code của module sẵn có.

Hai điều kiện này thoạt nghe có vẻ mâu thuẫn quá nhỉ? Theo lẽ thường, khi muốn thêm chức năng thì ta phải viết thêm code hoặc sửa code đã có. Đằng này, nguyên lý lại hạn chế việc sửa đổi source code!! Vậy làm thế nào để ta có thể thiết kế một module dễ mở rộng, nhưng lại khó sửa đổi?

Trước khi nói về lập trình, hãy cùng phân tích một vật dụng được thiết kế chuẩn theo Nguyên lý Đóng Mở: khẩu súng. Để bắn được xa hơn, ta có thể gắn thêm ống ngắm; để không gây tiếng động, ta có thể gắn nòng giảm thanh; để tăng số lượng đạn, ta có thể gắn thêm băng đạn phụ; khi cần cận chiến ta có thể gắn lưỡi lê vào luôn.

Dễ thấy, khẩu súng được thiết kế để ta dễ dàng mở rộng tính năng mà không cần phải mổ xẻ tháo lắp các bộ phận bên trong (source code) của nó. Một module phù hợp OCP cũng nên được thiết kế như vậy.

extensions

Ví dụ minh họa

Ta hãy cùng đọc code trong ví dụ dưới đây:


// Ta có 3 class: vuông, tròn, tam giác, kế thừa class Shape
public class Shape
{
}
public class Square : Shape
{
public double Height { get; set; }
}
public class Circle : Shape
{
public double Radius { get; set; }
}
public class Triangle : Shape
{
public double FirstSide { get; set; }
public double SecondSide { get; set; }
public double ThirdSide { get; set; }
}
// Module in ra diện tích các hình
public class AreaDisplay
{
public double ShowArea(List<Shape> shapes)
{
foreach (var shape in shapes) {
// Nếu yêu cầu thay đổi, thêm shape khác, ta phải sửa module Area Calculator
if (shape is Square) {
Square square = (Square)shape;
var area += Math.Sqrt(square.Height);
Console.WriteLine(area);
}
if (shape is Triangle) {
Triangle triangle = (Triangle)shape;
double TotalHalf = (triangle.FirstSide + triangle.SecondSide + triangle.ThirdSide) / 2;
var area += Math.Sqrt(TotalHalf * (TotalHalf – triangle.FirstSide) *
(TotalHalf – triangle.SecondSide) * (TotalHalf – triangle.ThirdSide));
Console.WriteLine(area);
}
if (shape is Circle) {
Circle circle = (Circle)shape;
var area += circle.Radius * circle.Radius * Math.PI;
Console.WriteLine(area);
}
}
}
}

view raw

old.cs

hosted with ❤ by GitHub

Ta có 3 class là Square, Circle, Rectangle. Class AreaDisplay tính diện tích các hình này và in ra. Theo code cũ, để tính diện tích, ta cần dùng hàm if để check và ép kiểu object đưa vào, sau đó bắt đầu tính. Dễ thấy nếu trong tương lại ta thêm nhiều class nữa, ta phải sửa class AreaDisplay, viết thêm chừng đó hàm if nữa. Sau khi chỉnh sửa, ta phải compile và deploy lại class AreaDisplay, dài dòng và dễ lỗi phải không nào??

Áp dụng OCP, ta sẽ cải tiến lại như sau:


// Ta có 3 class: vuông, tròn, tam giác, kế thừa class Shape
// Chuyển logic tính diện tích vào mỗi class
public abstract class Shape
{
public double Area();
}
public class Square : Shape
{
public double Height { get; set; }
public double Area() {
return Math.Sqrt(this.Height);
}
}
public class Circle : Shape
{
public double Radius { get; set; }
public double Area() {
return this.Radius * this.Radius * Math.PI;
}
}
public class Triangle : Shape
{
public double FirstSide { get; set; }
public double SecondSide { get; set; }
public double ThirdSide { get; set; }
public double Area() {
double TotalHalf = (this.FirstSide + this.SecondSide + this.ThirdSide) / 2;
var area += Math.Sqrt(TotalHalf * (TotalHalf – this.FirstSide) *
(TotalHalf – this.SecondSide) * (TotalHalf – this.ThirdSide));
return area;
}
}
// Module in ra diện tích các hình
public class AreaDisplay
{
public double ShowArea(List<Shape> shapes)
{
foreach (var shape in shapes) {
Console.WriteLine(shape.Area());
}
}
}

view raw

new.cs

hosted with ❤ by GitHub

Ta chuyển module tính diện tích vào mỗi class. Class AreaDisplay chỉ việc in ra. Trong tương lai, khi thêm class mới, ta chỉ việc cho class này kế thừa class Shape ban đầu. Class AreaDisplay có thể in ra diện tích của các class thêm vào mà không cần sửa gì tới source code của nó cả.

Lưu ý và kết luận

Nguyên lý OCP xuất hiện khắp mọi ngóc ngách của ngành lập trình. Một ứng dụng của nguyên lý này là hệ thống plug-in (cho Eclipse, Visual Studio, add-on Chrome). Để thêm các tính năng mới cho phần mềm, ta chỉ việc cài đặt các plug-in này, không cần can thiệp gì đến source code sẵn có.

Nguyên lý này cũng được áp dụng chặt chẽ khi viết các thư viện/framework. Ví dụ, khi sử dụng MVC, ta không được xem hay chỉnh sửa code của các class Router, Controller có sẵn, nhưng có thể viết các Controller mới, Router mới để thêm tính năng. Hoặc khi sử dụng ionicFramework, ta có thể tải thêm plug-in để sử dụng chức năng chụp hình, quét barcode mà không cần động vào source code của ionic.

Khi áp dụng nguyên lý này trong thiết kế, ta cần phải xác định được những thứ cần thay đổi, để thiết kế phù hợp với thay đổi đó. Đây là một việc rất rất KHÓ, kể cả với developer lâu năm. Nó đòi hỏi nhiều kinh nghiệm, tầm nhìn và một ít khả năng dự đoán. Còn nếu như lỡ đoán sai những điều cần thay đổi thì sao? Cứ viết code cho chạy trước đã rồi refactor dần dần là được thôi =))).

refac

Những nguyên tắc còn lại của SOLID sẽ được giới thiệu ở những bài viết sau nhé. Có thắc mắc hay góp ý gì các bạn cứ thoải mái nêu ra trong phần comment, mình sẽ cố gắng giải đáp.

Một số tài liệu để tham khảo thêm:

10 thoughts on “Series SOLID cho thanh niên code CỨNG: Open/Closed Principle”

  1. Mình xin góp ý cho bài viết của bạn (nhưng mình ko biết đúng ko nữa :D) là sử dụng Dependency Injection pattern là 1 ví dụ điển hình cho Open/Closed principle này

    Like

    1. Mình thì thấy nếu bạn không chắc chắn thì đừng nên góp ý vì sẽ gây hoang mang cho người đọc.
      Với lại mình nghĩ khi giới thiệu 1 khái niệm gì đó mới thì nên dùng một ví dụ đơn giản dễ hiểu, chứ như bạn nói thì mình đang tìm hiểu bài này lại phải nhảy sang tìm hiểu DI, khá là khó và mất thời gian 😀

      Like

  2. Dependency Injection pattern là thuộc nguyên tắc thứ 5, chữ D trong SOLID chứ đâu fai là ở phần chữ O này? Bài viết khá hay, tất nhiên để hiểu sâu hơn thì cần đọc thêm tài liệu code tiếng Anh và thực hành nhiều hơn. Cảm ơn admin về bài viết.

    Like

  3. Bạn cần chú trọng hơn khi lấy ví dụ vì code C# này chưa được đúng lắm, phần abstract class đã khai báo double Area() rồi thì các class kế thừa nó phải override Area() ^^

    Like

Leave a comment