Bộ nguyên lý SOLID – lập trình viên tương lai cần biết

0

Bộ nguyên lý SOLID có vai trò rất quan trọng khi phát triển ứng dụng và được sử dụng phổ biến trong thiết kế và lập trình hướng đối tượng. Khi sử dụng hợp lý, bộ nguyên lý SOLID giúp phần mềm dễ bảo trì, dễ mở rộng. Lập trình viên chuyên nghiệp bắt buộc phải biết và vận dụng thành thạo các nguyên lý SOLID. Đây là bộ nguyên lý mà sinh viên theo học các hướng/chuyên ngành phát triển ứng dụng cần biết. Bài viết này có mục tiêu giới thiệu bước đầu cho các bạn sinh viên các nguyên lý này để dần có ý thức áp dụng chúng khi code.

Giới thiệu chung về bộ nguyên lý SOLID

Bộ nguyên lý SOLID

Khi học lập trình hướng đối tượng (Object-Oriented Programming, OOP) trong trường, các bạn đều phải nắm chắc các nguyên lý của OOP. Các nguyên lý chính bao gồm tính trừu tượng (Abstraction), bao đóng (Encapsulation), đa hình (Polymorphism), kế thừa (Inheritance).

SOLID, ở khía cạnh khác, lại là các nguyên lý thiết kế trong OOP. Hiểu một cách đơn giản, các nguyên lý của OOP mô tả cách thức hoạt động, còn các nguyên lý SOLID mô tả cách thức vận dụng của OOP trong lập trình thực tế.

Việc tuân thủ theo SOLID giúp thiết kế (và code) phần mềm dễ đọc, dễ test, uyển chuyển, dễ bảo trì. Bạn nên hiểu rằng khâu code phần mềm chỉ chiếm khoảng 1/3 thời gian. Còn lại là dành cho khâu bảo trì (như thêm bớt chức năng, fix lỗi). SOLID giúp ích cực lớn cho khâu này.

Các nguyên lý SOLID khá khó tiêu đối với sinh viên hay thậm chí đối với lập trình viên mới vào nghề. Các chương trình đào tạo đại học cũng thường chỉ dạy code và công nghệ chứ không chú trọng về cách vận dụng. Vì vậy, việc tự học các vấn đề này là vô cùng quan trọng đối với sinh viên theo các hướng/chuyên ngành liên quan đến phát triển ứng dụng. Việc hiểu và vận dụng SOLID cũng có thể được dùng để đo sự tiến bộ của bạn trên con đường lập trình viên.

Để nắm được SOLID, bạn bắt buộc phải nắm vững các nguyên lý và kỹ thuật lập trình hướng đối tượng của một ngôn ngữ nào đó. Nếu vẫn chưa rành về lập trình hướng đối tượng thì chưa vội đọc những nguyên lý SOLID làm gì (vì có đọc cũng chẳng hiểu).

Các nguyên lý SOLID

SOLID là cách gọi tắt của một bộ năm nguyên lý sau:

Tên gọi các nguyên lý này khá khó dịch sang tiếng Việt. Vì vậy ở đây chúng ta dùng nguyên bản tiếng Anh.

SOLID không gắn với ngôn ngữ hoặc công nghệ cụ thể nào. Các nguyên lý SOLID là chung cho bất kỳ ngôn ngữ lập trình hướng đối tượng nào, dù là C#, Java hay C++. Tuy nhiên, cách thức thực hiện cụ thể sẽ phụ thuộc một phần vào các tính năng mà ngôn ngữ hỗ trợ.

Nói luôn là chúng ta sẽ không viết lại mô tả chính thức của các nguyên lý này như trong sách. Chúng ta sẽ cố diễn đạt nó bằng những từ ngữ đơn giản dễ hiểu. Các bạn cũng nên hiểu rằng đây là các nguyên lý, không phải kỹ thuật cụ thể. Bạn hiểu nguyên lý để định hướng cho cách làm.

Nguyên lý SRP – Single Responsibility Principle

Ví dụ

Hãy giả sử bạn làm việc ở một công ty nào đó. Bạn đảm nhiệm đồng thời công việc của kế toán và thủ kho. Như vậy bạn đồng thời phải biết và tuân thủ quy trình nghiệp vụ của cả hai bên. Bất kỳ bên nào có sự thay đổi về yêu cầu nghiệp vụ bạn sẽ phải thay đổi theo. Ôm nhiều vai cùng lúc vừa vất vả, vừa không chuyên tâm, vừa khó thay đổi.

Lấy một ví dụ khác. Giả sử bạn phát triển một phần mềm quản lý. Phần mềm này có chức năng kết xuất báo cáo (từ dữ liệu) và in ấn báo cáo. Tôi tin rằng nhiều bạn sẽ nhồi chúng vào cùng một class vì cùng là làm việc với báo cáo! Như vậy class này có thể thay đổi vì hai lý do: (1) nội dung của báo cáo thay đổi, (2) định dạng của báo cáo thay đổi.

Nguyên lý SRP

Nguyên lý chữ S cho rằng, việc có nhiều lý do khác nhau dẫn đến thay đổi một class như trên là một thiết kế tồi. Hay nói cách khác, chỉ có một lý do duy nhất để thay đổi một class. Từ đây cũng dẫn đến một cách giải thích khác: mỗi class chỉ nên chịu trách nhiệm cho một phần duy nhất của phần mềm.

Quay trở lại hai ví dụ trên, chức năng kết xuất báo cáo nên được đặt trong một class riêng, chức năng in báo cáo đặt trong một class khác. Hai nhiệm vụ khác nhau không đặt chung trong một class. Một người không nên đảm trách nhiều nhiệm vụ khác nhau.

Vận dụng

Lỗi thường gặp nhất khi các bạn học (và sử dụng) OOP là nhồi nhét đủ mọi thứ vào một class. Ví dụ, nhồi hết code giao diện với code xử lý nghiệp vụ và code xử lý dữ liệu. Đây là tình huống gặp đặc biệt nhiều khi dùng console hay windows forms.

Hãy cố gắng tách code ra nhiều class nhỏ theo chức năng của chúng sao cho mỗi class chỉ đảm nhiệm một nhiệm vụ xác định. Ví dụ, (1) class chuyên cho xuất thông tin của danh sách dữ liệu, (2) class chuyên cho nhập dữ liệu, (3), class chuyên cho truy xuất dữ liệu, v.v..

Việc tạo nhiều class nhỏ cũng có lợi thế so với một vài class lớn khi bảo trì code. Tuy nhiều class hơn nhưng mỗi class lại đơn giản hơn, do đó code ít bị lỗi hơn. Khi chỉnh sửa class nhỏ sẽ đơn giản hơn.

Để làm được việc này bạn phải phân tích rất rõ yêu cầu của bài toán. Từ đó đưa ra những chức năng chi tiết và hệ thống hóa chúng. Từ đó bạn xác định những class cần xây dựng sao cho mỗi class đảm nhiệm đúng một nhiệm vụ trong đó.

Nguyên lý OCP – Open-Closed Principle

Nguyên lý OCP

Nguyên lý này yêu cầu rằng mỗi class cần có tính “mở”, nghĩa là có thể bổ sung thêm chức năng mới, đồng thời phải có tính “đóng”, nghĩa là hạn chế tối đa sửa code của nó. Hiểu một cách đơn giản, mỗi class cần cho phép bổ sung thêm chức năng nhưng không được sửa trực tiếp mã nguồn của nó.

Yêu cầu này thường được thực hiện thông qua cơ chế kế thừa của OOP. Khi chúng ta cần thêm chức năng mới cho class thì không trực tiếp sửa mã nguồn của class gốc mà tạo ra class con kế thừa từ nó.

Do mỗi class được xây dựng xong sẽ được sử dụng bởi các class khác. Nếu thay đổi nó thì có thể dẫn đến lỗi ở các class cấp cao hơn sử dụng nó. Do vậy, nếu cần thay đổi thì phải tạo ra class con của nó và thực hiện thay đổi trong đó.

Vận dụng

Có một số cách khác nhau để vận dụng nguyên lý này phụ thuộc vào ngôn ngữ lập trình cụ thể. Ở đây chúng ta lấy ví dụ từ ngôn ngữ C#.NET framework.

Thứ nhất là thông qua cơ chế kế thừa. Tất cả các ngôn ngữ OOP đều hỗ trợ kế thừa. Khi đó class cần thiết kế để tránh tối đa sử dụng các thành phần không thể kế thừa. Trong C#, lớp con không thể kế thừa các thành phần static của lớp cha. Nên đặt các phương thức public là virtual để có thể dễ dàng ghi đè (override) ở lớp con.

Một điều khá lạ mà chúng tôi để ý là nhiều bạn sinh viên khi học OOP không hiểu và không biết áp dụng kế thừa.

Thứ hai là thông qua cơ chế delegate. Trong .NET framework, delegate là một cơ chế rất mạnh giúp mở rộng chức năng của class bằng cách cung cấp phương thức ở giai đoạn sử dụng class (thay cho giai đoạn định nghĩa như bình thường).

Thứ ba là thông qua phương thức mở rộng (extension method). Phương thức mở rộng cho phép bổ sung thêm phương thức mới vào bất kỳ class sẵn có nhưng không thông qua kế thừa. Cơ chế này cũng cho phép mở rộng class mà không cần thay đổi mã nguồn. Phương thức mở rộng rất phù hợp khi cần bổ sung một vài phương thức mới vào nhiều class.

Các bạn có thể để ý là không hề có code nào ở đây. Chúng tôi đã lưu ý từ trước rằng đây là các nguyên lý, tức là nó là các ý tưởng, là kim chỉ nam cho việc thiết kế và code thực tế.

Nguyên lý LSP – Liskov Substitution Principle

Nguyên lý này khá khó nhằn, đặc biệt nếu không hiểu rõ cơ chế kế thừa và đa hình.

Ví dụ

Cùng xem một ví dụ nhỏ. Giả sử bạn bạn báo cáo một ứng dụng quản lý sản phẩm cho giảng viên nhưng họ lại không thích cách thức bạn hiển thị danh sách sản phẩm và yêu cầu bạn phải chỉnh sửa.

Chiếu theo nguyên lý O ở trên, bạn không nên chỉnh trực tiếp code của class chịu trách nhiệm hiển thị danh sách sản phẩm mà nên tạo ra một class con của nó và thực hiện điều chỉnh ở đó.

Vấn đề đặt ra là liệu class cấp trên, nơi sử dụng class hiển thị danh sách sản phẩm, có thể tiếp tục sử dụng class mới mà không cần (hoặc rất ít) điều chỉnh code?

Nguyên lý LSP

Với những tình huống như trên, nguyên lý L đặt tiếp ra yêu cầu: khi thay thế object của class cũ (class cha) bằng object của class mới (class con) thì chương trình vẫn phải hoạt động bình thường, đồng thời không phải chỉnh sửa code của các class đang sử dụng nó.

Để tuân thủ hoàn toàn Liskov Substitution, khi thiết kế class đồng thời phải tuân thủ luôn nguyên tắc Dependency Inversion sẽ trình bày dưới đây.

Như vậy nguyên lý O và L (và cả D) đi thành cặp tay trong tay với nhau giúp code của bạn rất uyển chuyển. Có thể thay object/class này bằng cái khác mà không phải mất công code lại khắp nơi. Điều này rất có ích khi bảo trì hay nâng cấp phần mềm.

Nguyên lý này đặt ra yêu cầu sử dụng hợp lý kế thừa và đa hình.

Kế thừa và đa hình

Trong lập trình hướng đối tượng, kế thừa và đa hình là hai nguyên lý khác nhau.

Đa hình thiết lập mối quan hệ “là” (is-a relationship) giữa kiểu cơ sở (kiểu cha) và kiểu dẫn xuất (kiểu con). Ví dụ, nếu chúng ta có lớp cơ sở Bird và lớp dẫn xuất Parrot thì một object của Parrot cũng là object của Bird, kiểu Parrot cũng là kiểu Bird (đương nhiên rồi, vẹt là chim mà!).

Trong khi đó, kế thừa liên quan chủ yếu đến tái sử dụng code: code của lớp con thừa hưởng code của lớp cha. Một cách nói khác, đa hình liên quan tới quan hệ về ngữ nghĩa, còn kế thừa liên quan tới cú pháp.

Trong các ngôn ngữ như C++, C#, Java, hai khái niệm này hầu như được đồng nhất, thể hiện ở chỗ:

  1. class con thừa hưởng các thành viên của class cha (kế thừa, tái sử dụng code);
  2. một object thuộc kiểu con có thể gán cho biến thuộc kiểu cha, tức là kiểu cơ sở có thể dùng để thay thế cho kiểu dẫn xuất (đa hình).

Nguyên lý ISP – Interface Segregation Principle

Để hiểu nguyên lý này, trước hết bạn phải nắm được khái niệm và vai trò của interface trong ngôn ngữ lập trình mình đang sử dụng.

Khái niệm interface

Một cách đại khái, interface là một giao kèo giữa bên sử dụng và bên thực thi class. Cụ thể hơn, interface chứa các mô tả về phương thức và thuộc tính mà bên thực thi class phải xây dựng. Bên sử dụng thì không cần quan tâm đến cách thức xây dựng này.

Lấy một ví dụ khác. Giả sử đèn điện nhà bạn lắp toàn loại đui xoáy. Nếu bạn cần mua bóng đèn, có vô số loại khác nhau, từ đèn sợi đốt đến đèn huỳnh quang, từ hình vuông đến hình tròn. Nhưng chỉ cần nó là đui xoáy thì bạn đều có thể sử dụng được.

Khi so ra, đui xoáy ở đây chính là một dạng interface, là “giao kèo” giữa người sử dụng bóng đèn và người sản xuất bóng đèn. Người sản xuất chỉ cần đảm bảo “đui xoáy” cho bóng mình làm ra. Người sử dụng thì không cần quan tâm đến cách thức làm ra bóng đèn, miễn sao có đui xoáy là được.

Interface khi đó được bên sử dụng xem như một kiểu dữ liệu. Biến của kiểu dữ liệu này có thể tương thích với bất kỳ object nào tạo ra từ class thực thi giao diện tương ứng.

Nguyên lý ISP

Với vai trò một giao kèo (hay hợp đồng) như vậy, nguyên lý I đề nghị nên xây dựng các interface nhỏ chứa đúng những gì cần thiết có liên quan với nhau. Những điều kiện hợp đồng khác thì tách ra các interface riêng.

Hiểu một cách đơn giản, nếu có nhiều điều khoản khác nhau thì nên tách chúng thành các hợp đồng nhỏ, không nên gộp vào trong một hợp đồng lớn.

Điều này rất dễ hiểu: nếu hợp đồng (interface) chứa nhiều điều khoản (ít liên quan) thì bản thân bên thực thi (class thực thi giao diện) sẽ phải xây dựng hết các phương thức/thuộc tính tương ứng, dẫn đến có thể vi phạm nguyên lý S, hoặc có thể là thừa thãi.

Nguyên lý DIP – Dependency Inversion Principle

Đây là nguyên lý khó nhằn nhất của SOLID vì nó đi ngược lại cách hiểu thông thường các bạn được học trong lập trình hướng đối tượng.

Sự phụ thuộc giữa các class

Trước hết cần hiểu thế nào là sự phụ thuộc (dependency) giữa các class.

Hiểu một cách đơn giản nhất, class B được gọi là phụ thuộc vào class A nếu trong code của B xuất hiện A (như khởi tạo, gọi phương thức, v.v.). Khi này, class A phải được xây dựng trước class B. B được xem là class cấp cao hơn, A là class cấp thấp hơn.

Như vậy, sự phụ thuộc tạo ra thứ tự xây dựng các class có liên quan. Các class phụ thuộc nhau như vậy không thể được xây dựng song song. Class cấp thấp phải xây dựng trước. Đây là cách thức làm việc rất quen thuộc khi học lập trình hướng đối tượng.

Sự phụ thuộc này cũng có hệ quả xấu. Khi class cấp thấp thay đổi có thể dẫn đến thay đổi class cấp cao. Khi thay thế class cấp thấp sẽ phải sửa code của class cấp cao.

Nguyên lý DIP

Nguyên lý Dependency Inversion có hai ý:

  1. các class cấp cao không nên phụ thuộc vào các class cấp thấp. Thay vào đó, nên cho cả hai cùng phụ thuộc vào “cái trừu tượng” (abstraction) thứ ba.
  2. “cái trừu tượng” không nên phụ thuộc vào những cái cụ thể mà nên theo chiều ngược lại, nghĩa là những cái cụ thể phải phụ thuộc vào “cái trừu tượng”.

Nghe rất lằng nhằng khó tiêu phải không ạ! Cả hai anh này đều đảo ngược cách suy nghĩ quen thuộc của chúng ta khi học lập trình hướng đối tượng. Chúng ta sẽ giải thích kỹ hơn qua ví dụ vận dụng dưới đây.

* các class cấp cao không nên phụ thuộc vào các class cấp thấp

Vẫn tiếp tục với ví dụ class B phụ thuộc class A ở trên. Giờ chúng ta định nghĩa một interface mới, tạm đặt tên là IA, và cho A thực thi IA. Trong code của B giờ chỉ gọi đến IA mà không gọi đến A nữa. Như vậy B không còn phụ thuộc vào A mà quay sang phụ thuộc IA. B và A đã độc lập với nhau. Interface IA chính là “cái trừu tượng”, là kẻ thứ ba giúp B tránh phụ thuộc vào A; B và A là những “cái cụ thể”. Thay vì sử dụng interface, chúng ta cũng có thể sử dụng lớp trừu tượng (abstraction class) theo cách tương tự.

Bản chất của giải pháp này nằm ở chỗ, B và A bây giờ đưa ra một bản hợp đồng về những phương thức hay thuộc tính mà A cần phải thực hiện. B thì chỉ cần nhắm mắt sử dụng hợp đồng này (qua biến thuộc kiểu IA) mà không cần quan tâm A làm như thế nào. Cái này cũng giống như khi bạn đi mua bóng đèn trong ví dụ ở phần nguyên lý Interface Segregation ở trên. Người ta gọi quan hệ giữa B và A theo kiểu này là quan hệ qua giao diện, là một loại quan hệ gián tiếp, để phân biệt với kiểu quan hệ trực tiếp thông thường.

Yêu cầu thứ nhất này đảo ngược cách chúng ta cho các class tương tác so với khi học OOP.

* “cái trừu tượng” không nên phụ thuộc vào những “cái cụ thể”

Vẫn ví dụ A, B và IA ở trên. Theo nguyên lý này, khi thiết kế (ví dụ, sơ đồ class) chúng ta phải định nghĩa IA (cái trừu tượng) trước hết. IA xác định tương tác giữa A và B trong tương lai. B và A (cái cụ thể) được xây dựng sau.

Điều này có nghĩa là bản thân tương tác giữa các class phải được xem xét là một phần độc lập. Các class cụ thể sau đó mới xây dựng dựa trên tương tác này. Tương tác này được xây dựng dưới dạng interface hoặc abstract class.

Yêu cầu thứ hai này đảo ngược cách thức xây dựng class quen thuộc khi học OOP.

Vận dụng

Ở đây phát sinh một vấn đề. Do IA chỉ là một interface (hoặc lớp abstract), vậy object của A sẽ được tạo ở đâu? Vì nếu không tạo ra object của A thì ở giai đoạn runtime chắc chắn sẽ bị lỗi (dĩ nhiên rồi, làm gì có object thực sự mà chạy!). Có vài giải pháp khác nhau.

Cách thứ nhất là tạo ra thêm một class C chịu trách nhiệm khởi tạo cả B và A, đồng thời gán A cho IA (nằm trong B). Cách thứ hai là sử dụng kỹ thuật Dependency Injection với một IoC container (như Unity hay Ninject). Cách thứ ba là sử dụng một số kỹ thuật lập trình đặc biệt của ngôn ngữ, ví dụ trong .NET framework có thể sử dụng kỹ thuật lập trình Reflection. Cách thứ tư là sử dụng một vài mẫu thiết kế (design pattern) đặc biệt cho mục đích này như mẫu factory.

Nguyên lý này đòi hỏi bạn phải phân tích rất kỹ bài toán để xác định rõ tất cả các class sẽ xây dựng, vai trò và sự tương tác giữa chúng (có những anh nào, làm gì, và anh nào sử dụng anh nào). Từ đó áp dụng nguyên lý DI này để giúp phát triển đồng thời các class.

Kết luận

Trong bài viết này chúng tôi đã cố gắng đem lại cho bạn một cái nhìn tổng quát về bộ nguyên lý SOLID – bộ nguyên lý vận dụng OOP quan trọng hàng đầu đối với lập trình viên.

Nếu bạn để ý sẽ thấy, tổ hợp 5 nguyên lý SOLID giúp tạo ra các class tương đối độc lập (ít phụ thuộc nhau) và trọn vẹn. Điều này đem đến rất nhiều lợi ích khi phát triển ứng dụng.

Lợi ích trước hết là có thể dễ dàng thay thế và nâng cấp class khi cần thiết mà không ảnh hưởng đến code sẵn có. Điều này có giá trị rất lớn khi bảo trì.

Một lợi ích khác khi tuân thủ SOLID là bạn có thể phát triển các thành phần song song (do các class không phụ thuộc nhau, nguyên lý DIP). Điều này lợi ích cực lớn khi làm việc theo team.

Để nắm vững và vận dụng được các nguyên lý này trong công việc, trước hết bạn hãy đọc để thấm nhuần ý tưởng của chúng. Bạn có thể tìm kiếm các bài viết khác (rất nhiều) về chủ đề này trên Internet. Có thể có những bài viết giải thích dễ hiểu hơn, hoặc mang tính kỹ thuật nhiều hơn.

Sau khi hiểu rõ ý tưởng, bạn hãy tiếp tục tìm kiếm cách thức thực hiện trên ngôn ngữ lập trình mình cần. Do đây chỉ là bộ nguyên lý, cách thực hiện trên mỗi ngôn ngữ có thể có những đặc thù.

Cuối cùng, khi thực hiện bất kỳ bài toán nào, hãy suy nghĩ và phân tích thật kỹ lưỡng. Hãy xác định rõ những class nào sẽ xây dựng, nhiệm vụ cụ thể của chúng, cách chúng tương tác với nhau. Sau đó hãy điều chỉnh bản thiết kế đó cho phù hợp với các nguyên lý SOLID. Chỉ sau đó mới được bắt tay vào code.

Chúc bạn thành công!

Bình luận

avatar
  Đăng ký theo dõi  
Thông báo về