Inversion of Control nguyên lý của các nguyên lý
NỘI DUNG BÀI VIẾT
1. Nguyên lý Inversion of Control là gì?
Trước khi đến với định nghĩa Inversion of Control là gì? bạn hãy cùng tôi làm vài cốc trà đá chém gió tí. Trong ghế nhà trường có lẽ ai cũng sợ nhất môn Triết học bởi mấy lý do, khó hiểu (trìu tượng), nhiều định nghĩa cần học thuộc. Thế nhưng Triết học lại là khoa học của mọi khoa học bởi nó trìu tượng ở một tầng cao nhất. Bản thân tôi cũng đã quên tất cả những gì học được (nói đúng hơn là thuộc được) trong Triết học ngay sau khi thi xong môn này, thế nhưng sau một thời gian khá dài bộn bề lo toan với công việc và cuộc sống, những khái niệm, quy luật ngày xưa thuộc trong Triết học tự nhiên cứ lẩn vẩn trong đầu và rồi ngẫm lại, đúng thật! Điểm lại chút về 2 nguyên lý, 3 quy luật và 6 cặp phạm trù trong Triết học, có thể trong phạm vi hiểu biết của mình và những trải nghiệm đến hiện tại nó đã làm nên tất cả những gì đang diễn ra xung quanh ta. Sở dĩ viết vài câu chuyện phiếm ở đây hầu chuyện các bạn vì các khái niệm mà chúng ta bàn thảo trong bài viết này khá là trìu tượng, nó cũng như triết học là nguyên lý của các nguyên lý. Một trong những quy luật mà tôi hay chiêm nghiệm nhất là quy luật lượng chất:
Những thay đổi đơn thuần về lượng, đến một mức độ nhất định, sẽ chuyển hóa thành những sự khác nhau về chất.
Friedrich Engels
Bạn học một cái gì đó, hoặc làm một cái gì đó có thể không hiểu ngay nhưng đến khi có một lượng kiến thức, kinh nghiệm nhất định, bạn sẽ đột nhiên hiểu ra nó. Do vậy, nếu các định nghĩa về Inversion of Control dưới đây quá trìu tượng, khó hiểu bạn cũng đừng quá quan tâm, đến một ngày nào đó khi bạn lập trình đủ 100 dự án, có 10 năm “giờ bay” trong lập trình… tự nhiên sẽ hiểu. Chuyện phiếm vậy thôi, giờ chúng ta cùng vào phần chính của bài viết nhé. Định nghĩa nguyên lý Inversion of Control
Inversion of Control (IoC) là một nguyên lý thiết kế trong công nghệ phần mềm với các đoạn code khi đưa vào một framework sẽ nhận được luồng điều khiển từ framework hay nói một cách khác là được framework điều khiển. Kiến trúc phần mềm với thiết kế này sẽ đảo ngược quyền điều khiển so với lập trình hướng thủ tục truyền thống. Trong lập trình truyền thống các đoạn code thêm vào sẽ gọi các thư viện nhưng với IoC, framework sẽ gọi các mã thêm vào.
Nói thật, tôi cũng cố gắng đưa ra định nghĩa này về ngôn ngữ con người có thể hiểu được nhưng nó vẫn quá trìu tượng hoặc do tôi chưa đủ “giờ bay” nên cũng chỉ dừng lại ở một định nghĩa như vậy. Tôi cũng đã thử tìm hiểu các định nghĩa khác trên Internet nhưng cũng chỉ nhận được một câu: “Tốt nhất bạn nên quên nó đi”. Lại nói về cách nhận thức một vấn đề, theo quan điểm phép duy vật biện chứng, hoạt động nhận thức của con người đi từ trực quan sinh động đến tư duy trừu tượng, và từ tư duy trừu tượng đến thực tiễn. Tóm lại nếu chúng ta đi vào cái định nghĩa chết tiệt này ngay thì là một sai lầm trong cách tìm hiểu về một vấn đề.
Chém gió thôi nhé, Triết học ngày xưa mình thi lại đến lần thứ 3 mới qua :). Inversion of Control và Dependency Injection Thay đổi tư duy về kiểm thử theo Nguyên lý Agile
2. Trực quan sinh động về IoC
Inversion of Control là một sự “thi phường” trong việc mở rộng một framework, một đặc tính trong các framework. Chúng ta cùng xem một ví dụ đơn giản về chương trình nhập thông tin người dùng theo kiểu thủ tục truyền thống:
<?php
function process_name ($name) {
echo "Your name: " . $name;
}
function process_job ($job) {
echo "Your job: " . $job;
}
echo "What is your name?";
$answer = rtrim(fgets(STDIN));
process_name($answer);
echo "What is your job?";
$answer = rtrim(fgets(STDIN));
process_job($answer);
Code language: HTML, XML (xml)
Khi chạy ứng dụng này trong màn hình dòng lệnh, các dòng code của chúng ta đang có quyền kiểm soát, nó quyết định được khi nào đặt câu hỏi, khi nào đọc dữ liệu người dùng nhập vào và khi nào xử lý các kết quả này.
Tiếp tục, chúng ta viết lại ứng dụng sử dụng framework Laravel ở dạng giao diện đồ họa. Sự khác biệt lớn nhất giữa hai ứng dụng này là luồng điều khiển (flow of control). Trong chương trình dòng lệnh, chúng ta kiểm soát được khi nào các phương thức được gọi, nhưng trong trong chương trình dạng đồ họa thì không. Framework sẽ kiểm soát việc đó bằng một vòng lặp liên tục kiểm tra xem có dữ liệu nào được nhập vào không? Có thể bạn nhập nghề nghiệp trước khi nhập tên. Như vậy, trong ứng dụng thứ hai quyền điều khiển đã bị đảo ngược, quyền kiểm soát đã được về framework.
Đây là một ví dụ trực quan về nguyên lý Inversion of Control, nguyên lý này làm người ta liên tưởng đến một nguyên tắc khi làm việc trong Hollywood “Đừng gọi cho chúng tôi, chúng tôi sẽ gọi cho bạn”. Bạn đã hiểu được IoC là gì? nhưng tôi thì chưa, thật sự nó vẫn …éo thể hiểu được. Tạm thời gác qua việc đó, chúng ta cần thêm “lượng” để đến với bước nhảy vọt về “chất” ở cuối bài.
Một đặc điểm quan trọng của framework là các phương thức được định nghĩa bởi người dùng thông thường được gọi từ trong bản thân framework chứ không phải từ code ứng dụng của người dùng. Framework đóng vai trò của chương trình chính trong việc điều phối và sắp xếp hoạt động ứng dụng. Sự đảo ngược quyền kiểm soát này tạo ra cho framework sức mạnh thông qua việc mở rộng. Các phương thức được viết bởi người dùng định nghĩa các thuật toán trong framework cho một tính toán cụ thể. Inversion of Control cho thấy sự khác biệt giữa một framework và một thư viện.
- Một thư viện chỉ là tập hợp các tính năng mà bạn có thể sử dụng, nó được tổ chứ thành các class. Sau mỗi lần gọi một phương thức, thư viện sẽ làm một số việc và sau đó trả quyền điều khiển về cho người dùng.
- Framework là một biểu hiện của thiết kế trìu tượng với nhiều hành vi được xây dựng sẵn bên trong, để sử dụng nó bạn cần chèn các hành vi của bạn vào các nơi khác nhau trong framework bằng các class hoặc plugin. Code của framework sẽ gọi đến code của bạn tại những điểm cần thiết.
Có nhiều cách để bạn đưa thêm mã vào framework, trong ví dụ trên chúng ta sử dụng một textbox, bất kỳ khi nào, textbox phát hiện sự kiện người dùng nhập liệu, nó sẽ gọi đến các code trong một “bao đóng”. Một cách khác để làm điều này là để framework định nghĩa các sự kiện và code người sẽ đăng ký vào các sự kiện này. Trong Laravel, bạn có thể phát sinh một sự kiện khi thao tác với một đoạn code của mình và framework có cơ chế kiểm soát các sự kiện đó, hay nói cách khác bạn đã ủy quyền lại cho framework.
Các phương pháp tiếp cận là rất tốt, nhưng thỉnh thoảng bạn muốn kết hợp nhiều lời gọi phương thức trong một đơn vị mở rộng, framework cần định nghĩa một interface để client code phải thực thi nó cho các lời gọi phương thức liên quan. Đây chính là nguyên lý đóng mở trong nguyên lý SOLID cho thiết kế hướng đối tượng.
Thực sự bạn nào đọc được đến đoạn này tôi phải rất cảm phục vì sự kiên nhẫn của bạn, nhưng một lần nữa cặp phạm trù nguyên nhân và kết quả rất đáng để chúng ta phải cố gắng. Nguyên nhân: bạn kiên nhẫn tống vào đầu mớ lý thuyết trìu tượng này, kết quả: đến cuối bài bạn hiểu được “Inversion of Control là gì?”.
Trong cái trực quan có cái trực quan hơn, vậy tìm cái dễ nhất để hiểu trước rồi hiểu cái phức tạp sau. Nếu bạn đã tìm hiểu sơ lược về IoC trên mạng, bạn sẽ thấy có cả một rừng các thuật ngữ liên quan như Dependency Inversion Principle (DIP), Dependency Injection Pattern (DI), IoC Container. Khá là khó để phân biệt các khái niệm này, chúng ta cần một khẩu quyết chứa đựng cả một tàng thư.
DI is about wiring, IoC is about direction, and DIP is about shape.
IoC là hướng đi và DIP là định hình cụ thể của hướng đi còn DI là một thực hiện cụ thể.
Khẩu quyết này tạm vẽ thành hình ảnh cho dễ nhớ.
Đến đây chắc bạn đã đồng tình với tôi: “Inversion of Control là nguyên lý của các nguyên lý”.
3. Inversion of Control được hình thành như thế nào?
Đôi khi chúng ta tự hỏi, các nguyên lý như IoC, SOLID… được hình thành như thế nào? Tại sao người ta có thể nghĩ ra chúng? Quay trở lại với lập trình hướng đối tượng là một cách giải quyết các vấn đề theo tư duy hướng đối tượng. Cách thức này mô phỏng hệt như thế giới ngoài đời, do vậy các nguyên lý thiết kế trong cuộc sống thật hoàn toàn có thể đưa vào trong lập trình. Tiếp theo đây là một ví dụ tôi đọc được từ cuốn Dependency Injection in .NET, một cuốn sách giải thích khá hay về các khái niệm IoC, DI, DIP…
Trong nguyên lý cuối cùng Dependency Inversion của SOLID chúng ta có nói đến một ví dụ về một chiếc đèn bàn có dây điện được đấu nối trực tiếp vào ổ điện trong tường mà không qua ổ cắm và phích cắm, còn trong ví dụ này chúng ta thay nó bằng cái máy sấy tóc. Trường hợp này cho thấy vấn đề rất lớn khi các đối tượng phụ thuộc chặt chẽ với nhau trong thiết kế.
Trong một lần đi nhà nghỉ cùng bạn gái (giả tưởng thôi nhé), tôi đã giật mình khi nhìn thấy hình ảnh chiếc máy sấy được nối trực tiếp vào trong ổ điện mà không thông qua ổ cắm và phích cắm. Mục đích thì đã rõ, những kẻ thích táy máy sẽ “tắt điện” khi nhìn thấy cảnh này. Nhưng chuyện gì sẽ xảy ra khi chiếc máy sấy này hỏng, dù nó có là hàng Nhật xịn nhưng cũng có lúc hỏng chứ.
Để sửa chữa là khá phức tạp, đầu tiên chúng ta phải ngắt át-tô-mát, sau đó mở cái hộp điện ra và thay chiếc máy sấy mới vào. Ông thợ nào không cẩn thận có thể không bật điện lên để test xem cái máy mới thay có hoạt động không. Trong lập trình cũng vậy, nếu các module phụ thuộc chặt chẽ vào nhau (thường dùng thuật ngữ tightly coupled) thì khi một module có vấn đề, cả hệ thống sẽ rối tung, rất khó để duy trì và phát triển.
Thông thường, chẳng ai chạy dây các thiết bị điện trực tiếp vào hệ thống điện, thay vào đấy là sử dụng một phích cắm và ổ cắm. Một ổ cắm là một interface với chuẩn cắm phải phù hợp với hình dạng phích cắm. Như vậy, máy sấy tóc với ổ cắm và phích cắm đã hình thành một kết nối không phụ thuộc (loosely coupled). Có thể có nhiều cách kết hợp các thiết bị điện này với nhau để ra một hệ thống. Trong lập trình việc kết hợp này có thể so sánh với design pattern và các nguyên lý thiết kế.
Máy sấy tóc sẽ không còn ràng buộc với hệ thống điện, nếu chúng ta cần ổ điện cho laptop, đơn giản là rút máy sấy ra và cắm laptop vào ổ cắm. Các nhà thiết kế ổ cắm không quan tâm đến thiết bị điện như laptop, điện thoại, tivi… nhưng các thiết bị này vẫn hoạt động tốt khi cắm vào. Các thiết bị khác nhau có thể cắm vào ổ cắm mà không ảnh hưởng gì tương tự như nguyên lý thay thế Liskov trong thiết kế phần mềm. Trong Dependency Inject, nguyên lý Liskov là một trong những nguyên lý quan trọng, nó cho phép đáp ứng những yêu cầu trong tương lai thậm chí chúng ta chưa biết gì về nó trong hôm nay. Cũng giống như trong thực tế, nếu 10 năm nữa có một thiết bị điện mới thì nó vẫn cắm vào cái ổ cắm này mà không cần phải thay đổi bên trong.
Khi pin laptop đầy, bạn sẽ rút phích cắm của máy laptop ra và chuyển sang dùng pin. Trong lập trình, chúng ta luôn mong muốn một service/module/class nào đó là đang tồn tại, nếu thành phần đó đã được gỡ bỏ, chúng ta sẽ gặp lỗi NullReferenceException, với tình huống này chúng ta sẽ tạo ra một interface mà nó không làm gì. Đây là một design pattern được biết đến với tên là Null Object, nó đáp ứng việc rút phích cắm ra khỏi tường.
Mọi điều có thể xảy ra, nếu như hệ thống điện của cả khu đang gặp vấn đề, bạn sẽ cần đến một bộ lưu điện UPS để vẫn hoạt động được thêm một thời gian. Máy laptop và bộ lưu điện có những nhiệm vụ khác nhau, đây chính là nguyên lý đơn chức năng (single responsibility) trong thiết kế phần mềm. Cả UPS và laptop đều được sản xuất bởi các nhà máy khác nhau, được mua ở những thời điểm khác nhau nhưng có thể sử dụng kết hợp được với nhau, thậm chí bạn có thể cắm cả máy sấy tóc khi laptop không cắm vào bộ lưu điện.
Một thực tế là các nhu cầu luôn thay đổi, các hệ thống luôn mở rộng và cần thêm tính năng, việc mở rộng đôi khi chỉ là cách lắp ghép các thành phần với nhau theo những cách khác nhau. Decorator pattern là mẫu lập trình giúp thêm tính năng cho một class mà không cần viết lại hoặc thay đổi các code có sẵn. Một cách khác để thêm tính năng mới vào một code có sẵn là tổng hợp các implement hiện có cửa một interface sử dụng Composite pattern. Với việc sử dụng ổ cắm dài, chúng ta có thể thêm vào hoặc bớt đi các thiết bị điện cần chạy. Với cùng cách này Composite pattern dễ dàng thêm bớt tính năng bằng cách thay đổi tập các interface.
Đôi khi chúng ta có một thiết bị được mua ở nước ngoài với chuẩn đầu cắm khác với ổ cắm đang có, chúng ta cần một bộ chuyển đổi. Adapter pattern cũng thực hiện cùng một nhiệm vụ với các bộ chuyển đổi. Chỉ với một ví dụ thực tế về hệ thống điện đơn giản, chúng ta đã thấy có rất nhiều các ý tưởng đã được thực hiện ở trong thế giới hướng đối tượng trong lập trình.
4. Tại sao dùng Inversion of Control
Chuyện phiếm, chém gió kèm theo bao nhiêu cốc trà đá rồi mà vẫn không thể hiểu được đang nói về cái gì? Hic…
Ngay chính trong định nghĩa Inversion of Control đã nói nên mục đích chính của nguyên lý này đó là tính mở rộng của một hệ thống. Quay lại với câu chuyện về máy sấy tóc, các thiết kế ở ngoài đời giúp cho việc mở rộng một hệ thống là quá đơn giản. Trong lập trình cũng vậy, nhờ thực hiện những định hình cụ thể từ các nguyên lý, một hệ thống ứng dụng sẽ có tính mở rộng. Nhưng để đạt được tính mở rộng cho một ứng dụng thì trước tiên ứng dụng này cần loại bỏ sự phụ thuộc giữa một đối tượng này với một đối tượng khác.
Tính mở rộng có thể mở rộng ra hơn nữa ở góc độ duy trì và phát triển ứng dụng. Một hệ thống có thiết kế đơn giản sẽ dễ mở rộng hơn một hệ thống phức tạp. Hơn nữa các hệ thống áp dụng nguyên lý Inversion of Control sẽ dễ dàng trong kiểm thử ứng dụng do các thành phần có sự độc lập nhất định.
5. Quán đến giờ đóng cửa, hết trà đá!
“Em ơi! Anh thêm cốc nữa đê…”, không một tiếng đáp lại, miệng khô đắng, giọng gằn lên “Chủ quán…”, mịa nó chứ vẫn im lặng như tờ, cúi xuống định vớ đại con Chaco 81 lỗ quăng vào… Úi quá nửa đêm roài, con mưa ảnh hưởng bão cũng đã tạnh, chỉ còn chút gió vi vu… “Quán đến giờ đóng cửa, hết trà đá!” giọng nói lanh lảnh cất lên từ một khuôn mặt bây bi con bà chủ quán.
Mải chém gió, giờ mới nhớ ra, mới được nửa chặng đường từ trực quan đến tư duy trìu tượng. Thế còn từ tư duy trìu tượng đến thực tiễn, thôi hẹn bữa khác mạn đàm tiếp…
Nguồn: https://topdev.vn/blog/inversion-of-control-nguyen-ly-cua-cac-nguyen-ly/
Để lại một bình luận