Hế lô anh em, chào mừng anh em đến với blog của mình. Đôi lời chia sẻ đầu bài viết xíu. Mình dự định viết về một series mới là nguyên lý lập trình (programming principles) và những thứ liên quan (lên cao hơn là software architecture). Một phần là để giúp cho các anh em lập trình viên (developer) có kiến thức tốt hơn để trở thành một senior chính hiệu. Chứ code theo bản năng để cho ra sản phẩm (chạy được) thì … dev gà cũng làm được. Dev thực thụ sẽ có những dòng code đầy tính thẩm mĩ và mê hoặc lòng người – hơi chém =]]. Bài viết này mình đề cập tới nguyên lý dependency Inversion hay đảo ngược sự phụ thuộc khi dịch ra tiếng Việt.
Anh em tìm đọc được bài viết này, chứng tỏ anh em đang có nhu cầu tìm hiểu về các nguyên lý SOLID đúng không? Thôi thì chúng ta vào bài luôn và mình giới thiệu về SOLID một chút.
À bài viết này hơi dài một chút, hơi nhiều code một chút. Because dài và nhiều code thì mình mới có thể truyền tải hết được các kiến thức về nguyên lý Dependency Inversion cho anh em được. Chúc mừng anh em trước nếu anh em đọc và lĩnh hội được kiến thức trong bài viết này.
Sơ lược các nguyên lý SOLID
SOLID principles được giới thiệu lần đầu năm 2000 bởi Robert C. Martin (Uncle Bob), và có 5 nguyên lý sau:
- Single Responsibility (S)
- Open / close principle (O)
- Liskov substitution principle (L)
- Interface Segregation (I)
- Dependency Inversion (D)
Bài viết này mình sẽ trình bày chi tiết nhất về nguyên lý cuối cùng – Dependency Inversion Principle (DIP). Sẽ có anh em thắc mắc là không biết gì mấy nguyên lý đầu thì thì quất luôn nguyên lý cuối sẽ hiểu không? Anh em đừng lo, sẽ hiểu bình thường, mình lo tất =]].
Giải thích nguyên lý Dependency Inversion Principle
Nguyên văn của DIP như sau:
- High-level modules should not depend on low-level modules. Both should depend on abstractions.
- Abstractions should not depend on details. Details should depend on abstractions.
Dịch ra tiếng Việt:
- Module cấp cao (high-level module) không nên phụ thuộc vào module cấp thấp (low-level module). Mà cả 2 nên phụ thuộc vào abstractions.
- Abstraction không nên phụ thuộc vào chi tiết (implementation). Mà chi tiết (implementation) nên phụ thuộc vào abstraction.
Đọc thôi là thấy khó hiểu ** rồi phải không anh em =]]? Đừng lo! Mình sẽ giải thích cho anh em hiểu. Chứ ban đầu khi đọc về nguyên lý này, mình cũng như anh em, thua và muốn chửi thề!
Bad design khi không áp dụng Dependency Inversion
Xem ví dụ bên dưới – Ví dụ về thiết kế một chương trình nghe nhạc siêu siêu đơn giản. Đây là một cách viết code rất phổ biến. Có thể anh em sẽ thấy quen, hoặc chính anh em đã từng viết như thế cũng nên.
class MusicPlayer
{
private $file;
public function __construct()
{
$this->file = new MP3File(); // Vi phạm DIP
}
public function play()
{
return $this->file->play();
}
}
class MP3File
{
public function play()
{
return "Play MP3 file!";
}
}
Hoặc một cách viết khác như bên dưới: Inject dependency thông qua constructor của class. Cách viết này cho phép chúng ta viết Unit test dễ hơn nhưng vẫn vi phạm DIP
.
Lưu ý: Có một số từ ngữ như dependency
, unit test, inject, high/low-level module, … Anh em nếu mới tiếp xúc lần đầu thì sẽ không hiểu. Anh em đừng lo lắng, mình sẽ cho anh em “tiếp xúc” nhiều lần hơn trong xuyên suốt bài viết để anh em hiểu.
public function __construct(MP3File $file) // Vi phạm DIP
{
$this->file = $file;
}
Code như thế này thì … toang quá!
Giải thích một chút về thiết kế chương trình ở trên, chúng ta bắt đầu phát triển một phần mềm nhỏ để có thể chơi nhạc. Chúng ta có class MusicPlayer
đang sử dụng class MP3File
(Hay còn gọi là phụ thuộc – dependency).
Như vậy, với đoạn code trên chúng ta có thể giải thích một số khái niệm dưới đây:
- High (low)-level module: Class A nào đó sử dụng class B thì class A chính là high-level module. Và class B chính là low-level module. Như vậy ở ví dụ trên thì
MusicPlayer
là high-level module, cònMP3File
chính là low-level module. - Dependency:
MP3File
chính là 1 dependency (hay còn gọi là sự phụ thuộc) của high-level moduleMusicPlayer
- Hard dependency: Bạn có thể nhìn dòng code này
$this->file = new MP3File();
ở trên. Và đây được gọi là mộthard dependency
. Lưu ý thêm, khi trong code bạn có hard dependency thì bạn sẽ khó viết Unit test.
Đọc tới đây ổn đúng không anh em? Chúng ta cùng đi tiếp nào.
Tại sao thiết kế ở trên là Bad Design?
Khó bảo trì, khó mở rộng
Class MusicPlayer
bị phụ thuộc vào class MP3File
(dependency). Khi chúng ta thay đổi class MP3File
thì class MusicPlayer
cũng bị thay đổi theo. Điều này vi phạm Open / close principle
(anh em có thể search google 1p29s để đọc sơ sơ và nắm được nguyên lý này một xíu xíu nhá). Và giả sử trong tương lai hệ thống của chúng ta có rất nhiều class như MusicPlayer
(đang phụ thuộc vào MP3File
). Mỗi khi chúng ta thay đổi class MP3File
, thì phải thay đổi tất cả những nơi sử dụng class này. Dẫn đến khó bảo trì
và dễ gây ra bug
trong quá trình chỉnh sửa, phát triển hay bảo trì.
Mình lấy luôn một ví dụ cụ thể về sự thay đổi được đề cập ở trên cho anh em dễ nắm. Đọc đoạn code dưới sau đó đọc phần comment trong code để nắm nhá.
class MusicPlayer
{
private $file;
public function __construct()
{
// Thì phải đổi code ở đây để đối ứng
// cho việc thay đổi classname bên dưới
$this->file = new MP3MusicFile();
}
public function play()
{
return $this->file->play();
}
}
class MP3MusicFile // Thay đổi classname ở đây.
{
public function play()
{
return "Play MP3 file!";
}
}
Hiện tại thiết kế trên chỉ chạy cho file MP3, và khi hệ thống của anh em chỉ support cho duy nhất file MP3 thì đây là một thiết kế ổn. Nhưng trên thực tế, chẳng có cái máy chơi nhạc quái nào lại chỉ support cho duy nhất file MP3 cả, còn nhiều file khác như WAV, AAC, M4A, e.g. nữa chứ. Đây là vấn đề khó mở rộng
cho hệ thống. Code theo phong cách này mà mở rộng cho nhiều file khác là anh em… há mỏ ngay =]].
Chính vì vậy thiết kế trên chính là Bad design
. Một thiết kế như sh**.
Khó viết Unit test
Sơ lược một chút nhé!
Ở thiết kế trên khi chúng ta sử dụng new MP3MusicFile()
, đây là một hard dependency nên sẽ cực kì khó viết unit test. Làm sao biết là khó viết, thì chúng ta sẽ đi sơ lượt qua Unit test một chút. Mình sẽ giải thích cho cả những anh em chưa từng viết unit test vẫn có thể hiểu được. Nên sẽ hơi dài, anh em nào rành unit test rồi (và hiểu tại sao khó viết unit test) thì có thể bỏ qua phần này.
Tại sao chúng ta lại đề cập tới Unit test trong bài này? trên thực tế, khi anh em và đội ngũ làm ra một sản phẩm phần mềm, thì chúng ta thường gọi nó là production code (hay code sản phẩm). Bên cạnh production code, trong một số project “được đầu tư kĩ lưỡng hơn“ thì sẽ có thêm testing code (thông thường là unit test, và đôi khi có cả automation test nhưng trong bài viết này chúng ta chỉ đề cập tới unit test). Mục đích của unit test nói nôm na là kiểm tra tính đúng đắn theo từng đơn vị nhỏ như function, method, class, modules. Khi sản phẩm có unit test, chắc chắn rằng anh em sẽ hạn chế được bug một cách tối đa trong quá trình phát triển sản phẩm cũng như mở rộng, bảo trì, refactor.
Và một điều chắc chắn khi anh em viết code theo nguyên lý Dependency Inversion thì việc viết Unit test sẽ vô cùng đơn giản!
Ví dụ nè
Ví dụ chúng ta có một method downloadMusicFile
và với những kiến thức đã đọc tới bây giờ thì trong method này có một hard dependency là MusicFileRepository
.
// Đây là production code
public function downloadMusicFileById(int $fileId)
{
$musicFileRepository = new MusicFileRepository();
$musicFileRepository->download($fileId);
}
Chúng ta sẽ thử viết Unit test như bên dưới.
Về ý tưởng để test hàm downloadMusicFileById
thì đơn giản làm sao khi hàm này được gọi. Chúng ta phải kiểm tra xem method download
có được gọi đúng với $fileId
không là đủ rồi. Chúng ta không cần quan tâm method download
có chạy đúng hay không. Vì muốn biết nó chạy đúng hay không thì chúng ta sẽ viết unit test riêng cho hàm download
.
Và rõ ràng unit test (viết bằng PHPUnit) sẽ không thể chạy được (failed case) bởi vì chúng ta không thể mock hard dependency MusicFileRepository
.
À, mock
là một kĩ thuật được xài trong unit test rất nhiều. Anh em có 1p29s để search google và quay lại bài viết vì mình không thể giải thích quá nhiều sẽ dẫn tới một bài viết quá dài.
// Đây là unit test cho production code ở trên
// Tạo một mock object cho MusicFileRepository class
// chỉ mock cho method downloadMusicFileById()
$stub = $this->getMockBuilder(MusicFileRepository::class)
->setMethods(['downloadMusicFileById'])
->getMock();
// Expect method download được call 1 lần
$stub->expects($this->once())->method('downloadMusicFileById');
Nếu dùng kĩ thuật inject
dependency vào constructor hoặc method thì có thể dễ dàng mock MusicFileRepository
với kĩ thuật như trên.
Bên dưới là 2 cách để có thể viết unit test đơn giản hơn: Constructor injection
và method injection
.
// Method injection
function downloadMusic(
MusicFileRepository $musicFileRepository,
int $fileId
) {
$this->musicFileRepository->download($fileId);
}
// Constructor injection
private MusicFileRepository $musicFileRepository;
public function __construct(MusicFileRepository $musicFileRepository)
{
$this->musicFileRepository= $musicFileRepository;
}
public function downloadMusicFile(int $fileId)
{
$this->musicFileRepository->download($fileId);
}
Túm cái váy về Unit test
Túm cái váy lại, nếu trong code xuất hiện Hard Dependency thì việc viết Unit test sẽ trở nên khó khăn hơn. Chúng ta đề cập tới chữ khó ở đây để nói lên rằng, trong một số trường hợp vẫn có thể viết Unit test được. Đó chính là kĩ thuật Black Box. Nói đơn giản là chúng ta không cần quan tâm tới cấu trúc bên trong function/method viết gì. Mà chỉ cần quan tâm tới input, output thì khi đó có thể áp dụng kĩ thuật Black Box được.
Anh em có thể để lại comment nếu muốn một bài viết chuyên sâu về Unit Test. Hoặc có thể tìm kiếm thêm thông tin trên Internet về các kĩ thuật Unit test
Dependency Inversion Principle design
Vậy câu hỏi đặt ra là làm thế nào để chương trình của chúng ta vừa dễ phát triển, vừa dễ dàng bảo trì và mở rộng. Bây giờ hãy xem chương trình ở trên được thiết kế lại như sau:
class MusicPlayer
{
private $file;
// Chổ này bây giờ là phụ thuộc vào interface
public function __construct(PlayerFile $file)
{
$this->file = $file;
}
public function play()
{
return $this->file->play();
}
}
// Xuất hiện interface ở đây
interface PlayerFile {
public function play();
}
class MP3File implements PlayerFile
{
public function play()
{
return "Play MP3 file!";
}
}
class FLACFile implements PlayerFile
{
public function play()
{
return "Play FLAC file!";
}
}
Chúng ta đã tạo ra một interface PlayerFile
để phá vỡ sự phụ thuộc. Bây giờ thiết kế mới sẽ là
Hay nói cách khác, chúng ta đã tuân thủ nguyên tắc thứ nhất của DIP: High-level modules should not depend on low-level modules. Both should depend on abstractions
.
Và 2 class MP3File
và FLACFile
đang phụ thuộc vào interface PlayerFile
tuân thủ nguyên tắc thứ 2 của DIP: Abstractions should not depend on details. Details should depend on abstractions
.
Bây giờ với thiết kế mới, MusicPlayer
hoàn toàn chỉ phụ thuộc vào interface. Nên dù có thay đổi MP3, FLAC, hay thêm, mở rộng bất cứ loại file nhạc mới nào thì class MusicPlayer
vẫn sẽ không bị thay đổi. Hay nói cách khác là đã tuân thủ nguyên lý Open / close principle
. Điều này sẽ làm cho các developer khác tiếp cận tốt hơn, code clean hơn, dễ bảo trì và mở rộng hơn. Xong!
Đọc tới đây nếu anh em còn chưa hiểu thì có thể đọc lại vài lần nữa. Còn nếu vẫn không hiểu thì anh em để lại comment cho mình bên dưới. Mình sẽ giải đáp thắc mắc cho anh em. Chúc mừng và cám ơn anh em đã đọc tới
Lời chia sẻ “thật lòng”
Chúng ta là lập trình viên cũng có thể được xem là những kiến trúc sư trong thế giới lập trình. Việc thiết kế ra những chương trình/phần mềm ổn định, dễ bảo trì và mở rộng là điều cực kì quan trọng đối với một lập trình viên. Không phải ai mới bước vào thế giới lập trình cũng có thể ngày một ngày hai trở nên giỏi giang. Nhưng có một điều chắc chắn rằng, nếu anh em rèn luyện càng nhiều, thì khả năng sẽ càng tăng. Sẽ ngày càng giỏi hơn và dần trở thành một lập trình viên thực thụ.
To be continue…
Liên qua tới nguyên lý Dependency Inversion, còn 2 khái niệm nữa là Inversion of Control và Dependency Injection. Mình sẽ làm rõ 2 khái niệm này ở trong bài viết khác. Thân mời anh em đến dự tiệc….á nhầm đón đọc.
Những bài viết liên quan: