+12

OOP và nguyên tắc SOLID, KISS, YAGNI & DRY

Ngày nay, việc phát triển một phần mềm đòi hỏi nhiều sự nổ lực và cố gắng của nhiều người. Phần mềm càng ngày càng trở nên phức tạp nên việc phát triển phần mềm theo team nhiều người là một điều cần thiết. Và trong môi trường đó, với cương vị là một lập trình viên (developer) thì việc nắm rõ những mô hình cũng như là các nguyên tắc để xây dựng sản phẩm phần mềm là yêu cầu bắt buộc mà một lập trình viên giỏi cần nắm vững. Dưới đây là sự giới thiệu khái quát về mô hình lập trình hướng đối tượng (OOP) cũng như là trình bày các nguyên tắc lập trình được đông đảo lập trình viên thừa nhận và áp dụng: SOLID, KISS, YAGNI, DRY

OOP (Object-oriented Programing)

OOP hay lập trình hướng đối tượng là một mô hình lập trình. Ở đây tư tưởng cốt lõi là trừu tượng hóa các khái niệm cần giải quyết thành các đối tượng có thể biểu diễn và quản lý được trong ngôn ngữ lập trình.
Mô hình này giúp cho nhiều vấn đề được giải quyết dễ dàng trong thế giới thực.
Nguyên tắc xây dựng của mô hình này tập trung xoay quanh các class. Việc thiết kế các class và tạo ra các đối tượng (object) có hiệu quả cao là mối quan tâm hàng đầu trong lập trình hướng đối tượng.

Các tính chất của lập trình hướng đối tượng

OOP có 4 tính chất chính sau:

  • Tính đóng gói (Encapsulation): Tính chất này không cho việc thay đổi thuộc tính của đối tượng một cách trực tiếp. Muốn thay đổi thuộc tính của đối tượng cần phải thông qua phương thức mà đối tượng cung cấp.
  • Tính kế thừa (Inheritance): Là tính chất mà một class có thể kế thừa được các thuộc tính, phương thức của một class khác (gọi là class cha). Tính chất này giúp cho mô hình lập trình hướng đối tượng có thể tái sử dụng lại code giúp tối ưu hóa việc lập trình. Đây là một tính chất quan trọng cũng như cốt lõi tạo nên sự áp dụng rộng rãi của lập trình hướng đối tượng như ở hiện tại
  • Tính trừu tượng (Abstraction): Là tính chất thể hiện rằng đối tượng chỉ cần tập trung vào các thuộc tính, thông tin liên quan đến mục đích, nhiệm vụ mà nó cần giải quyết, không cần quan tâm đến các thuộc tính dư thừa không liên quan khác.
  • Tính đa hình (Polymorphism): Là tính chất một phương thức (method) được gọi ở các đối tượng khác nhau cùng kế thừa từ một lớp thì có thể biểu hiện khác nhau (overriding) hoặc một phương thức của đối tượng truyền vào số lượng tham số khác nhau thì sẽ thực hiện các hành động khác nhau (overloading) Vì tính đa hình là một tính chất khá khó hiểu của lập trình hướng đối tượng nên dưới đây sẽ là một ví dụ minh họa:
    Overriding
class ElectronicDevice //Base class
{
	public function run()
	{
		echo 'ElectronicDevice';
  }
}

class Laptop extends ElectronicDevice
{
  public function run()
	{
		echo 'Laptop';
  }
}

class TV extends ElectronicDevice
{
  public function run()
	{
		echo 'TV';
  }
}

$laptop = new Laptop()
$tv = new TV()
echo $laptop->run(); // Laptop
echo $tv->run(); // TV

Overloading:

class User {
	public void getInfo(int id) {
		System.out.println("Info with id");
	}
	
	public void getInfo(int id, String name) {
		System.out.println("Info with id and name");
	}
	
	public void getInfo(int id, String name, String address) {
		System.out.println("Info with id, name and address");
	}
}

Lưu ý: PHP không có overloading như các ngôn ngữ java, c#. Với php có thể sử dụng một số cách đề thay thế overloading.

SOLID

Với sự phát triển và thịnh hành của mô hình lập trình hướng đối tượng OOP thì yêu cầu các nguyên tắc trong việc xây dựng và thiết kế cần được đặt ra để giúp tối ưu nhất và đảm bảo clean code. Do đó SOLID được đưa ra.
SOLID được đưa ra bởi Robert C. Martin và Michael Feathers. Bao gồm các nguyên tắc:

  • Single responsibility principle (SRP)
  • Open/Closed principle (OCP)
  • Liskov substitution principle (LSP)
  • Interface segregation principle (ISP)
  • Dependency inversion principle (DIP)

1. Single responsibility principle (SRP) - Nguyên tắc đơn chức năng

Nguyên tắc này quy định mỗi class chỉ nên giữ một trách nhiệm duy nhất. (Chỉ nên có một lý do duy nhất để thay đổi class)
Một trách nhiệm này có thể hiểu là các phương thức trong class chỉ nên tác động tới đối tượng được trừu tượng hóa thành class, tránh tạo ra các phương thức có thể tác động đến các đối tượng là sự trừu tượng hóa của một khái niệm hay một class khác.
VD: Trong 1 công ty có các nhân viên với các chức vụ khác nhau: Lập trình viên, Salesman, Marketer. Và các nhân viên này đều kế thừa từ class Employee ➔ Có thể viết 1 class Employee bao gồm tất cả các phương thức của các lớp trên. Dù vậy nếu như vậy thì sẽ vi phạm nguyên tắc SRP khi class Employee đảm nhiệm quá nhiều trách nhiệm. Khi đó class sẽ dễ dàng phình to và tạo sự khó khăn khi muốn thay đổi hoặc thêm bớt chức năng (vì mỗi lần thay đổi đều sẽ ảnh hưởng tới các đối tượng tạo ra từ class employee một cách không cần thiết).
➔ Để giải quyết vấn đề này thì ta có thể tách lớp Employee cồng kềnh ở trên thành các lớp con kế thừa từ lớp Employee như Developer, Salesman, Marketer. Rồi sử dụng các lớp con này để tạo đối tượng mà ta mong muốn.
Điểm cốt lõi khi áp dụng nguyên tắc này đó chính là khả năng nhận ra một phương thức, thuộc tính của class có nên tách ra thành một class khác hay không. Nếu có thể nhận ra được điểm tối giản của một class nơi nó không thể tách ra thành class khác được nữa (nếu tách thì sẽ không hiệu quả) thì là lúc áp dụng nguyên tắc này thành công.

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

Nguyên tắc này quy định nếu như có một chức năng mới thì không nên sửa đổi bổ sung vào class có sẵn mà nên viết thêm một class khác mở rộng cho class có sẵn (bằng kế thừa, ...) và sử dụng.
Nguyên lý này sẽ tạo ra nhiều class nhưng sẽ đảm bảo việc class cũ sẽ không bị ảnh hưởng hay thay đổi, từ đó sẽ đỡ tốn thời gian test lại class cũ đó.
Cách để viết một class có thể đáp ứng được yêu cầu trên là tách phần dễ thay đổi ra khỏi phần khó thay đổi. Lúc đó phần khó thay đổi sẽ không cần phải thay đổi nữa và phần dễ thay đổi sẽ được thay đổi bằng cách thêm các lớp khác.

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

Nguyên tắc này quy định các lớp con kế thừa từ lớp cha có thể thay thế class cha mà không làm hòng tính đúng đắn của chương trình.
Ví dụ về sự vi phạm nguyên tắc LSP:
Khi xây dựng class Animal, các class con kế thừa như Bird và Tiger

class Animal 
{
	public function run()
	{
		echo "Run";
	}
	public function fly()
	{
		echo "Fly"
	}
}

class Bird extends Animal
{
	
}

class Tiger extends Animal
{
	// Không sử dụng function fly()
}

➔ Có thể thấy lớp Tiger kế thừa lớp Animal từ đó có thể sử dụng phương thức fly() ➔ phương thức này sẽ bị lỗi dẫn đến việc lớp Tiger không thể thay thế được cho lớp Animal ➔ vi phạm nguyên tắc LSP
Để giải quyết vấn đề trên thì có thể tạo interface cho phương thức fly và chỉ có class Bird implement phương thức fly đó.
Một số dấu hiệu vi phạm LSP:

  • Các lớp dẫn xuất có các phương thức ghi đè phương thức của lớp cha nhưng với chức năng hoàn toàn khác.
  • Các lớp dẫn xuất có phương thức ghi đè phương thức của lớp cha là một phương thức rỗng.
  • Các phương thức bắt buộc kế thừa từ lớp cha ở lớp dẫn xuất nhưng không được sử dụng.
  • Phát sinh ngoại lệ trong phương thức của lớp dẫn xuất. Điểm cốt lõi cần ghi nhớ khi áp dụng nguyên tắc này là chỉ tạo lớp B kế thừa lớp A khi lớp B có thể hoàn toàn thay thế cho lớp A.

4. Interface segregation principle (ISP) - Nguyên tắc phân tách

Nguyên tắc này quy định một interface không nên có quá nhiều phương thức (method) cần implement. Nếu một interface quá lớn thì nên tách interface đó ra thành nhiều interface nhỏ hơn đảm nhiệm các chức năng riêng.
VD:

interface Repository {
	public function getAll();
	public function get($id);
	public function save($data);
	public function update($id, $data);
	public function delete($id);
	public function getAllWithPaginate($page, $pageSize);
	public function getAllWithSort($sort);
	...
}

Có thể thấy ở ví dụ interface Repository trên thì nó bao gồm rất nhiều phương thức phải implement. Nếu như thêm chức năng thì interface trên sẽ bị phình to một cách nhanh chóng khó có thể kiểm soát. Đồng thời một số class có thể không cần dùng đến hết các phương thức interface cung cấp nhưng vẫn phải implement nó dẫn đến việc tốn thời gian. (VD class Category có thể không cần đến các phương thức getAllWithPaginate, getAllWithSort).
➔ Cách giải quyết: Chia nhỏ interface trên thành các interface nhỏ hơn.

interface CrudRepository {
	public function getAll();
	public function get($id);
	public function save($data);
	public function update($id, $data);
	public function delete($id);
}
interface PagingAndSortingRepository {
	public function getAllWithPaginate($page, $pageSize);
	public function getAllWithSort($sort);
}

➔ Giúp dễ dàng hơn trong việc sử dụng cũng như mở rộng interface.

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

Nguyên tắc này quy định:

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

Có thể hiểu nguyên tắc này quy định là các source code dependency chỉ trỏ tới các thành phần trừu tượng (abstraction), chứ không phải là các thành phần cụ thể (concretion).
Bản chất cốt lõi của nguyên tắc này là tránh sự phụ thuộc vào các module, các thành phần cụ thể dễ thay đổi trong quá trình code. Thay vào đó nên phụ thuộc vào các thành phần trừu tượng (abstraction) vì các thành phần này ít biến động.
Lưu ý: nguyên tắc này có thể có một vài ngoại lệ như sử dụng các thành phần cụ thể của các background hệ điều hành hoặc các platform vì chúng là những thứ rất ít khi thay đổi.
Cách để áp dụng và thực hiện nguyên tắc này là để các module cấp cao định nghĩa các interface, sau đó các lớp module cấp thấp sẽ thực thi các interface đó.
VD:

interface DBConnection {
    public function connect();
}

class MySQLConnection implements DBConnection {
    public function connect() {
        echo "MySQL connected";
    }
}

class OracleConnection implements DBConnection {
    public function connect() {
        echo "Oracle connected";
    }
}
class MongoConnection implements DBConnection {
    public function connect() {
        echo "MongoDB connected";
    }
}

Kết luận

Việc áp dụng các nguyên tắc của SOLID sẽ giúp cho các thiết kế của phần mềm trở nên rõ ràng, minh bạch, dễ hiểu - điều này rất quan trọng trong môi trường phát triển phần mềm khi các phần mềm đều được xây dựng dựa trên các team.
Đồng thời nó cũng giúp cho code có thể dễ dàng thay đổi, mở rộng, tăng khả năng tái sử dụng code từ đó giúp cho thời gian phát triển phần mềm trở nên nhanh hơn và sản phẩm phầm mềm có chất lượng tốt hơn.

Nguyên tắc KISS YAGNI & DRY

KISS

Keep It Simple, Stupid - KISS là nguyên tắc được được đặt ra bởi Kelly Johnson, với ý nghĩa nhấn mạnh tầm quan trọng của sự đơn giản trong các đoạn code. Đoạn code càng đơn giản thì khả năng để đọc và hiểu được đoạn code đó càng nhanh, càng đơn giản càng dễ dàng để bảo trì cũng như thay đổi trong tương lai, việc này sẽ giúp tiết kiệm thời gian hơn rất nhiều.
Việc giữ cho code được đơn giản nhưng vẫn đáp ứng đầy đủ các nhu cầu nghiệp vụ là một nhiệm vụ đòi hỏi thời gian và công sức để tối giản hoá. Code càng tinh gọn, dễ hiểu thì khả năng áp dụng của nó càng mạnh mẽ.
Những cách để áp dụng KISS:

  • Không lạm dụng các design parttern, các thư viện nếu như không cần thiết
  • Phân loại nhỏ bài toán lớn ra thành các bài toán nhỏ hơn để xử lý ➔ làm mọi thứ đơn giản hơn
  • Đặt tên biến, phương thức một cách rõ ràng, dễ đọc

YAGNI

You Aren't Gonna Need It - YAGNI là nguyên tắc chú trọng đến việc không nên làm phức tạp hóa một yêu cầu bằng các giả định trong tương lai. Hay nói cách khác là đừng giả định và xây dựng các chức năng của một phần mềm trước khi cần dùng tới nó.
Việc giả định các chức năng và code chúng sẽ gây ra rất nhiều sự lãng phí về thời gian, tiền bạc cũng như công sức của team (review code, testing, ...) và đôi khi sẽ không thu lại được gì (Khi chức năng đó trong tương lai không hề cần thiết). Do đó chỉ nên dành thời gian phát triển các chức năng cần thiết ở hiện tại.
Lưu ý: Nguyên tắc này không bao gồm phạm vi các nhiệm vụ cần đề code được clean, dễ thay đổi. Phạm vi của nguyên tắc này chỉ nói đến các chức năng nghiệp vụ của phần mềm chừ không phải các yêu cầu kĩ thuật của phần mềm. Một source code của phần mềm vẫn cần phải tuân thủ theo các nguyên tắc thiết kế (Clean code, SOLID) để đảm bảo tính linh hoạt cho phần mềm.

DRY

Don't Repeat Yourself - DRY là một nguyên tắc quen thuộc và cốt lõi trong ngành lập trình. Nguyên tắc được xây dựng bởi Andrew Hunt và David Thomas trong cuốn sách của họ The Pragmatic Programmer, muốn nhấn mạnh đến việc nên tái sử dụng lại code hết mức có thể.
Nguyên tắc này giúp cho các phần của code ít bị lặp lại hơn, dễ dàng và nhanh chóng thay đổi các đoạn code (chỉ cần thay đổi ở một nơi mà không cần phải thay đổi ở nhiều nơi) từ đó giảm thiểu thời gian phát triển phần mềm.
Để áp dụng nguyên tắc này thì bất cứ khi nào có một đoạn mã được sử dụng 2 lần ở những nơi khác nhau thì nên đóng gói lại đoạn mã đó (tạo hàm, tạo class, ...) để sau này có thể gọi đến nó để sử dụng lại.

Tài liệu tham khảo


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí