Open/Closed Principle

Ngày 16 tháng 10 năm 2017 | 216 views

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ý:

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).

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.

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);
      }
    }
  }
}

Ta có 3 class là SquareCircleRectangle. 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 AreaDisplayviế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());
    }
  }
}

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 ;)

Nguồn: toidicodedao.com

Hiểu Về Synchronized Trong Java

Ngày 13 tháng 9 năm 2018

13-09-2018
56
Dependency Inversion Principle

Ngày 24 tháng 10 năm 2017

24-10-2017
134
Interface Segregation Principle

Ngày 24 tháng 10 năm 2017

24-10-2017
146