The Quality of Software Design - Part 2
Bài đăng này đã không được cập nhật trong 8 năm
Part 2. The Utopia of Reuse
Khi bàn về chất lượng thiết kế phần mềm, tiếp sau Correctness, Robustness, có rất nhiều thuật ngữ khác nhưng có lẽ phải kể đến Extendibility (Tính mở rộng) và Reusability (Tính tái sử dụng). Tôi nghĩ các bạn đều hiểu đại khái ý nghĩa của những từ này. Nói một cách đơn giản, đó là “Có dễ thêm, thay đổi chức năng không?” và “Có thể sử dụng như một bộ phận ở chỗ khác không?”. Tuy nhiên, tại sao những điều này lại quan trọng đến thế nhỉ? Bạn đã từng bao giờ đặt câu hỏi như vậy chưa? Đặc biệt trong những trường hợp phát triển phần mềm có chức năng được xác định theo yêu cầu từ khách hàng thì “Extendibility” hay “Reusability” đều là những tiêu chuẩn đánh giá quan trọng mà chúng ta phải nghĩ đến sau Correctness đúng không? Đầu tiên, định nghĩa “Correct” = “làm đúng theo yêu cầu” có đúng không? Cách nghĩ này không chính xác 100% nhưng cũng không phải sai hoàn toàn. Thực tế, cho đến bây giờ, trong ngành phần mềm máy tính có lịch sử nửa thế kỷ này, đặc biệt rất nhiều phần mềm đời cũ được viết bằng những ngôn ngữ như COBOL, hầu như được tạo ra mà không có bất kỳ sự cân nhắc nào về “Extendibility” hay “Reusability”. Mặc dù vậy, chúng vẫn hoạt động bình thường. Nếu có thể tạo ra phần mềm chạy đúng mà không cần mở rộng hay không cần tái sử dụng mà mỗi lần đều tạo mới từ đầu, và có thể lo liệu được phần effort cho việc tạo mới đó thì chắc không có vấn đề gì. (*Tham khảo)
Trong tác phẩm nổi tiếng ”Object-Oriented Software Construction” ra đời năm 1988, Bertrand Meyer đã thảo luận về ”Reusability”. Khi đó có lẽ có nhiều bạn độc giả bây giờ còn chưa được sinh ra. Đầu tiên, tôi xin được tóm tắt những lợi ích mong đợi từ đoạn code ”Reusable” mà Meyer chỉ ra: Timeliness: Tiết kiệm thời gian bỏ ra để phát triển phần mềm. Decreased maintenance effort: Tiết kiệm effort maintenance. Reliability: Dễ dàng đảm bảo tính tin cậy. Efficiency: Dễ dàng tối ưu hóa hiệu quả tài nguyên. Consistency: Nâng cao toàn bộ cohesion phần mềm thông qua việc sử dụng Library tốt Investment: Biến know-how thành tài sản. Có lẽ không khó khăn gì để hiểu hầu hết những khái niệm này. Tuy nhiên khái niệm Consistency có vẻ hơi khó hiểu một chút. Ý nghĩa của khái niệm đó là: “Thông qua việc xây dựng phần mềm dựa trên Library (Framework) được thiết kế tốt, mang lại ảnh hưởng tích cực cho toàn bộ thiết kế, góp phần vào việc nâng cao chất lượng phần mềm cuối cùng”.
Vậy, đầu tiên tôi muốn hỏi, khi nói đến “Reusability”, các bạn nghĩ đến điều gì? Hay có phải “việc sử dụng standard library của C“ là một phần của “Reusability” không? Câu trả lời tất nhiên là “Yes” rồi. Ngay cả những thứ được tiêu chuẩn hóa mang tính lịch sử như standard library, stl, Framework ở nhiều môi trường phát triển khác nhau v.v những thứ do provider của OS cung cấp thì cũng chính là “reuse” thành quả của những người đi trước. Việc “reuse” những Library hay Framework, Components có sẵn như thế này được gọi là “reuse với tư cách là consumer”. Bởi vì, việc lựa chọn môi trường thích hợp, sử dụng một cách có hiệu quả thì cũng có thể cho ra thiết kế phần mềm có chất lượng do Reusability. Đây không phải chuyện hay ho gì nhưng cách đây khoảng 10 năm trước, có một công ty, tạm gọi là công ty S, thực hiện một dự án phát triển phầm mềm có sử dụng Framework nọ. Nếu suy nghĩ như bây giờ thì do size của phần mềm được yêu cầu phát triển và ý tưởng về Framework là không phù hợp. Framework được sử dụng quá cồng kềnh. Kết quả tạo ra vô cùng tệ hại, bị trễ ngày release, phần mềm khi được release thì “không cả khởi động được”. Mỗi khi khởi động lại phát sinh Exception. Người dùng sau khi cài đặt và sử dụng phần mềm đó đã bình luận trên Internet như sau:
Chương trình này mỗi khi khởi động thì lại hỏi tôi là “Bạn có muốn debug không?” Tôi mà debug á? Câu chuyện đó đã trở thành truyền thuyết trong phòng phần mềm của công ty S.
Nhắc lại một chút “việc chọn Library thích hợp để sử dụng một cách có hiệu quả” là một việc vô cùng quan trọng đối với chất lượng của thiết kế phần mềm. Việc cố ép Framework quá lớn vào một phần mềm có utility thấp là không đúng. Việc mất một lượng effort khổng lồ để đưa một phần mềm lớn vào một môi trường chật hẹp bất chấp việc đã có sẵn Library phù hợp cũng là sai. Cụ thể trong trường hợp sau, Bertrand Meyer đã gọi là hội chứng NIH (Not Invented Here), ý chỉ việc kỳ thị những phần mềm hay Library không phải do bản thân cá nhân hay nhóm tạo ra. Việc đáng lẽ chỉ code trong một ngày nếu dùng Library có sẵn thì lại mất hàng mấy tháng trời để tự code. Tất nhiên, cũng có “ những phần mềm cần phải làm thế”. Hệ thống liên quan đến Quốc Phòng hay hệ thống Tài Chính là một ví dụ. Nói cho cùng thì phần “phù hợp” là phần quan trọng, cần phải luôn luôn lưu ý để việc sử dụng một cách có hiệu quả Library “phù hợp” theo chất lượng phần mềm được yêu cầu. Thực ra, tôi muốn các bạn hiểu được rằng việc “reuse với tư cách consumer” là một việc đòi hỏi khả năng nhận thức sâu sắc và nhiều kinh nghiệm.
Vậy, tiếp theo chúng ta hãy cùng thử nghĩ về việc “reuse với tư cách producer” trên quan điểm chương trình do bản thân mình tạo được reuse. Khi nghĩ về Extendibility và Reusability của chương trình của mình thì bước đầu tiên là phải nghĩ về “Extendibility và Reusability trong nội tại một chương trình”. Tạm thời không nghĩ đến việc “có thể sử dụng ở chương trình khác hay không?” hay “có thể làm Library common được không?” vội nhé. Trong chương trình mà các bạn viết có “những chỗ đang thực hiện cùng 1 xử lý” hay “những chỗ đang thực hiện xử lý tương tự” đúng không? Nếu có đoạn code nào như vậy thì hãy thử đặt một chút nghi vấn với những hoạt động trong thiết kế của bản thân bạn. Khi bạn viết chương trình, bạn chưa bao giờ “copy and paste” ư? Khi nghĩ “chỗ này nên làm thế nào nhỉ?” bạn có thể so sánh với code của người khác hay code mình đã viết trước đây và nhận ra rằng “À, mình đã code như vậy ở đây” rồi copy và paste vào phần đấy. Nếu như bạn đã thực hiện việc “copy and paste” xử lý tương tự từ chỗ khác vào để dùng thì trước tiên nên thử suy nghĩ về việc “xử lý mà mình copy lúc này là xử lý gì?” và “xử lý như thế nào?”. Đó có thể là “chọn ra URL nơi redirect từ response string nhận được từ server” hoặc “bỏ qua 1 box của MP4 file” hay “xác định giá trị đỉnh trong các phần của mẫu” cũng được. Nếu những xử lý đó không được cung cấp dưới dạng method của class và hơn nữa không thể thực hiện được bằng method call của Library tiêu chuẩn thì chẳng phải là “xử lý cái gì đó theo cách nào đó” ấy cần phải được cung cấp dưới dạng method của class hay sao? Ít nhất thì xử lý đó cũng được thực hiện ở chỗ gốc mà bạn đã copy và ở chỗ bạn paste nó vào thì xử lý đó cũng được thực hiện nên nó nên được cung cấp xử lý với tư cách là internal method thì tốt hơn đúng không?
Tôi nghĩ rằng có thể sẽ có những designer giàu kinh nghiệm không cho rằng phương pháp luận này là tốt nhất. Tôi cũng hoàn toàn hiểu được điều này nhưng trước đây tôi đã thấy nhiều ví dụ về việc những người trẻ tuổi thực hiện “copy and paste” với mục đích được mô tả dưới đây rồi tạo nên những đoạn code không thể maintain được. Vậy nên việc “Có cơ hội để chú thích” là vô cùng quan trọng
Vốn dĩ, tôi mong muốn các bạn có thể lọc ra những phần có thể làm thành method internal “ngay từ đầu” để thiết kế class. Nhưng để rèn luyện được kỹ năng đó thì cần có kinh nghiệm và sự học hỏi của bản thân các bạn. Thực ra, có nguyên tắc thiết kế phần mềm để làm được kỹ năng đó nên tôi muốn giới thiệu tới các bạn. Đó là nguyên tắc có tên là “Single Responsibility Principle”. Nói một cách đơn giản thì nguyên tắc đó là: Mỗi class phải có trách nhiệm với một phần chức năng đơn lẻ của phần mềm, và ngược lại, trược lạiệm đó được phụ trách chỉ bởi một class. Do đó: “không thể giải quyết được những rắc rối khi cùng một nội dung lại được xử lý ở các class hay method nằm rải rác khắp nơi” và “không được để các phần của cùng một xử lý nằm nhiều chỗ khác nhau”. (Oh, việc diễn đạt được ý nghĩa thật không đơn giản! Do đó có thể có ai đó thấy rằng: nghe có vẻ không đúng vậy!). Hãy xem xét ý: “không thể giải quyết được những rắc rối khi cùng một nội dung lại được xử lý ở các class hay method nằm rải rác khắp nơi”. Nếu trong cùng một method, bạn vừa thực hiện việc biến đổi, vừa thực hiện việc xử lý, rồi lại ép xử lý đó theo logic để kết thúc là sai lầm. Hay diễn đạt dễ hiểu hơn là cần phải phân chia rõ ràng ra (bằng các methods) phần nào phụ trách “biến đổi”, phần nào “thực hiện xử lý”, phần nào “làm cho phù hợp”. Như thế sẽ dễ hiểu hơn cho bạn. Ví dụ như đàn anh vào công ty trước, ngồi bên tay trái bạn sẽ làm công việc “biến đổi vấn đề thành một dạng thức thích hợp để có thể xử lý”, bạn làm công việc “thực hiện xử lý”, nữ đồng nghiệp bên phải phụ trách nhiệm vụ “làm cho vấn đề được xử lý trở nên nhất quán”. Project Manager của bạn muốn giải quyết được một issue nào đó thì đầu tiên sẽ gọi anh senior đó ra để nhờ chuyển đổi vấn đề, tiếp theo sẽ gọi bạn ra để xử lý vấn đề đó, cuối cùng là trao kết quả cho nữ đồng nghiệp để điều chỉnh theo logic. Project Manager không thể chỉ dẫn chi tiết về công việc của mỗi bạn được nhưng anh senior đó, bạn và nữ đồng nghiệp đó phải có trách nhiệm với công việc của bản thân mình. Bằng cách làm như vậy thì kể cả khi có thay đổi về spec ở phần “thực hiện xử lý” mà bạn phụ trách thì cũng chỉ cần bạn thay đổi phương pháp xử lý là xong (chỉ cần thay đổi method phụ trách việc xử lý là xong). Kể cả khi những người khác không biết gì thì chỉ cần bạn có thể vận hành xử lý tốt, tất cả sẽ đều tiến triển tốt! Quay trở lại câu chuyện, bạn đang copy and paste đoạn code có sẵn. Cụ thể, ví như bạn dạy lại việc của mình cho ai đó khác và sau đó người kia cũng làm công việc tương tự như bạn. Nếu làm như vậy, khi có thay đổi ở xử lý do bạn tạo, bạn cũng phải thông báo về việc chỉnh sửa cho người đó. Khi đó có thể phát sinh ra lỗi và bug. Hành động copy and paste này là một ví dụ tồi để minh họa về việc anti Single Responsibility Principle source code. Tất nhiện, trong thế giới hiện thực thì bạn là duy nhất. Muốn nhân bản một người như bạn lên để có thể đảm nhiệm được công việc tương tự (tăng Truck number) là điều không thể dù không phải là xấu. Nói cho cùng thì đó là việc mà tôi mong muốn thực hiện được. Tuy nhiên, trong chương trình máy tính, có thể tạo ra “bản sao của đối tượng là bạn” (có lẽ giống như kiểu instance hay gì đó) một cách đơn giản.
Một ví dụ về việc có thể thực hiện trong chương trình nhưng không thể áp dụng cho con người: 1: class Me { 2: method Xu_ly() { 3: // Thực hiện xử lý 4: }; 5: }; 6: 7: Me my_instance_no1 = new Me; 8: Me my_instance_no2 = new Me;
Một ví dụ về việc có thể là tốt về mặt con người nhưng không tốt xét về mặt chương trình: 1: class Me { 2: method Xu_ly() { 3: // Thực hiện xử lý 4: }; 5: }; 6: class MyPupil { 7: method Xu_ly() { 8: // Bản sao của quá trình xử lý 9: }; 10: }; 11: 12: Me my_instance_no1 = new Me; 13: MyPupil my_pupil_instance_no1 = new MyPupil;
Nếu viết như vậy. tôi nghĩ các bạn sẽ hiểu ngay rằng xét về mặt chương trình thì nó rất lãng phí. Nếu phần copy and paste là cả method hay class thì có thể bạn sẽ không làm nhưng nếu đoạn code đó là là một phần của method thì hẳn bạn sẽ làm. Kể cả là một kỹ sư có kinh nghiệm, nếu đặt tay lên ngực nghĩ lại thì chắc cũng không phải chỉ mới một, hai lần mắc sai lầm như thế. (Thực xấu hổ nhưng tôi cũng có nhiều lần làm như vậy rồi!). Tuy nhiên, ngày nay, chúng ta hãy khắc ghi quan niệm rằng “việc tạo mới một đoạn code giống hệt đoạn code khác nhờ vào copy and paste thì thực tế, dù đó chỉ là một phần của method cũng là cách làm tồi nhất”. ”Một ví dụ” ở đây là có một vụ việc như thế này đã xảy ra, mà nguyên nhân chỉ đơn giản là do copy and paste. Ví dụ việc “do đã tìm thấy xử lý tương tự nên bắt chước cách làm để tạo ra một đoạn code chỉ khác một chút” chắc hẳn không phải là ít gặp trong thâm niên coding của bản thân các bạn nhỉ? Có thể trường hợp này xảy ra với người khác, không phải là bạn trong thực tế. Ở đây sẽ gây ra phiền toái cho người khác chứ không chỉ là copy and paste. Dù làm cùng một việc nhưng chỉ cần cách viết khác nhau một chút thôi có thể sẽ không thấy được là “đang làm cùng một việc”. Nếu để thành như vậy thì sẽ phát sinh tình trạng “Bug đã fix rồi nhưng chỉ cần thay đổi trình tự một chút sẽ lại phát sinh bug tương tự.” Đúng vậy, đó chính là nơi mà bạn sẽ thấy hay gặp vấn đề “Reactivate Bug report” nhất. Bây giờ hẳn là bạn đang nghĩ: “Chương trình cũng đã chạy được ở một mức độ nhất định rồi nên refactoring xem sao?” Đúng là cũng có cách làm như vậy. Nhưng nên thay đổi cách suy nghĩ phụ thuộc hoàn toàn vào refactoring như vậy đi. Trong dự án bạn từng làm trước đây, lúc còn dư dả thời gian trong lịch trình, bạn có thể đã thong thả refactoring nhỉ? Nhưng ở thời điểm cao điểm của dự án, chắc là bạn đã từng ở vào thế “tiến thoái lưỡng nan” bất đắc dĩ phải overnight để refactoring phải không? Ở đây, điều tôi muốn nhắn nhủ đến các bạn là “Trước khi thực hiện copy and paste, hãy luôn ý thức về reusability của code trong dự án để xử lý những vấn đề về thiết kế và code. Cho dù bạn có nói là sẽ refactor sau nhưng hầu như sau đó không có cơ hội nào cho bạn làm việc đó nữa. Người phụ trách thiết kế chỉ có thể nói đến việc “Cải thiện design bằng refactoring” khi bình thường anh ta luôn ý thức về Reusability!
Tôi nghĩ những độc giả tinh ý hẳn là cũng sắp nhận ra rằng: Ý chính của bài báo không hề đi theo lý luận của “Exendibility” và “Reusability”. Đến tận đây cũng chưa có chữ nào về “Extendibility” cả mà ngay cả về “Reusability” thì cũng chỉ mới “đi loanh quanh” ở “trạm thu phí” ngoài cổng vào mà thôi. Đúng vậy, hẳn là các bạn cũng hiểu ý nghĩa của tiêu đề bài báo ”The Utopia of Reuse”. Bây giờ, chúng ta đang nhìn thấy viễn cảnh lý tưởng về “Reuse” còn đang ở một chân trời xa xăm, nơi mà chúng ta chưa thể đi đến trong hiện thực, giống như một giấc mơ vậy. Mà có khi nơi ấy vốn chẳng tồn tại trên thực tế.
Không, không có chuyện như vậy đâu. Chúng ta sẽ sớm đến được bến bờ lý tưởng nơi những “đóa hoa” “Extendibility” và “Reusability” nở rộ. Chắc hẳn con đường chúng ta đi không hề bằng phẳng nhưng không phải là không thể đi được. Và việc “suy nghĩ lại về copy and paste” chính là bước đầu tiên trên con đường ấy. Lần sau, chúng ta hãy cùng tiếp tục hành trình thực sự tới “miền lý tưởng” của Reuse!
All rights reserved