SOLID Principles

Các nguyên tắc SOLID của lập trình hướng đối tượng giúp làm cho các thiết kế hướng đối tượng trở nên dễ hiểu, linh hoạt và dễ bảo trì hơn.

Chúng cũng giúp dễ dàng tạo mã có thể đọc và kiểm tra được mà chúng ta có thể cùng nhau làm việc ở mọi nơi và mọi lúc cũng như giúp bạn biết cách viết mã tốt nhất

SOLID là từ viết tắt đại diện cho năm nguyên tắc thiết kế của thiết kế lớp Hướng đối tượng. Những nguyên tắc này là:

  • S – Single-responsibility Principle
  • O – Open-closed Principle
  • L – Liskov Substitution Principle
  • I – Interface Segregation Principle
  • D – Dependency Inversion Principle

Mỗi nguyên tắc đóng vai trò là kim chỉ nam để thúc đẩy các phương pháp thiết kế tốt và giúp đảm bảo rằng mã có thể mở rộng, bảo trì và dễ hiểu. Trong bài viết này, chúng ta sẽ khám phá sâu từng nguyên tắc SOLID và cách chúng có thể được áp dụng cho Laravel.

Single-responsibility Principle

Nguyên tắc trách nhiệm duy nhất, hay SRP, tuyên bố rằng một lớp chỉ có một lý do để thay đổi. Nói cách khác, một lớp chỉ nên có một trách nhiệm và tập trung làm tốt một việc. Nguyên tắc này giúp giảm độ phức tạp của mã và giúp bảo trì dễ dàng hơn.

Trong Laravel, SRP có thể được áp dụng bằng cách tạo các lớp nhỏ hơn, tập trung hơn để xử lý các tác vụ cụ thể. Ví dụ, thay vì có một lớp duy nhất xử lý đăng ký và đăng nhập của người dùng, sẽ tốt hơn nếu có các lớp riêng biệt để làm từng việc này.

PHP
class User
{
	public function register($data) {
    // Register user logic
  }

  public function login($data) {
    // Login logic
  }
}

Thay vào đó, mỗi lớp sẽ có một nhiệm vụ riêng biệt.

PHP
class UserRegistration {
  public function register($data) {
    // Register user logic
  }
}

class UserLogin {
  public function login($data) {
    // Login logic
  }
}

Open/Closed Principle (OCP)

Nguyên tắc đóng mở có thể gây nhầm lẫn vì nó là nguyên tắc hai chiều. Theo định nghĩa của Bertrand Meyer trên Wikipedia, nguyên tắc đóng mở (OCP) nói rằng các thực thể phần mềm (lớp, mô-đun, chức năng, v.v.) phải mở để mở rộng nhưng đóng để sửa đổi.

Định nghĩa này có thể gây nhầm lẫn, nhưng một ví dụ và sự làm rõ thêm sẽ giúp bạn hiểu rõ hơn.

Có hai thuộc tính chính trong OCP:

  • Nó có thể open – Điều này có nghĩa là bạn có thể mở rộng những gì mô-đun có thể thực hiện.
  • closed để sửa đổi — Điều này có nghĩa là bạn không thể thay đổi mã nguồn, mặc dù bạn có thể mở rộng hoạt động của một mô-đun hoặc thực thể.

OCP có nghĩa là một lớp, mô-đun, hàm và các thực thể khác có thể mở rộng hành vi của chúng mà không cần sửa đổi mã nguồn. Nói cách khác, một thực thể có thể được mở rộng mà không cần sửa đổi chính thực thể đó.

Trong Laravel, nguyên tắc này có thể được áp dụng bằng cách sử dụng các interface và các abstract class. Bằng cách xác định interface cho một tác vụ cụ thể, có thể tạo ra nhiều triển khai thực hiện tác vụ đó, mỗi tác vụ có chức năng riêng.

PHP
class PaymentMethod {
  public function processPayment($amount) {
    // Payment processing logic
  }
}

Thay vào đó:

PHP
interface PaymentMethodInterface {
  public function processPayment($amount);
}

Chúng ta có thể implement để sử dụng và không ảnh hưởng gì đến mã nguồn:

PHP
class CreditCardPayment implements PaymentMethodInterface {
  public function processPayment($amount) {
    // Credit card payment processing logic
  }
}

class BankTransferPayment implements PaymentMethodInterface {
  public function processPayment($amount) {
    // Bank transfer payment processing logic
  }
}

Liskov Substitution Principle (LSP)

LSP tuyên bố rằng các đối tượng của siêu lớp có thể được thay thế bằng các đối tượng của lớp con mà không ảnh hưởng đến tính chính xác của chương trình. Nói cách khác, một lớp con phải thay thế cho lớp cha của nó.

Trong Laravel, nguyên tắc này có thể được áp dụng bằng cách sử dụng tính kế thừa và đa hình. Bằng cách tạo một lớp cơ sở xác định chức năng chung cho một tập hợp các lớp, có thể tạo các lớp con kế thừa từ lớp cơ sở và thêm chức năng độc đáo của riêng chúng.

Một ví dụ rất phổ biến là kịch bản hình chữ nhật, hình vuông. Rõ ràng là tất cả các hình vuông đều là hình chữ nhật vì chúng là tứ giác có bốn góc đều là góc vuông. Nhưng không phải hình chữ nhật nào cũng là hình vuông. Để là hình vuông thì các cạnh của nó phải có cùng độ dài.

Hãy ghi nhớ điều này, giả sử bạn có một lớp hình chữ nhật để tính diện tích hình chữ nhật và thực hiện các thao tác khác như set màu sắc cho nó

PHP
class Rectangle {
    public function setWidth($width)
		{
        $this->width = $width;
    }

    public function setHeight($height) 
		{
				$this->height = $height;
    }

    public function setColor($color)
		{
				// set color processing logic
    }

    public function getArea() 
		{
        return $this->width * $this->height;
    }
}

Biết rõ rằng tất cả các hình vuông đều là hình chữ nhật, bạn có thể kế thừa các đặc tính của hình chữ nhật. Vì chiều rộng và chiều cao phải giống nhau nên bạn có thể điều chỉnh nó:

PHP
class Square extends Rectangle {
    public function setWidth($width)
		{
        $this->width = $width;
        $this->height = $width;
    }
		public function setHeight($height)
		{
        $this->width = $height;
        $this->height = $height;
    }
}

Nhìn vào ví dụ, nó sẽ hoạt động bình thường:

PHP
$rectangle = new Rectangle();
$rectangle->setWidth(10);
$rectangle->setHeight(5);
$area = $rectangle->getArea());
echo $area; // 50

Ở phần trên, bạn sẽ nhận thấy rằng một hình chữ nhật được tạo và chiều rộng và chiều cao được đặt. Sau đó, bạn có thể tính toán diện tích chính xác.

Nhưng theo LSP, bạn muốn các đối tượng của lớp con của bạn hoạt động giống như các đối tượng của lớp cha của bạn. Có nghĩa là nếu bạn thay thế Hình chữ nhật bằng Hình vuông, mọi thứ vẫn hoạt động tốt:

PHP
$square = new Square();
$square->setWidth(10);
$square->setHeight(5);

Bạn sẽ nhận được 100, vì setWidth(10) được cho là đặt cả chiều rộng và chiều cao thành 10. Nhưng vì setHeight(5), giá trị này sẽ trả về 25.

PHP
$square = new Square();
$square->setWidth(10);
$square->setHeight(5);
$area = $square->getArea());
echo $area; // 25

Điều này phá vỡ LSP. Đối tượng hình vuông lớp con không thể thay thế đối tượng hình chữ nhật siêu lớp để tính diện tích. Để khắc phục điều này, cần có một lớp chung cho tất cả các hình dạng sẽ chứa tất cả các phương thức chung mà bạn muốn các đối tượng của lớp con của mình có quyền truy cập. Sau đó, đối với các phương thức riêng lẻ, bạn tạo một lớp riêng cho hình chữ nhật và hình vuông

PHP
class Shape {
    public function setColor($color)
		{
        $this->color = $color;
    }
    public function getColor()
		{
        return $this->color;
    }
}

class Rectangle extends Shape {
    public function setWidth($width)
		{
        $this->width = $width;
    }
    public function setHeight($height) {
        $this->height = $height;
    }
    public function getArea() {
        return $this->width * $this->height;
    }
}

class Square extends Shape {
    public function setSide($side) {
        $this->side = $side;
    }
    public function getArea() {
        return $this->side * $this->side;
    }
}

Bằng cách này, bạn có thể đặt màu và lấy màu bằng cách sử dụng siêu lớp hoặc lớp con:

PHP
// superclass
$shape = new Shape();
$shape->setColor('red');
$color = $shape->getColor());
echo $color; // red

// subclass
$rectangle = new Rectangle();
$rectangle->setColor('green');
$rectangleColor = $rectangle->getColor());
echo $rectangleColor; // green

// subclass
$square = new Square();
$square->setColor('blue');
$squareColor = $square->getColor());
echo $squareColor; // blue

Interface Segregation Principle (ISP)

ISP tuyên bố rằng các lớp không nên bị buộc phải triển khai các giao diện mà chúng không sử dụng. Nguyên tắc này giúp giảm độ phức tạp của mã và giúp bảo trì dễ dàng hơn.

Trong Laravel, nguyên tắc này có thể được áp dụng bằng cách tạo các giao diện nhỏ hơn, tập trung hơn để xác định các nhiệm vụ cụ thể.

Ví dụ: giả sử bạn có một giao diện xác định các phương thức để vẽ các hình dạng cụ thể.

PHP
interface ShapeInterface {
    public function calculateArea();
    public function calculateVolume();
}

Khi bất kỳ lớp nào triển khai giao diện này, tất cả các phương thức phải được xác định ngay cả khi bạn không sử dụng chúng hoặc nếu chúng không áp dụng cho lớp đó.

PHP
class Square implements ShapeInterface {
    public function calculateArea()
		{
        //...
    }
    public function calculateVolume()
		{
        //...
    }  
}

class Cuboid implements ShapeInterface {
    public function calculateArea()
		{
        //...
    }
    public function calculateVolume()
		{
        //...
    }    
}

class Rectangle implements ShapeInterface {
    public function calculateArea()
		{
        //...
    }
    public function calculateVolume()
		{
        //...
    }   
}

Nhận thấy rằng chúng ta không thể tính thể tích của hình vuông hoặc hình chữ nhật. Vì lớp này triển khai giao diện nên bạn cần xác định tất cả các phương thức, ngay cả những phương thức bạn không sử dụng hoặc không cần.

Thay vào đó, ta nên tách nhỏ interface ra:

PHP
interface ShapeInterface {
    public function calculateArea();
}

interface ThreeDimensionalShapeInterface {
    public function calculateArea();
    public function calculateVolume();
}

Bây giờ bạn có thể triển khai giao diện cụ thể hoạt động với từng lớp.

PHP
class Square implements ShapeInterface {
    public function calculateArea()
		{
        //...
    } 
}

class Cuboid implements ThreeDimensionalShapeInterface {
    public function calculateArea()
		{
        //...
    }
    public function calculateVolume()
		{
        //...
    }    
}

class Rectangle implements ShapeInterface {
    public function calculateArea()
		{
        //...
    }  
}

Dependency Inversion Principle (DIP)

DIP nêu rõ rằng các mô-đun cấp cao không nên phụ thuộc vào các mô-đun cấp thấp, mà cả hai đều phải phụ thuộc vào sự trừu tượng hóa. Nguyên tắc này giúp giảm sự ghép nối giữa các mô-đun và giúp việc duy trì và mở rộng mã dễ dàng hơn.

Theo Wikipedia, nguyên tắc này nêu rõ rằng:

  • Các mô-đun cấp cao không được nhập bất cứ thứ gì từ các mô-đun cấp thấp. Cả hai nên phụ thuộc vào sự trừu tượng (ví dụ: giao diện).
  • Sự trừu tượng phải độc lập với các chi tiết. Chi tiết (triển khai cụ thể) sẽ phụ thuộc vào sự trừu tượng.

Trong Laravel, nguyên tắc này có thể được áp dụng bằng cách sử dụng dependency injection. Bằng cách xác định các phần phụ thuộc dưới dạng trừu tượng thay vì triển khai cụ thể, có thể thay đổi cách triển khai phần phụ thuộc mà không ảnh hưởng đến mã phụ thuộc vào nó.

Thay vì ta triển khai cụ thể:

PHP
class UserController {
  public function showProfile(int|string $id) {
    $userRepository = new UserRepository;
    $user = $userRepository->getUser($id);

    return view('user.profile', ['user' => $user]);
  }
}

Thì ta có thể xác định phần phụ thuộc dưới dạng trừu tượng:

PHP
class UserController {
  protected $userRepository;

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

  public function showProfile(int|string $id) {
    $user = $this->userRepository->getUser($id);

    return view('user.profile', ['user' => $user]);
  }
}

Advantages of SOLID

Sử dụng các nguyên tắc SOLID trong phát triển phần mềm có một số lợi ích:

  • Tăng khả năng bảo trì: Các nguyên tắc SOLID giúp tạo mã dễ bảo trì và sửa đổi hơn theo thời gian. Khi mỗi lớp hoặc mô-đun có một trách nhiệm duy nhất và tuân theo Nguyên tắc Mở/Đóng, các thay đổi có thể được thực hiện mà không ảnh hưởng đến các phần khác của mã.
  • Tăng khả năng kiểm tra: Nguyên tắc SOLID cũng giúp kiểm tra mã dễ dàng hơn. Mã tuân theo Nguyên tắc trách nhiệm duy nhất thường dễ kiểm tra hơn vì có ít sự phụ thuộc hơn và ít tác dụng phụ có thể xảy ra hơn.
  • Tăng tính linh hoạt: Bằng cách tuân thủ Nguyên tắc Mở/Đóng, bạn có thể tạo mã linh hoạt và có khả năng mở rộng hơn. Khi cần thêm chức năng mới, bạn có thể làm như vậy bằng cách thêm các lớp hoặc giao diện mới thay vì sửa đổi mã hiện có.
  • Tổ chức mã tốt hơn: Bằng cách tuân theo các nguyên tắc SOLID, bạn có thể tổ chức mã của mình tốt hơn và làm cho các nhà phát triển khác dễ hiểu hơn. Mã được tổ chức tốt và tuân theo các mẫu thiết kế rõ ràng sẽ dễ bảo trì và mở rộng hơn.

Conclusion

SOLID là một tập hợp các nguyên tắc thiết kế giúp thúc đẩy các phương pháp thiết kế tốt và đảm bảo rằng mã có thể mở rộng, bảo trì và dễ hiểu. Bằng cách áp dụng những nguyên tắc này trong Laravel, các nhà phát triển có thể tạo mã mạnh mẽ, có thể bảo trì và dễ mở rộng quy mô.

Cảm ơn mọi người đã dành thời gian để đọc 🙏

0 Shares:
4 comments
Leave a Reply

Your email address will not be published. Required fields are marked *

You May Also Like