"Sự Phân Rã" - nền tảng "triết học" của lập trình


    Chào các bạn thân mến, hôm nay tôi muốn chia sẻ với mọi người một "ý tưởng" mà có lẽ nhiều bạn đã từng làm qua nhưng không mấy khi chú ý. Đó là khía cạnh hấp dẫn của "sự phân rã"  trong lĩnh vực lập trình.
    Nào cùng bắt đầu nhé!

    Thời kỳ đầu của lập trình, khi mới là thuở đầu sơ khai, thời mà con người ta vẫn vụng về với việc viết những dòng mã đơn giản nhất. Họ dùng các bảng điều khiển cơ khí hoặc gõ lệnh trực tiếp trên bàn phím máy tính. 
    Assembly và Fortran hầu như không có khái niệm về hàm và phương thức như trong các ngôn ngữ lập trình hiện đại. Trong Assembly, các chỉ thị và lệnh được sắp xếp theo thứ tự thực hiện tương ứng với các phép toán cơ bản trên phần cứng.
Screen Shot 2017-08-21 at 3.05.43 PM.png 262 KB

    Nhưng tôi vẫn cảm thấy manh nha một thứ gì đó trong suy nghĩ của họ, những ý niệm đầu tiên về "sự phân rã" trong lập trình. Có lẽ các khái niệm sau này như Class, Interface, Method cũng bắt nguồn từ những khó khăn của lập trình viên trong quá trình làm việc với code. Vì thời gian của các lập trình viên được trả được trả rất cao nhưng việc viết ra hàng đóng thứ một cách ngổn ngang, rồi lại tốn thời gian nhiều để kiếm lại nó và sửa lỗi lại không đem đến nhiều giá trị cho một cá nhân hay doanh nghiệp.

     Trong nội dung bài viết này, tôi sử dụng chủ yếu Java như những ví dụ để trình bày suy nghĩ của tôi về ý niệm của "sự phân rã" trong lập trình.

    1.SOLID
    1.1 S (Single Responsibility principle)
   
Mã nguồn được tổ chức thành nhiều module, mỗi module bao gồm nhiều lớp, mỗi lớp lại chứa nhiều phương thức, và mỗi phương thức có thể chứa các phương thức con nhỏ hơn. Tuy nhiên, mỗi module, mỗi lớp và mỗi phương thức chỉ nên thực hiện duy nhất một chức năng. Đây là cách chia nhiệm vụ trong quá trình phân tách dự án.
    Sự phân rã về nhiệm vụ giống như Chia để trị (Divide and Conquer), nó khiến việc hiện thức các chức năng đơn giản, và dễ mở rộng hệ thống.
    1.2 I (Interface Segregation Principle)
    Interface Segregation Principle ,
chúng ta đến với một nguyên tắc cơ bản hơn của thiết kế hệ thống là, hãy chia nhỏ các interface nhỏ nhất có thể, để đảm bảo chúng ta có thể hiện thức hết tất cả các abstract method trong interface. Tôi lấy một ví dụ sau

public interface GiaCam {
	Boolean coHaiChan(Object g);
	void an(Object g);
	void hitTho(Object g) 
}

public class VitTroi implements GiaCam {
	....
}

public class VitNhua implements GiaCam {
	....
} 
    Trên thực tế, đoạn code sau đã vi phạm nguyên tắc L (Liskov Substitution Principle) vì VitNhua không thể thay thể hoàn toàn 100% interface GiaCam, nó chỉ có thể hiện thực 1 abstract method duy nhất là  Boolean coHaiChan(Object g);
Để giải quyết vấn đề này, ta "phân rã" interface lớn thành các interface nhỏ nhất có thể.


public interface HinhDangGiaCam {
	Boolean coHaiChan(Object g);
}

public interface HanhDongGiaCam {
	void an(Object g);
	void hitTho(Object g) 
}

public class VitTroi implements HinhDangGiaCam, HanhDongGiaCam  {
	....
}

public class VitNhua implements HinhDangGiaCam {
	....
}
    Java hỗ trợ đa kế thừa cho implements nên việc bạn có thể chia nhỏ các interface để chúng có thể mô ta khái quát nhất, tính chất của 1 class là điều nên làm. Và "sự phân rã" trong lập trình là hướng đi của chúng ta.

    1.3 D (Dependency Inversion Principle)
    Bạn nào làm việc quen với Spring Framework hay MicroProfile thì chắc không còn xa lạ với nó.
    Việc thiết kế các Module trong các framework trên đều dựa trên Dependency Inversion Principle. Điều đó có nghĩa là, các module được tách nhỏ ra, chúng không phụ thuộc vào nhau, việc chúng được tại sử dụng là nhờ chúng ta tiêm các module để sử dụng lại các hàm đã implement. Mỗi module sẽ được chuyên biệt và được "phân rã" cho một chức năng, và chỉ duy nhất thực hiện chức năng đó.

    2. OOP (Object-Oriented Programming)
    2.1 Sự trừu tượng (Abstraction)
   
Lâu nay, ai học về sự trừu tượng trong OOP chỉ nghĩ là:
    "Tính trừu tượng là chọn dữ liệu từ một nhóm lớn hơn để chỉ hiển thị các chi tiết có liên quan của đối tượng cho người dùng. Nó giúp giảm độ phức tạp và nỗ lực lập trình. Đây là một trong những khái niệm quan trọng nhất của OOP."
Nhưng có một điều là, tại sao người ta lại làm như vậy? Khi đã hiểu được vấn đề, thì tại sao không viết thẳng ra code, mà phải đi define đủ thứ, làm nó dài dòng ra.
    Câu trả lời là "Khổ trước sướng sau thế mới giàu". Mình đùa thôi!!
    Thực ra, trong các dự án lớn, việc bạn cứ phải đọc code là điều không thể tránh khỏi. Cho nên, việc bạn có thể không cần đọc code mà vẫn hiểu được code, đó mới là đẳng cấp tối cao. Việc Interface ra đời, như mình đã trình bày ở đầu bài về giai đoạn khởi nguyên của lập trình là hết sức cần thiết. Nhớ có interface hay abstraction, việc bạn "phân rã" giữa việc giới thiệu và định nghĩa trở nên rõ ràng hơn. Bố cục dự án trở nên mạnh lạc hơn. Để mình ví dụ cho bạn thấy nhé. Nói suông chắc không tin đâu.

public class TinhTien() {
	public double tinhBangTienMat() {} 

	public double tinhBangTheTinDung() {} 

	public double tinhBangTheNganHang() {} 

	public double tinhBangSoHoNgheo() {} 
}
    Cách code trên không sai tẹo này, nhưng bạn có thấy nó hơi mì ăn liền không? Nếu bạn muốn viết thêm mấy phương thức nữa để mô tả về TheTinDung thì sao? Class nó sẽ càng bự ra, bự đến lúc, bạn nhìn thấy nó là ngán và không muốn refactor nữa thì thôi. Thay vì đó, bạn có thể implement như sau, cái nào ra cái đó, nhìn là mê ly liền.

interface TinhTien {
	double tinh()
}

public class TienMat implements TinhTien() {
	public double tinh() {
	}
}

public class TheTinDung implements TinhTien() {
	public double tinh() {		
	}
}
    Đó, chanh ra chanh, bưởi ra bưởi. Với cách thiết kế này, mình tin, sau này, bạn sẽ dễ scale out hệ thống của mình ra. Bởi vậy mới có câu "Đường dài, mới biết ngựa hay". Tóm lại đối với abstraction, đó là sự phân chia về logic, phân rã quá trình define với quá trình implements để sau này, bạn dễ phát triển hệ thống hơn.
 
 2.2 Encapsulation
   
Các Modifier  như public, private, protect giúp bạn xác định tầm vực của một class hay method, khoanh vùng, đóng gói các method, đảm bảo sự bảo mật cho hệ thống code. Đó là sự phân rã về tầm vực trong lập trình. Các biến, hàm ở tầm vực nào thì chỉ có giá trị trong tâm vực đó.

    3. Các design partern và các chiến lược trong lập trình
   
Đầu tiên tôi muốn đề cập đến là chiến lược chia để trị. Chia để trị đề cập đến việc giải quyết một vấn đề phức tạp bằng cách chia nó thành các vấn đề nhỏ hơn, dễ giải quyết hơn, sau đó kết hợp các giải pháp nhỏ này để tạo ra một giải pháp tổng thể. Chiến lược này nhấn mạnh sự phân chia công việc và sự tách rời giữa các phần khác nhau của một vấn đề để giảm độ phức tạp và tăng tính tái sử dụng.
    Bên cạnh đó, cũng có một số design partern dựa trên ý niệm phân rã, tôi có thể liệt kê sau như là: Composite Pattern, Strategy, Template Method Pattern Pattern, Factory Pattern, Factory, Observer.         
           
    4. Phân rã trong lập trình hiện đại 
    4.1 Single Page
   
Single-page React, Angular và sự phân rã trong lập trình có mối quan hệ chặt chẽ. Cả hai công nghệ đều hướng tới việc phân chia ứng dụng thành các thành phần nhỏ hơn để quản lý dễ dàng và tái sử dụng. Sự phân rã là một nguyên tắc thiết kế giúp tách biệt logic và chức năng của ứng dụng thành các phần độc lập, dễ bảo trì và kiểm thử. React và Angular đều cung cấp cách tiếp cận modularity và component-based để triển khai sự phân rã. Điều này giúp tăng hiệu suất, khả năng mở rộng và quản lý mã trong quá trình phát triển ứng dụng web hiện đại.
    4.2 Microservice
   
Microservices và sự phân rã trong lập trình có mối quan hệ mật thiết. Microservices là một kiến trúc phần mềm trong đó ứng dụng được chia thành các dịch vụ nhỏ, độc lập và có khả năng mở rộng riêng biệt. Mỗi dịch vụ tập trung vào một nhiệm vụ cụ thể và giao tiếp thông qua các giao thức nhẹ nhàng như HTTP hoặc message queue. Khi áp dụng sự phân rã vào microservices, mỗi dịch vụ có thể được phát triển độc lập, sử dụng công nghệ và ngôn ngữ khác nhau tuỳ theo yêu cầu. Điều này tạo ra tính linh hoạt, khả năng mở rộng và dễ quản lý trong việc xây dựng và triển khai hệ thống phức tạp.
    4.3 Lamda Function AWS
    Lambda Functions của AWS và sự phân rã trong lập trình có mối quan hệ chặt chẽ. Lambda Functions là một dịch vụ tính toán điện toán đám mây của AWS, cho phép bạn chạy mã mà không cần quản lý máy chủ. Sự phân rã trong lập trình là nguyên tắc tách biệt logic và chức năng thành các phần nhỏ để dễ bảo trì và kiểm thử. Khi sử dụng Lambda Functions, bạn có thể triển khai các hàm độc lập như các microservices. Mỗi hàm có thể thực hiện một tác vụ cụ thể và được kích hoạt bởi sự kiện từ các nguồn khác nhau. Sự phân rã giúp tăng tính linh hoạt và khả năng mở rộng khi triển khai các hàm riêng biệt và chỉ trả tiền cho lượng tài nguyên thực sự sử dụng. 
    4.4 Web3, Blockchain
   
Web3 và blockchain có mối quan hệ mật thiết với sự phân rã trong lập trình. Web3 là một bộ công cụ và giao diện lập trình ứng dụng (API) cho phép tương tác với blockchain. Blockchain, một công nghệ phân tán và bảo mật, lưu trữ dữ liệu trong các khối liên kết và được quản lý bởi các nút mạng phân tán. Sự phân rã trong lập trình là nguyên tắc tách biệt logic và chức năng thành các phần nhỏ để dễ bảo trì và kiểm thử. Mỗi phần của ứng dụng có thể được triển khai như một hợp đồng thông minh (smart contract) hoặc một dịch vụ độc lập.

    Mặc dù chỉ là những vì dụ sơ khai, nhưng mong các bạn nắm được về ý niệm phân rã trong lập trình. Nếu nó không nhiều tiện ích thì người ta đã không sử dụng nó một cách triệt để đến như vậy từ lý thuyết đến các ứng dụng đang chạy trong thực tế. Mong sẽ giúp các bạn khai phá ra một điều gì đó. 
Cám ơn các bạn đã đọc!!!
Xin chào thân ái và quyết thắng!!!