Quay lại

5 Nguyên Tắc Thiết Kế Phần Mềm Trong SOLID Chuyên mục PHP và Laravel    2024-05-16    137 Lượt xem    133 Lượt thích    comment-3 Created with Sketch Beta. 0 Bình luận

5 Nguyên Tắc Thiết Kế Phần Mềm Trong SOLID

SOLID

SOLID là một viết tắt cho năm nguyên tắc thiết kế phần mềm quan trọng trong lập trình hướng đối tượng. Các nguyên tắc này được đưa ra để tạo ra mã nguồn dễ đọc, dễ bảo trì và mở rộng. Dưới đây là mô tả của mỗi nguyên tắc:

  1. S - Single Responsibility Principle (Nguyên tắc đơn trách nhiệm): Điều này có nghĩa là mỗi Class nên chỉ đảm nhận một trách nhiệm cụ thể và không nên kết hợp nhiều chức năng không liên quan với nhau trong cùng một Class.

  2. O - Open/Closed Principle (Nguyên tắc mở đóng): Một phần mềm cần phải mở rộng được, nhưng không nên thay đổi các thành phần hiện có để mở rộng. Thay vào đó, phải sử dụng kế thừa hoặc interface để mở rộng chức năng.

  3. L - Liskov Substitution Principle (Nguyên tắc thay thế Liskov): Các đối tượng của một Class con có thể thay thế cho các đối tượng của Class cha mà không làm thay đổi tính đúng đắn của chương trình.

  4. I - Interface Segregation Principle (Nguyên tắc phân chia interface): Người dùng không nên bị ép buộc phải phụ thuộc vào các interface mà họ không sử dụng. Thay vào đó, nên có nhiều interface nhỏ hơn, chuyên biệt cho các công việc cụ thể.

  5. D - Dependency Inversion Principle (Nguyên tắc đảo ngược phụ thuộc): Các module cấp cao không nên phụ thuộc vào các module cấp thấp. Thay vào đó, cả hai nên phụ thuộc vào các trừu tượng. Điều này giúp giảm thiểu sự phụ thuộc giữa các thành phần của mã nguồn, tăng tính linh hoạt và dễ bảo trì.

Single Responsibility Principle

Một class chỉ nên giữ 1 trách nhiệm duy nhất (Chỉ có thể sửa đổi class với 1 lý do duy nhất)

Nguyên tắc Đơn định (Single Responsibility Principle - SRP) là một trong năm nguyên tắc thiết kế phần mềm trong SOLID. Nó khuyến khích đến việc ràng buộc mỗi Class chỉ nên có một trách nhiệm duy nhất. Điều này có nghĩa là mỗi Class nên chỉ thực hiện một nhiệm vụ hoặc chức năng cụ thể và không nên kết hợp nhiều chức năng không liên quan với nhau trong cùng một Class.

Ví dụ:

Giả sử bạn đang phát triển một ứng dụng web quản lý nhiệm vụ (task management application). Trong ứng dụng của bạn, có một Class Task để đại diện cho mỗi nhiệm vụ.

Đầu tiên, hãy viết một phiên bản của Class Task mà không tuân theo nguyên tắc SRP:

class Task {
    private $description;
    private $dueDate;
    private $assignedTo;

    public function __construct($description, $dueDate, $assignedTo) {
        $this->description = $description;
        $this->dueDate = $dueDate;
        $this->assignedTo = $assignedTo;
    }

    public function getDescription() {
        return $this->description;
    }

    public function getDueDate() {
        return $this->dueDate;
    }

    public function getAssignedTo() {
        return $this->assignedTo;
    }

    public function notifyAssignedUser() {
        // Gửi email thông báo cho người được giao nhiệm vụ
    }

    public function saveToDatabase() {
        // Lưu thông tin của nhiệm vụ vào cơ sở dữ liệu
    }
}​

Trong phiên bản này, Class Task không tuân thủ nguyên tắc SRP vì nó chứa logic không liên quan đến quản lý thông tin nhiệm vụ, bao gồm việc gửi email noti cho người được giao nhiệm vụ(Assigned User) và lưu thông tin task vào cơ sở dữ liệu.

Bây giờ, hãy sử dụng nguyên tắc SRP để chia nhỏ Class Task thành các Class riêng biệt với trách nhiệm duy nhất:

class Task {
    private $description;
    private $dueDate;
    private $assignedTo;

    public function __construct($description, $dueDate, $assignedTo) {
        $this->description = $description;
        $this->dueDate = $dueDate;
        $this->assignedTo = $assignedTo;
    }

    public function getDescription() {
        return $this->description;
    }

    public function getDueDate() {
        return $this->dueDate;
    }

    public function getAssignedTo() {
        return $this->assignedTo;
    }
}

class EmailNotifier {
    public function sendNotification($recipient, $message) {
        // Gửi email thông báo
    }
}

class TaskRepository {
    public function saveTask($task) {
        // Lưu thông tin của nhiệm vụ vào cơ sở dữ liệu
    }
}​

Trong phiên bản này, các trách nhiệm đã được phân chia rõ ràng: Class Task chỉ quản lý thông tin của task, Class EmailNotifier chỉ quản lý việc gửi email thông báo, và Class TaskRepository chỉ quản lý việc lưu thông tin nhiệm vụ vào cơ sở dữ liệu. Điều này giúp cho mã nguồn trở nên dễ bảo trì và mở rộng hơn, và nó tuân thủ nguyên tắc SRP.

SRP được sử dụng khi:

  1. Thiết kế hoặc triển khai một Class, module hoặc hàm mới.
  2. Cần phân chia công việc giữa các thành viên trong nhóm phát triển.
  3. Muốn đảm bảo rằng mã nguồn dễ bảo trì và mở rộng trong tương lai.

Open/Closed Principle

Có thể thoải mái mở rộng 1 class, nhưng không được sửa đổi bên trong class đó (open for extension but closed for modification)

Nguyên tắc mở đóng (Open/Closed Principle - OCP) trong SOLID là một nguyên tắc thiết kế phần mềm quan trọng. Nó khuyến khích việc thiết kế phần mềm sao cho các thành phần có thể được mở rộng (extended) để thêm các chức năng mới mà không cần phải sửa đổi mã nguồn gốc.

Để hiểu rõ hơn, hãy xem một ví dụ với ngôn ngữ lập trình PHP:

Giả sử bạn đang phát triển một ứng dụng thương mại điện tử và bạn có một Class Product để đại diện cho các sản phẩm được bán trên trang web của bạn. Ban đầu, bạn có các sản phẩm như electronicbook.

Dưới đây là một phiên bản của Class ProductKHÔNG tuân theo nguyên tắc OCP:

class Product {
    private $name;
    private $price;
    private $type;

    public function __construct($name, $price, $type) {
        $this->name = $name;
        $this->price = $price;
        $this->type = $type;
    }

    public function getPrice() {
        return $this->price;
    }

    public function getType() {
        return $this->type;
    }

    public function calculateDiscount() {
        if ($this->type === 'electronic') {
            return $this->price * 0.1; // Giảm giá 10% cho sản phẩm điện tử
        } elseif ($this->type === 'book') {
            return $this->price * 0.05; // Giảm giá 5% cho sách
        }
    }
}​

Đoạn code trên hoạt động tốt và tính năng của nó cũng không sai. Tuy nhiên, nếu bạn muốn thêm một loại sản phẩm mới, như clothing, bạn sẽ phải sửa đổi lớp Product bằng cách thêm một điều kiện mới vào phương thức calculateDiscount(). Điều này làm việc thêm tính năng mới không tuân thủ nguyên tắc OCP.

Bây giờ, hãy sử dụng nguyên tắc OCP để tạo ra một thiết kế tốt hơn, chúng ta sẽ tao ra 1 class Product mới để mở rộng cho tính năng như sau:

interface DiscountCalculator {
    public function calculateDiscount($price);
}

class ElectronicDiscount implements DiscountCalculator {
    public function calculateDiscount($price) {
        return $price * 0.1; // Giảm giá 10% cho sản phẩm điện tử
    }
}

class BookDiscount implements DiscountCalculator {
    public function calculateDiscount($price) {
        return $price * 0.05; // Giảm giá 5% cho sách
    }
}

class ClothingDiscount implements DiscountCalculator {
    public function calculateDiscount($price) {
        return $price * 0.2; // Giảm giá 20% cho quần áo
    }
}

class Product {
    private $name;
    private $price;
    private $discountCalculator;

    public function __construct($name, $price, DiscountCalculator $discountCalculator) {
        $this->name = $name;
        $this->price = $price;
        $this->discountCalculator = $discountCalculator;
    }

    public function getPrice() {
        return $this->price;
    }

    public function calculateDiscount() {
        return $this->discountCalculator->calculateDiscount($this->price);
    }
}​

$product = new Product('clothing', 200, new ClothingDiscount);
$product->calculateDiscount();

Trong thiết kế này, chúng ta sử dụng một interface DiscountCalculator và triển khai nó trong các Class ElectronicDiscount, BookDiscountClothingDiscount để tính toán giảm giá cho từng loại sản phẩm. Class Product không cần biết cụ thể về cách tính toán giảm giá cho từng loại sản phẩm mà chỉ cần giao tiếp thông qua interface DiscountCalculator, điều này làm cho mã nguồn dễ mở rộng và bảo trì hơn.

Có thể thấy rằng, cách thiết kế này làm cho Class Product ban đầu của chúng ta trở nên: ĐÓNG với mọi sự thay đổi bên trong class Product, nhưng luôn MỞ cho sự kế thừa để mở rộng sau này. Trong tương lai, khi nhu cầu mở rộng chương trình xuất hiện, có thêm nhiều đối tượng nữa cần xử lí thì chúng ta chỉ cần thêm Class mới là sẽ giải quyết được vấn đề, trong khi vẫn đảm bảo được chương trình có sẵn không bị ảnh hưởng, nhờ đó mà hạn chế được phạm vi test, giúp giảm chi phí phát triển. Đó cũng là một trong những lợi ích ở khía cạnh dễ bảo trì sản phẩm.

Như vậy, trong thực tế, khi bạn muốn mở rộng chức năng của một class mà không vi phạm nguyên tắc OCP, thường bạn sẽ tạo mới một class mới thay vì chỉnh sửa class cũ. Điều này giúp giữ cho mã nguồn dễ bảo trì và mở rộng hơn.

Cụ thể, OCP được sử dụng khi:

  1. Bạn muốn dễ dàng mở rộng chức năng của một phần của hệ thống mà không ảnh hưởng đến các phần khác.
  2. Bạn muốn giảm thiểu việc phải sửa đổi mã nguồn hiện có khi thêm mới chức năng.
  3. Bạn muốn tăng tính linh hoạt và dễ bảo trì của mã nguồn bằng cách tạo ra các thành phần có thể tái sử dụng và mở rộng được.

Liskov Substitution Principle

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

Nguyên tắc Thay thế Liskov (Liskov Substitution Principle - LSP) là một trong năm nguyên tắc SOLID trong lập trình hướng đối tượng, được phát biểu bởi Barbara Liskov. Nguyên tắc này nói rằng các đối tượng của một Class con thể thay thế cho các đối tượng của Class cha mà không làm thay đổi tính đúng đắn của chương trình.

Hiểu một cách đơn giản, nếu bạn có một Class T và một Class con S của T, thì bạn có thể sử dụng các đối tượng của Class S thay thế cho các đối tượng của Class T mà không làm thay đổi bất kỳ hành vi nào của chương trình. Điều này đảm bảo rằng Class con S thực hiện mọi hành vi của Class cha T một cách chính xác và nhất quán.

Hãy tưởng tượng bạn có 1 class cha tên Mèo. Các class MèoDen, MèoVàng có thể kế thừa class này, chương trình chạy bình thường. Tuy nhiên nếu ta viết class MèoChạyPin, cần pin mới chạy được. Khi class này kế thừa class Mèo, vì không có pin không chạy được, sẽ gây lỗi. Đó là 1 trường hợp vi phạm nguyên lý này.

Hãy xem xét một ví dụ minh họa về nguyên tắc Liskov Substitution Principle (LSP) trong bối cảnh của các loại Mèo. Trong ví dụ này, chúng ta có một lớp cha là Meo (Mèo) và các lớp con như MeoDen (Mèo Đen), MeoVang (Mèo Vàng), và MeoChayPin (Mèo chạy pin).

Vi phạm LSP

Trước tiên, chúng ta sẽ xem một trường hợp vi phạm LSP, nơi MeoChayPin kế thừa từ Meo nhưng lại cần pin mới hoạt động, điều này gây ra lỗi khi thay thế Meo bằng MeoChayPin.

class Meo {
    // mèo kêu 
    public function miaow() {
        echo "meo meo! meo!\n";
    }

    public function run() {
        echo "I am running!\n";
    }
}

class MeoDen extends Meo {
    // MeoDen kế thừa hoàn toàn từ Meo
}

class MeoVang extends Meo {
    // MeoVang kế thừa hoàn toàn từ Meo
}

class MeoChayPin extends Meo {
    private $hasBattery;

    public function __construct($hasBattery) {
        $this->hasBattery = $hasBattery;
    }

    public function miaow() {
        if (!$this->hasBattery) {
            throw new Exception("MeoChayPin needs battery to meo meo!");
        }
        echo "Electronic meo meo! meo meo!\n";
    }

    public function run() {
        if (!$this->hasBattery) {
            throw new Exception("MeoChayPin needs battery to run!");
        }
        echo "I am running with battery power!\n";
    }
}

function makeMeomiaow(Meo $meo) {
    $meo->miaow();
}

$meoDen = new MeoDen();
makeMeomiaow($meoDen); // Outputs: meo! meo!

$meoVang = new MeoVang();
makeMeomiaow($meoVang); // Outputs: meo! meo!

$meoChayPin = new MeoChayPin(false);
makeMeomiaow($meoChayPin); // Throws exception: MeoChayPin needs battery to meo meo!​

Trong ví dụ này, MeoChayPin không thể thay thế Meo mà không gây ra lỗi vì nó cần pin để hoạt động.

Tuân thủ LSP

Để tuân thủ nguyên tắc LSP, chúng ta có thể tách riêng hành vi của MeoChayPin bằng cách sử dụng giao diện (interface) hoặc một abstract class.

interface Miaowable {
    public function miaow();
}

interface Runable {
    public function run();
}

class Meo implements Miaowable, Runable {
    public function miaow() {
        echo "meo meo! meo!\n";
    }

    public function run() {
        echo "I am running!\n";
    }
}

class MeoDen extends Meo {
    // MeoDen kế thừa hoàn toàn từ Meo
}

class MeoVang extends Meo {
    // MeoVang kế thừa hoàn toàn từ Meo
}

class ElectronicMeo implements Miaowable, Runable {
    private $hasBattery;

    public function __construct($hasBattery) {
        $this->hasBattery = $hasBattery;
    }

    public function miaow() {
        if (!$this->hasBattery) {
            throw new Exception("ElectronicMeo needs battery to meo meo!");
        }
        echo "Electronic Miaow! Miaow!\n";
    }

    public function run() {
        if (!$this->hasBattery) {
            throw new Exception("ElectronicMeo needs battery to run!");
        }
        echo "I am running with battery power!\n";
    }
}

function makeMeoMiaow(Miaowable $meo) {
    $meo->miaow();
}

$meoDen = new MeoDen();
makeMeoMiaow($meoDen); // Outputs: meo! meo!

$meoVang = new MeoVang();
makeMeoMiaow($meoVang); // Outputs: meo! meo!

$meoChayPin = new ElectronicMeo(true);
makeMeoMiaow($meoChayPin); // Outputs: Electronic meo! meo!

Giải thích

Trong ví dụ này:

  • Chúng ta sử dụng hai interface MiaowableRunable để định nghĩa các hành vi có thể miaowrun.
  • Lớp Meo và các lớp con của nó (MeoDen, MeoVang) triển khai các interface này mà không thay đổi hành vi cơ bản.
  • Lớp ElectronicMeo cũng triển khai các interface này nhưng cần kiểm tra xem có pin hay không trước khi thực hiện hành vi.

Khi nào sử dụng LSP?

Nguyên tắc LSP nên được sử dụng bất cứ khi nào bạn:

  1. Kế thừa lớp: Khi bạn thiết kế một lớp con từ một lớp cha, đảm bảo rằng lớp con có thể thay thế cho lớp cha mà không làm thay đổi hành vi của chương trình.
  2. Xây dựng hệ thống mở rộng: Khi bạn muốn mở rộng hệ thống mà không làm thay đổi hoặc gây lỗi cho các phần hiện tại của hệ thống.
  3. Bảo trì và nâng cấp hệ thống: Khi bạn muốn giữ cho hệ thống dễ bảo trì và nâng cấp bằng cách đảm bảo các thành phần có thể thay thế cho nhau một cách an toàn.

Bằng cách tuân thủ LSP, bạn đảm bảo rằng các thành phần trong hệ thống của bạn có thể được mở rộng và thay thế một cách an toàn mà không làm thay đổi hành vi mong đợi của chương trình.

Interface Segregation Principle

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ể

Interface Segregation Principle (ISP) là nguyên tắc thứ tư trong năm nguyên tắc SOLID. Nguyên tắc này nói rằng không nên ép buộc một class phải triển khai các interface mà nó không sử dụng. Thay vào đó, nên chia các interface lớn thành các interface nhỏ và cụ thể hơn để các class chỉ cần triển khai những interface cần thiết.

Giải thích:

  • Interface lớn: Một interface chứa nhiều phương thức mà các class triển khai có thể không sử dụng hết. Điều này làm cho các class triển khai bị ràng buộc bởi những phương thức không cần thiết, gây khó khăn cho việc bảo trì và mở rộng.
  • Interface nhỏ: Một interface chứa ít phương thức hơn và chỉ tập trung vào một nhóm chức năng cụ thể. Các class sẽ chỉ triển khai những phương thức mà chúng thực sự cần.

Ví dụ:

Giả sử chúng ta có một ứng dụng quản lý động vật trong sở thú. Ban đầu, chúng ta có một interface lớn như sau:

interface Animal {
    public function eat();
    public function sleep();
    public function fly();
    public function swim();
}​

Class Dog và Bird triển khai interface này:

class Dog implements Animal {
    public function eat() {
        // Code for dog eating
    }

    public function sleep() {
        // Code for dog sleeping
    }

    public function fly() {
        // Dogs can't fly, but still have to implement this method
    }

    public function swim() {
        // Code for dog swimming
    }
}

class Bird implements Animal {
    public function eat() {
        // Code for bird eating
    }

    public function sleep() {
        // Code for bird sleeping
    }

    public function fly() {
        // Code for bird flying
    }

    public function swim() {
        // Birds can't swim, but still have to implement this method
    }
}​

Như bạn thấy, Dog phải triển khai phương thức fly mà nó không sử dụng, và Bird phải triển khai phương thức swim mà nó không sử dụng. Đây là vi phạm nguyên tắc ISP.

Áp dụng Interface Segregation Principle:

Chúng ta sẽ chia interface Animal thành các interface nhỏ hơn:

interface Eater {
    public function eat();
}

interface Sleeper {
    public function sleep();
}

interface Flyer {
    public function fly();
}

interface Swimmer {
    public function swim();
}​

Bây giờ, chúng ta chỉ cần triển khai những interface cần thiết cho từng class:

class Dog implements Eater, Sleeper, Swimmer {
    public function eat() {
        // Code for dog eating
    }

    public function sleep() {
        // Code for dog sleeping
    }

    public function swim() {
        // Code for dog swimming
    }
}

class Bird implements Eater, Sleeper, Flyer {
    public function eat() {
        // Code for bird eating
    }

    public function sleep() {
        // Code for bird sleeping
    }

    public function fly() {
        // Code for bird flying
    }
}​

Khi nào sử dụng Interface Segregation Principle:

  1. Khi một interface có quá nhiều phương thức: Điều này có thể làm cho các class triển khai phải thực hiện những phương thức không cần thiết.
  2. Khi có nhiều class triển khai cùng một interface nhưng không sử dụng hết các phương thức của interface đó: Chia nhỏ interface giúp các class chỉ cần triển khai những phương thức cần thiết.
  3. Khi muốn tăng tính bảo trì và mở rộng của hệ thống: Bằng cách chia nhỏ các interface, bạn có thể dễ dàng thay đổi hoặc mở rộng một phần của hệ thống mà không ảnh hưởng đến phần còn lại.

Dependency Inversion Principle

Các module cấp cao không nên phụ thuộc vào các modules cấp thấp. Cả 2 nên phụ thuộc vào abstraction.

Interface (abstraction) không nên phụ thuộc vào chi tiết, mà ngược lại. ( Các class giao tiếp với nhau thông qua interface, không phải thông qua implementation.)

Sử dụng nguyên tắc ISP giúp hệ thống của bạn trở nên linh hoạt, dễ bảo trì và dễ mở rộng hơn.

Dependency Inversion Principle (DIP) là nguyên tắc cuối cùng trong năm nguyên tắc SOLID. Nguyên tắc này nói rằng:

  1. Các module cấp cao không nên phụ thuộc vào các module cấp thấp. Cả hai nên phụ thuộc vào các abstraction (interface hoặc abstract class).
  2. Các abstraction không nên phụ thuộc vào chi tiết. Chi tiết nên phụ thuộc vào abstraction.

Giải thích:

  • Module cấp cao: Thường là các phần của hệ thống thực hiện các logic nghiệp vụ chính.
  • Module cấp thấp: Thường là các phần của hệ thống thực hiện các tác vụ chi tiết như truy xuất dữ liệu, xử lý mạng, v.v.
  • Abstraction: Là các interface hoặc abstract class mà các module cấp cao và cấp thấp sẽ phụ thuộc vào.

Tại sao cần DIP?

  • Giảm sự phụ thuộc chặt chẽ: Khi các module cấp cao phụ thuộc trực tiếp vào các module cấp thấp, thay đổi trong module cấp thấp có thể gây ra sự thay đổi trong module cấp cao, làm cho hệ thống khó bảo trì.
  • Tăng tính linh hoạt và khả năng mở rộng: Bằng cách phụ thuộc vào abstraction, bạn có thể dễ dàng thay thế các module cấp thấp mà không cần thay đổi module cấp cao.

Ví dụ:

Giả sử bạn đang phát triển một ứng dụng gửi thông báo. Ban đầu, bạn có một class Notification (module cấp cao) phụ thuộc trực tiếp vào class EmailService (module cấp thấp):

class EmailService {
    public function send($to, $message) {
        // Logic gửi email
        echo "Sending email to $to: $message";
    }
}

class Notification {
    private $emailService;

    public function __construct() {
        $this->emailService = new EmailService();
    }

    public function notify($to, $message) {
        $this->emailService->send($to, $message);
    }
}

// Sử dụng
$notification = new Notification();
$notification->notify('example@example.com', 'Hello, this is a notification!');​

Trong ví dụ này, Notification phụ thuộc trực tiếp vào EmailService. Nếu bạn muốn thay thế EmailService bằng SMSService, bạn sẽ phải thay đổi code trong Notification.

Áp dụng Dependency Inversion Principle:

Đầu tiên, tạo một interface MessageService mà cả EmailService và SMSService sẽ triển khai:

interface MessageService {
    public function send($to, $message);
}

class EmailService implements MessageService {
    public function send($to, $message) {
        // Logic gửi email
        echo "Sending email to $to: $message";
    }
}

class SMSService implements MessageService {
    public function send($to, $message) {
        // Logic gửi SMS
        echo "Sending SMS to $to: $message";
    }
}​

Sau đó, Notification sẽ phụ thuộc vào interface MessageService thay vì phụ thuộc vào EmailService cụ thể:

class Notification {
    private $messageService;

    public function __construct(MessageService $messageService) {
        $this->messageService = $messageService;
    }

    public function notify($to, $message) {
        $this->messageService->send($to, $message);
    }
}

// Sử dụng
$emailService = new EmailService();
$smsService = new SMSService();

$emailNotification = new Notification($emailService);
$emailNotification->notify('example@example.com', 'Hello, this is an email notification!');

$smsNotification = new Notification($smsService);
$smsNotification->notify('123456789', 'Hello, this is an SMS notification!');​

Khi nào sử dụng Dependency Inversion Principle:

  1. Khi bạn muốn tăng tính linh hoạt và khả năng mở rộng của hệ thống: DIP cho phép bạn thay thế hoặc thay đổi các module cấp thấp mà không cần thay đổi các module cấp cao.
  2. Khi bạn muốn giảm sự phụ thuộc giữa các module: DIP giúp giảm sự liên kết chặt chẽ giữa các phần khác nhau của hệ thống, làm cho hệ thống dễ bảo trì hơn.
  3. Khi bạn muốn áp dụng các nguyên tắc lập trình hướng đối tượng tốt hơn: DIP giúp bạn tuân thủ các nguyên tắc như SOLID, làm cho code của bạn sạch hơn và dễ hiểu hơn.

Sử dụng DIP giúp hệ thống của bạn trở nên linh hoạt hơn, dễ bảo trì và mở rộng hơn.

Nguyên tắc SOLID và ưu, nhược điểm

1. Single Responsibility Principle (SRP) - Nguyên tắc trách nhiệm duy nhất

  • Định nghĩa: Một class chỉ nên có một lý do để thay đổi, tức là nó chỉ nên có một trách nhiệm duy nhất.

  • Ưu điểm:

    • Dễ bảo trì: Mã nguồn dễ hiểu và bảo trì hơn vì mỗi class chỉ thực hiện một nhiệm vụ.
    • Tăng tính tái sử dụng: Các class có thể được tái sử dụng trong các ngữ cảnh khác nhau mà không cần thay đổi.
    • Giảm thiểu lỗi: Sự thay đổi trong một phần của hệ thống ít có khả năng gây ra lỗi ở các phần khác.
  • Nhược điểm:

    • Tăng số lượng class: Việc tuân thủ SRP có thể dẫn đến việc tạo ra nhiều class hơn, làm phức tạp cấu trúc hệ thống.
    • Phân chia trách nhiệm không rõ ràng: Đôi khi, việc xác định đúng trách nhiệm duy nhất của một class có thể khó khăn.

2. Open/Closed Principle (OCP) - Nguyên tắc mở/đóng

  • Định nghĩa: Các thực thể phần mềm (class, module, hàm, v.v.) nên mở để mở rộng nhưng đóng để sửa đổi.

  • Ưu điểm:

    • Dễ mở rộng: Bạn có thể thêm chức năng mới mà không làm thay đổi mã nguồn cũ, giảm nguy cơ gây lỗi.
    • Tăng tính linh hoạt: Hệ thống có thể dễ dàng mở rộng theo yêu cầu mới mà không cần phải sửa đổi mã nguồn đã kiểm thử.
  • Nhược điểm:

    • Phức tạp hóa thiết kế: Việc tạo ra các abstraction (interface hoặc abstract class) để tuân thủ OCP có thể làm phức tạp thiết kế.
    • Chi phí ban đầu cao: Việc thiết kế hệ thống để tuân thủ OCP có thể đòi hỏi nhiều công sức và thời gian hơn.

3. Liskov Substitution Principle (LSP) - Nguyên tắc thay thế Liskov

  • Định nghĩa: Các đối tượng trong một chương trình nên có khả năng bị thay thế bằng các đối tượng của các lớp con của chúng mà không làm thay đổi tính đúng đắn của chương trình.

  • Ưu điểm:

    • Tăng tính nhất quán: Giúp đảm bảo rằng các lớp con có thể thay thế cho lớp cha mà không làm thay đổi hành vi của chương trình.
    • Dễ dàng kiểm thử và bảo trì: Giảm thiểu lỗi phát sinh khi thay thế các đối tượng trong hệ thống.
  • Nhược điểm:

    • Khó khăn trong thiết kế lớp kế thừa: Việc thiết kế các lớp con để tuân thủ LSP có thể phức tạp và đòi hỏi sự cẩn thận.
    • Giới hạn trong việc mở rộng lớp con: LSP có thể giới hạn cách bạn mở rộng lớp con để đảm bảo không phá vỡ tính nhất quán.

4. Interface Segregation Principle (ISP) - Nguyên tắc phân chia interface

  • Định nghĩa: Nhiều interface đặc thù cho client sẽ tốt hơn một interface tổng quát. Các class không nên bị buộc phải triển khai các interface mà chúng không sử dụng.

  • Ưu điểm:

    • Giảm sự phụ thuộc không cần thiết: Class chỉ cần triển khai những phương thức mà nó thực sự sử dụng.
    • Tăng tính linh hoạt: Dễ dàng thay đổi và bảo trì các thành phần của hệ thống.
  • Nhược điểm:

    • Tăng số lượng interface: Có thể dẫn đến việc tạo ra nhiều interface nhỏ, làm phức tạp hệ thống.
    • Khó quản lý: Việc quản lý nhiều interface nhỏ có thể trở nên phức tạp.

5. Dependency Inversion Principle (DIP) - Nguyên tắc đảo ngược phụ thuộc

  • Định nghĩa: Các module cấp cao không nên phụ thuộc vào các module cấp thấp. Cả hai nên phụ thuộc vào abstraction. Các abstraction không nên phụ thuộc vào chi tiết. Chi tiết nên phụ thuộc vào abstraction.

  • Ưu điểm:

    • Tăng tính linh hoạt: Dễ dàng thay thế các module cấp thấp mà không ảnh hưởng đến các module cấp cao.
    • Dễ bảo trì và mở rộng: Giảm sự phụ thuộc giữa các thành phần của hệ thống, làm cho hệ thống dễ bảo trì và mở rộng hơn.
  • Nhược điểm:

    • Tăng độ phức tạp: Việc sử dụng các abstraction có thể làm tăng độ phức tạp của thiết kế.
    • Chi phí ban đầu cao: Cần nhiều thời gian và công sức để thiết kế hệ thống theo DIP từ đầu.

Tổng kết

Áp dụng các nguyên tắc SOLID giúp bạn thiết kế các hệ thống phần mềm dễ bảo trì, mở rộng và linh hoạt hơn. Tuy nhiên, cần cẩn trọng để không làm phức tạp hóa quá mức thiết kế và đảm bảo rằng việc áp dụng các nguyên tắc này là hợp lý và cân bằng với yêu cầu thực tế của dự án.

Bình luận (0)

Michael Gough
Michael Gough
Michael Gough
Michael Gough
Michael Gough
Michael Gough
Michael Gough
Michael Gough
Michael Gough
Michael Gough
Michael Gough
Michael Gough
Michael Gough
Michael Gough
Michael Gough
Michael Gough

Bài viết liên quan

Learning English Everyday