Showing posts with label SOLID. Show all posts
Showing posts with label SOLID. Show all posts

Series SOLID cho thanh niên code CỨNG: Interface Segregation Principle – Phần 4

Đây là đây là bài viết thứ tư trong series “SOLID cho thanh niên code cứng”. Ở bài viết này, mình sẽ nói về Interface Segregation Principle – Nguyên lý phân tách interface.
  1. Single Responsibility Principle
  2. Open/Closed Principle
  3. Liskov Substitution Principle
  4. Interface Segregation Principle
  5. Dependency Inversion Principle
Interface Segregation Principle – Nguyên lý phân tách interface:
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ể
1

Giải thích nguyên lý

Trước khi giải thích nguyên lý, mình nhắc lại khái niệm interface một chút cho các bạn mất căn bản nhé.
Interface là một lớp rỗng chỉ chứa khai báo về tên phương thức không có khai báo về thuộc tính hay thứ gì khác và các phương thức này cũng là rỗng. Bởi vậy bất kỳ lớp nào sử dụng lớp interface đều phải định nghĩa các phương thức đã khai báo ở lớp interface.
Để thiết kế một hệ thống linh hoạt, dễ thay đổi, các module của hệ thống nên giao tiếp với nhau thông qua interface. Mỗi module sẽ gọi chức năng của module khác thông qua interface mà không cần quan tâm tới implementation bên dưới.  Như đã nói ở trên, do interface chỉ chứa khai báo rỗng về method, khi một class implement một interface, class đó phải implement toàn bộ các method được khai báo trong interface đó.
Điều này tương đương với việc nếu ta tạo ra 1 interface bự (hơn 100 method chẳng hạn), mỗi class sẽ phải implement toàn bộ 100 method đó, kể những method không bao giờ sử dụng đến. Nếu áp dụng ISP, ta sẽ chia interface này ra thành nhiều interface nhỏ, các class chỉ cần implement những interface có chức năng mà chúng cần, không cần phải implement những chức năng thừa nữa.
2

Ví dụ minh họa

Đây là nguyên lý dễ hiểu nhất trong SOLID, các bạn chỉ đọc code một chút là hiểu ngay! Giả sử ta muốn viết một chương trình giới thiệu thuộc tính của các loài động vật. Động vật nào cũng có thể ăn, uống, ngủ, ta thiết kế interface IAnimal như sau:
public interface IAnimal {
  void Eat();
  void Drink();
  void Sleep();
}
public class Dog : IAnimal {
  public void Eat () {} 
  public void Drink () {} 
  public void Sleep () {} 
}
public class Cat : IAnimal {
  public void Eat () {} 
  public void Drink () {} 
  public void Sleep () {} 
}
Khi ta muốn thêm 1 số loài động vật mới và tính năng vào, ta phải thêm có thêm method vào trong interface như: bơi lội, bay, săn mồi, … Điều này làm interface phình to ra. Khi một loài động vật kế thừa interface, nó phải implement luôn cả những hàm không dùng đến.
public interface IAnimal {
  void Eat();
  ...
  void Swim();
  void Fly();
}
public class Fish : IAnimal {
  public void Eat () {} 
  ...
  public void Swim() {}
  public void Fly() { throw new Exception("Cá éo biết bay"); }
}
public class Bird : IAnimal {
  public void Eat () {} 
  ...
  public void Swim() { throw new Exception("Chim éo biết bơi"); }
  public void Fly()
}
Trong thực tế cũng vậy, khi cần thêm chức năng mới, ta thường thêm method vào interface, làm cho interface ngày càng phình to ra. Khi thêm method vào interface  IAnimal, những class cũ như DogCat đều phải implement những method mới nên mất thời gian. Giải pháp trong tình huống này là tách  interface IAnimal ra thành các interface nhỏ như sau:
public interface IAnimal {
  void Eat();
  void Drink();
  void Sleep();
}
public interface IBird {
  void Fly();
}
public interface IFish {
  void Swim();
}

// Các class chỉ cần kế thừa những interface có chúc năng chúng cần
public class Dog : IAnimal {
  public void Eat() {}
  public void Drink() {}
  public void Sleep() {}
}
public class FlappyBird: IAnimal, IBird {
  public void Eat() {}
  public void Drink() {}
  public void Sleep() {}
  public void Fly() {}
}
public class FormosaFish: IAnimal, IBird {
  public void Eat() {}
  public void Drink() {}
  public void Sleep() {}
  public void Swim() {}
}
Để có thể phân tách một interface lớn thành các interface nhỏ một cách hợp lý, các bạn nên xem lại bài viết đầu tiên của series về Single Responsibility Principle. Tuy nhiên, đôi khi việc tách ra nhiều interface có thể làm tăng số lượng interface, tăng số lượng class, ta cần cân nhắc lợi hại trước khi áp dụng nhé.

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

Áp dụng nguyên lý ISP sẽ làm hệ thống linh hoạt hơn, đồng thời giảm thiểu code thừa (do phải implement những tính năng không cần thiết). Tuy nhiên, trong thực tế vẫn có nhiều trường hợp bất khả kháng mà chúng ta phải tạo 1 interface lớn. Ví dụ: Trong một ứng dụng ASP.NET, nếu muốn implement chức năng đăng nhập/phân quyền cho user, ta phải implement iinterface MembershipProvider. Interface này khá là bự với hơn 27 method (Giờ có khi còn nhiều hơn).
public override bool ChangePassword(string username, string oldPassword, string newPassword)
public override bool DeleteUser(string username, bool deleteAllRelatedData)
public override bool EnablePasswordRetrieval()
public override int MinRequiredPasswordLength()

// Thực sự ta chỉ cần hàm này
public override bool ValidateUser(string username, string password)
Có lẽ mục đích của Microsoft khi design interface này là để cover toàn bộ các chức năng cần có của việc đăng nhập/phân quyền. Tuy nhiên, do interface này lớn, nếu ta chỉ muốn làm phân quyền đơn giản thì việc implement 27 method này là hoàn toàn mất thời gian và không cần thiết.
3
Ở bài viết sau, mình sẽ giới thiệu về Dependency Inversion Principle – nguyên lý cuối cùng trong SOLID.
Phạm Huy Hoàng
(toidicodedao)

Series SOLID cho thanh niên code cứng: Liskov Substitution Principle (P3)

Giới thiệu

Đây là đây là bài viết thứ 3 trong series “SOLID cho thanh niên code cứng”. Ở bài viết này, mình sẽ nói về Liskov Substitution Principle – Nguyên lý Thay Thế Lít Kốp (LSP).
  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ý:
Trong một chương trình, các object của class con có thể thay thế class cha mà không
làm thay đổi tính đúng đắn của chương trình

Giải thích nguyên lý

Xin cảnh báo trước một chút là nguyên lý này hơi trừu tượng và khó hiểu (các bác developer nước ngoài cũng tranh cãi khá nhiều về nó), do đó mình sẽ cố gắng giải thích một cách đơn giản nhất có thể. Nếu đọc lần đầu không hiểu, các bạn cố gắng đọc kĩ lại vài lần nhé, nếu vẫn không hiểu thì… chịu khó google tìm bài khác vậy.
Để giữ tính đúng đắn của chương trình, class con phải thay thế được class cha. Nói dễ hiểu là thế này: Ngày xửa ngày xưa, hẳn bạn nào cũng có 1 cái Individual Portable Brick Game, tên tiếng Việt là máy chơi game xếp hình “thần thánh”. Thuở ấy, mỗi lần máy hết pin, mình lại xin mấy nghìn ra ngoài hàng mua pin con ó gắn vào chơi tiếp. Một lần nọ, hết pin mà không có tiền, mình đi lượm mấy cục pin con heo gắn vào chơi tạm. Không ngờ gắn pin vào xong, vừa hí hửng bật máy lên thì máy bị cháy vì điện thế của pin hơn bình thường. Thế là đi tong luôn cái máy chỉ vì… tiếc tiền mua pin.

Theo lý thuyết, class PinConHeo là con của class Pin, khi ta dùng PinConHeo gắn vào làm Pin thì máy phải chạy bình thường. Tuy nhiên trong trường hợp của mình, class Pin Con Heo đã vi phạm LSP vì đã gây lỗi khi dùng thay cho class Pin (Sau này, mình gặp một trải nghiệm tương tự với class PhimConH.., nhưng mà thôi để lần khác kể vậy).

Ví dụ minh họa

Mình sẽ đưa ra 2 ví dụ thường gặp về việc vi phạm LSP:

Ví dụ thứ nhất, class con quăng exception khi gọi hàm

Giả sử, ta muốn viết một chương trình để mô tả các loài chim bay. Đại bàng, chim sẻ, vịt bay được, nhưng chim cánh cụt không bay được. Do chim cánh cụt cũng là chim, ta cho nó kế thừa class Bird. Tuy nhiên, vì cánh cụt không biết bay, khi gọi hàm bay của chim cánh cụt, ta sẽ quăng NoFlyException.
public class Bird {
  public virtual void Fly() { Console.Write("Fly"); }
}
public class Eagle : Bird {
  public override void Fly() { Console.Write("Eagle Fly"); }
}
public class Duck : Bird {
  public override void Fly() { Console.Write("Duck Fly"); }
}
public class Penguin : Bird {
  public override void Fly() { throw new NoFlyException(); }
}

var birds = new List { new Bird(), new Eagle(), new Duck(), new Penguin() };
foreach(var bird in birds) bird.Fly(); 
// Tới pengiun thì lỗi vì cánh cụt quăng Exception
Ta tạo 1 mảng chứa các loài chim rồi duyệt các phần tử. Khi gọi hàm Flycủa class Penguin, hàm này sẽ quăng lỗi. Class Penguin gây lỗi khi chạy, không thay thế được class cha của nó là Bird, do đó nó đã vi phạm LSP.

Ví dụ thứ 2, class con thay đổi hành vi class cha

Đây là ví dụ kinh điển về hình vuông và hình chữ nhật mà mọi người thường dùng để giải thích LSP, mình chỉ viết và giải thích lại đôi chút.
Đầu tiên, hãy cùng đọc đoạn code dưới đây. Ta có 2 class cho hình vuông và hình chữ nhật. Ai cũng biết hình vuông là hình chữ nhật có 2 cạnh bằng nhau, do đó ta có thể cho class Square kế thừa classRectangle để tái sử dụng code.
public class Rectangle
{
    public int Height { get; set; }
    public int Width { get; set; }
    
    public virtual void SetHeight(int height)
    {
        this.Height = height;
    }
    public virtual void SetWidth(int width)
    {
        this.Width = width;
    }
    public virtual int CalculateArea()
    {
        return this.Height * this.Width;
    }
}

public class Square : Rectangle
{
    public override void SetHeight(int height)
    {
        this.Height = height;
        this.Width = height;
    }
    
    public override void SetWidth(int width)
    {
        this.Height = width;
        this.Width = width;
    }
}
Do hình vuông có 2 cạnh bằng nhau, mỗi khi set độ dài 1 cạnh thì ta set luôn độ dài của cạnh còn lại. Tuy nhiên, khi chạy thử, hành động này đã thay đổi hành vi của của class Rectangle, dẫn đến vi phạm LSP.
// code from http://prasadhonrao.com/solid-principles-liskov-substitution-principle-lsp/
Rectangle rect = new Rectangle();
rect.SetHeight(10);
rect.SetWidth(5);
System.Console.WriteLine(rect.CalculateArea()); // Kết quả là 5 * 10

// Below instantiation can be returned by some factory method
Rectangle rect1 = new Square(); 
rect1.SetHeight(10);
rect1.SetWidth(5);
System.Console.WriteLine(rect1.CalculateArea()); 
// Kết quả là 5 x 5. Nếu đúng phải là 10x5, vì diện tích 1 hình chữ nhật là dài x rộng
// Class Square sửa hành vi của class cha Rectangle, set cả dài và rộng về 5 
Trong trường hợp này, để code không vi phạm LSP, ta phải tạo 1 class cha là class Shape, sau đó cho Square và Rectangle kế thừa class Shapenày.

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

Đây là nguyên lý… dễ bị vi phạm nhất, nguyên nhân chủ yếu là do sự thiếu kinh nghiệm khi thiết kế class. Thông thường, design các class dựa theo đời thật: hình vuông là hình chữ nhật, chim cánh cụt là chim. Tuy nhiên, không thể bê nguyên văn mối quan hệ này vào code. Hãy nhớ 1 điều:
Trong đời sống, A là B (hình vuông là hình chữ nhật, chim cánh cụt là chim) không có nghĩa là class A nên kế thừa class B. Chỉ cho class A kế thừa class B khi class A thay thế được cho class B.
Pin con heo là pin nhưng không thay thế được cho pin, chim cánh cụt là chim nhưng không thay thế được cho chim, do đó 2 ví dụ này vi phạm LSP.
Nguyên lý này ẩn giấu trong hầu hết mọi đoạn code, giúp cho code linh hoạt và ổn định mà ta không hề hay biết. Ví dụ như trong C#, ta có thể chạy hàm foreach với List, ArrayList, LinkedList bởi vì chúng cùng kế thừa interface IEnumerable.  Các class List, ArrayList, .. đã được thiết kế đúng LSP, chúng có thể thay thế cho IEnumerable mà không làm hỏng tính đúng đắn của chương trình.
Một số tài liệu để tham khảo thêm:

Series solid cho thanh niên code cứng: OPEN/CLOSED PRINCIPLE (P2)

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.

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 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 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ụngionicFramework, 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 =))).

SOLID cho thanh niên code CỨNG: Single Responsibility Principle

Cách đây khá lâu, mình đã có một bài viết tổng quát về SOLID Principle, những nguyên lý thiết kế OOP. Nhắc lại một chút cho các bạn đã quên.
Đây là những nguyên lý được đúc kết bởi máu xương vô số developer, rút ra từ hàng ngàn dự án thành công và thất bại. Một project áp dụng những nguyên lý này sẽ có code dễ đọc, dễ test, rõ ràng hơn. Và việc quan trọng nhất là việc maintainace code sẽ dễ hơn rất nhiều.
Nắm vững những nguyên lý này, đồng thời áp dụng chúng trong việc thiết kế + viết code sẽ giúp bạn tiến thêm 1 bước trên con đường thành senior nhé (1 ông senior bên FPT Software từng bảo mình thế).
SOLID bao gồm 5 nguyên lý dưới đây:
  1. Single Responsibility Principle
  2. Open/Closed Principle
  3. Liskov Substitution Principle
  4. Interface Segregation Principle
  5. Dependency Inversion Principle
Giới thiệu
Đây là bài viết đầu tiên trong series “SOLID cho thanh niên cứng”. Các nguyên lý SOLID này khá hữu ích, nhưng mình không thấy được dạy ở các trường. Ở mỗi bài viết, mình sẽ phân tích rõ hơn về các nguyên lý này, kèm theo code minh họa. Hi vọng chúng sẽ giúp các bạn hiểu rõ hơn và áp dụng nguyên lý này vào code.
Ở bài viết đầu tiên, mình sẽ nói về Single Responsibility Principle – Nguyên lý Đơn Trách Nhiệm. Nội dung nguyên lý:
Một class chỉ nên giữ một trách nhiệm duy nhất 
(Chỉ có thể thay đổi class vì một lý do duy nhất)
Giải thích nguyên lý
Ta có thể tạm hiểu “trách nhiệm” ở đây tương đương với “chức năng”. Tại sao một class chỉ nên giữ một chức năng duy nhất?? Để hiểu điều này, hãy nhìn vào hình dưới.

Hãy xem con dao trong hình như một class với rất nhiều chức năng. Con dao này “có vẻ” khá là tiện dụng, nhưng lại cồng kềnh và nặng nề. Đặc biệt, khi có một bộ phận bị hư hỏng, ta phải tháo nguyên con dao ra để sửa. Việc sửa chữa và cải tiến rất phức tạp, có thể ảnh hưởng tới nhiều bộ phận khác nhau.
Một class có quá nhiều chức năng cũng sẽ trở nên cồng kềnh và phức tạp. Trong ngành IT, requirement rất hay thay đổi, dẫn tới sự thay đổi code. Nếu một class có quá nhiều chức năng, quá cồng kềnh, việc thay đổi code sẽ rất khó khăn, mất nhiều thời gian, còn dễ gây ảnh hưởng tới các module đang hoạt động khác.
Áp dụng SRP vào con dao phía trên, ta có thể tách nó ra làm kéo, dao, mở nút chai,… riêng biệt là xong, cái gì hư chỉ cần sửa cái đấy. Với code cũng vậy, ta chỉ cần thiết kế các module sao cho đơn giản, một module chỉ có 1 chức năng duy nhất là xong (Nói vậy chứ việc xác định, gom nhóm chức năng không hề dễ đâu nhé).

Ví dụ minh họa
Đoạn code dưới đây là ví dụ cho việc vi phạm SRP. Lỗi này hồi mới học code mình cũng hay mắc phải. Class Student có quá nhiều chức năng: chứa thông tin học sinh, format hiển thị thông tin, lưu trữ thông tin.
public class Student {
  public string Name { get; set;}
  public int Age { get; set;}
  
  // Format class này dưới dạng text, html, json để in ra
  public string GetStudentInfoText() {
    return "Name: " + Name + ". Age: " + Age;
  }
  public string GetStudentInfoHTML() {
    return "<span>" + Name + " " + Age + "</span>";
  }
  public string GetStudentInfoJson() {
    return Json.Serialize(this);
  }
  // Lưu trữ xuống database, xuống file
  public void SaveToDatabase() {
    dbContext.Save(this);
  }
  public void SaveToFile() {
    Files.Save(this, "fileName.txt");
  }
}
Code như thế thì có làm sao không? Hiện tại thì không sao cả, nhưng khi code lớn dần, thêm chức năng nhiều hơn, class Student sẽ bị phình to ra. Chưa kể, nếu như có thêm các class khác như Person, Teacher v…v, đoạn code hiển thị/lưu trữ thông tin sẽ nằm rải rác ở nhiều class, rất khó sửa chữa và nâng cấp.
Để giải quyết, ta chỉ cần tách ra làm nhiều class, mỗi class có một chức năng riêng là xong. Khi cần nâng cấp, sửa chữa, sẽ diễn ra ở từng class, không ảnh hưởng tới các class còn lại.
// Student bây giờ chỉ chứa thông tin
public class Student {
  public string Name { get; set;}
  public int Age { get; set;}
}

// Class này chỉ format thông tin hiển thị student
public class Formatter {
  public string FormatStudentText(Student std) {
    return "Name: " + std.Name + ". Age: " + std.Age;
  }
  public string FormatStudentHtml(Student std) {
    return "<span>" + std.Name + " " + std.Age + "</span>";
  }
  public string FormatStudentJson(Student std) {
    return Json.Serialize(std);
  }
}

// Class này chỉ lo việc lưu trữ
public class Store {
  public void SaveToDatabase(Student std) {
    dbContext.Save(std);
  }
  public void SaveToFile(Student std) {
    Files.Save(std, "fileName.txt");
  }
}
Lưu ý: Không phải lúc nào cũng nên áp dụng nguyên lý này vào code. Một trường hợp hay gặp là các class dạng Helper hay Utilities – các class này vi phạm SRP 1 cách trắng trợn. Nếu số lượng hàm ít, ta vẫn có thể cho tất cả các hàm này vào 1 class, xét cho cùng, toàn bộ các hàm trong helper đều có chức năng xử lý các tác vụ nho nhỏ.
// Class Helper vi phạm SRP
// Nhưng vì nhỏ, ta có thể bỏ qua
public class Helper {
  public string getUser();
  public DateTime getTime();
  public string getCurrentLocation();
  public DbConnection getDatabaseConnection();
}
Tuy nhiên, khi Helper có thêm nhiều chức năng, nó trở nên phức tạp và cồng kềnh hơn (Bạn mình từng gặp trường hợp một class có tới gần 10000 dòng code). Lúc này, ta cần áp dụng SRP để chia nó thành các module nho nhỏ để dễ quản lý.
// Helper đã bự, ta cần tách
public class Helper {
  public string getUser();
  //.....
  public DateTime getTime();
  //.....
  public string getCurrentLocation();
  //.....
  public DbConnection getDatabaseConnection();
}

// Tách helper thành các helper nhỏ hơn
public class UserHelper {
}
public class TimeLocationHelper {
}
public class DatabaseHelper {
}
Lưu ý và kết luận
Về bản chất, nguyên lý chỉ là nguyên lý, nó chỉ là hướng dẫn chứ không phải là quy tắc tuyệt đối bất di bất dịch. Nếu tìm hiểu kĩ, các bạn sẽ thấy vẫn có vài lập trình viên mổ xẻ, phản đối, chỉ ra những chỗ chưa ổn của các nguyên lý này. Tuy vậy, việc hiểu rõ chúng vẫn giúp code ta viết ra dễ đọc, dễ hiểu, dễ quản lý hơn.
SRP là nguyên lý đơn giản dễ hiểu nhất, nhưng cũng khó áp dụng đúng nhất. Sự khác nhau giữa dev giỏi và dev bình thường là ở chỗ, cả 2 cùng biết về các qui tắc và nguyên lý, nhưng dev giỏi sẽ biết khi nào cần áp dụng, khi nào không.
(toidicodedao)

The Ultimate XP Project

  (Bài chia sẻ của tác giả  Ryo Amano ) Trong  bài viết  số này, tôi muốn viết về dự án phát triển phần mềm có áp dụng nguyên tắc phát triển...