5 lessons in object-oriented design from Sandi Metz
https://18f.gsa.gov/2016/06/24/5-lessons-in-object-oriented-design-from-sandi-metz/ Lần đầu tiên tôi được nghe Sandi Metz nói chuyện tại một buổi họp tại San Francisco vào năm 2012. Một trong số những điều cô ấy đã nói tại buổi họp đó đã có tác động sâu sắc đến tôi: "Ngày nay, code chỉ cần ...
https://18f.gsa.gov/2016/06/24/5-lessons-in-object-oriented-design-from-sandi-metz/
Lần đầu tiên tôi được nghe Sandi Metz nói chuyện tại một buổi họp tại San Francisco vào năm 2012. Một trong số những điều cô ấy đã nói tại buổi họp đó đã có tác động sâu sắc đến tôi: "Ngày nay, code chỉ cần làm việc một lần, nhưng cần phải được dễ dàng thay đổi mãi mãi. "
Tại thời điểm đó, tôi muốn viết một mã code có thể sử dụng được trong một vài năm và hiểu rằng refactoring là một ý tưởng tốt. Nhưng cũng như nhiều bài học khác mà tôi đã được học như người mới bắt đầu, tôi đã không thực sự hiểu được lý do tại sao lại là refactoring. Và điều nó có được đơn giản có thể là: Code cần phải được refactored để nó có thể dễ dàng thay đổi hơn trong tương lai.
Nếu một chương trình tôi viết mà không bao giờ cần thay đổi thì refactoring sẽ là một điều vô nghĩa. Nhưng trên thực tế, mọi thứ luôn luôn thay đổi. Trong thế giới phần mềm, con người luôn muốn thay đổi, ví dụ thêm, xóa, sửa một chức năng nào đó. Và đó chính là những điều mà ta cần viết các mã code. Vì vậy chúng ta cần phải refactor. Chúng ta không thay đổi để làm nó tốt hơn, mà chúng ta thay đổi để làm nó trở nên dễ dàng thay đổi hơn trong tương lai.
Tháng trước tôi đã hoàn thành khóa học "Sandi's object-oriented design". Đó là 3 ngày học tập với cường độ rất cao thông qua các bài tập về refactoring và thảo luận về các mã code theo các nhóm với lớp học của tôi là khoảng 30 sinh viên. Tôi đã thu được rất nhiều kiến thức bổ ích từ khóa học và muốn chia sẽ với mọi người top lessons mà tôi học được.
1. Mục đích của thiết kế là để giảm chi phí thay đổi
Bài học này tương tự với ý tưởng refactoring code chính vì thế nó có thể thay đổi mãi mãi, nhưng nó tập chung vào phần business value của việc làm cho code dễ dàng thay đổi. Rất nhiều người nghĩ rằng refactoring như là một bonus step hoặc là một thứ gì đó mà họ có thể làm khi mà họ có thời gian (thường được cho trong backlog chẳng hạn, khi nào có thời gian mới làm). Nhưng trên thực tế việc refactoring liên tục lại là mục tiêu trung tâm của bất kì một tổ chức sản xuất phần mềm nào đó. Nếu không refactoring bạn có thể sẽ không có được phần mềm với thiết kế tốt nhất. Với một phần mềm được thiết kế tốt, bạn sẽ dễ dàng để thay đổi. Những điều đó sẽ giúp bạn dễ dàng thay đổi và đương nhiên sẽ cần ít thời gian hơn khi có sự thay đổi. Và ít thời gian có nghĩa là chi phí sẽ giảm đi.
Một số người có thể nghĩ rằng: "Refactoring là mất thời gian, tốt hơn là tôi có thể dành thời gian đó để cung cấp một tính năng nào đó trong tương lai". Trong một số trường hợp như Sandi đã nói thì đó là đúng. Nếu như business của bạn sẽ thất bại vào ngày mai nếu bạn không cung cấp một feature nào đó trong hôm nay, sau đó bằng mọi cách bạn có thể viết một số crappy code và deploy chúng trên production. Nhưng nếu bạn làm việc trên codebase có nghĩa là code của bạn có thời gian sử dụng vô hạn trong tương lai, chính vì cậy bạn cần tập trung vào những hậu quả lâu dài của hành động ngày hôm nay. Nếu bạn viết một mã code với design kém để tiết kiệm thời gian ngay bây giờ, bạn cần phải tự hỏi mình rằng: "bạn có đang thực sự tiết kiệm tiền cho tổ chức bằng cách cung cấp các tính năng nhanh hơn, hoặc khiến họ xuống dòng nhiều hơn bằng cách viết các mã code mà nó khó có thể thay đổi?"
2. Tiếp cận với hanging green thấp nhất
Trong bài tập đầu tiên của khóa học, Sandi yêu cầu chúng tôi viết một Ruby script để làm cho một file của automated tests pass. Script này cần hát bất kì câu nào đó của bài hát "99 bottles of beer on the wall" với number của câu đó.
Tôi cho rằng mình là một người tốt với việc làm mọi thứ theo cách "dễ dàng". Tôi luôn luôn nói rằng "đầu tiên làm cho nó làm việc trước sau đó sẽ làm cho nó tốt hơn". Vì vậy tôi bắt đầu bằng cách viết code để làm cho câu đầu tiên trong bài hát chạy. Sau đó tôi bắt đầu viết code cho câu 1, nơi mà tôi đã dùng một số suy diễn logic để xác định số lượng bottle và cần lưu ý rằng từ "bottles" cần phải được singularized để trở thành "bottle" vì vậy tôi sẽ viết một số điều kiện cho cho điều đó. Phần thứ hai của câu đó cũng khác nhau, nên tôi đã viết một số condition khác cho nó. Sau đó tôi đến câu "zero" và nó cần một condition khác. Và sau đó nhiều điều khác nữa.
Không lâu sau đó, tôi đã có một method với 40 dòng code và nó khá là lộn xộn. Và đây là code cho một bài hát đơn giản. Và tôi nghĩ rằng nó được viết theo cách dễ nhất có thể.
class Bottles def verse(number) if number - 1 == 0 next_number = "no more" else next_number = number - 1 end if number - 1 == 1 next_bottle = "bottle" else next_bottle = "bottles" end if number == 1 bottle = "#{number} bottle" elsif number == 0 bottle = "no more bottles" else bottle = "#{number} bottles" end if number == 1 pronoun = "it" else pronoun = "one" end if number == 0 second_verse = "Go to the store and buy some more, " + "99 bottles of beer on the wall. " else second_verse = "Take #{pronoun} down and pass it around, " + "#{next_number} #{next_bottle} of beer on the wall. " end "#{bottle.capitalize} of beer on the wall, " + "#{bottle} of beer. " + second_verse end end
** Thời gian cho shameless green **
Khi chúng tôi hoàn thành bài tập này, Sandi dạy chúng tôi khái niệm "shameless green": Làm những điều dễ nhất có thể để làm cho bài test của bạn pass (chuyển sang mầu xanh). Tình trạng này được gọi là "shameless" green bởi vì mục tiêu của bạn là làm cho test pass, không có gì hơn nữa. Bạn có thể cảm thấy xấu hổ bởi mã code của bạn viết ra có vẻ quá đơn giản khi được viết bởi một người nào đó với trình độ thông minh của bạn. Sau đó và chỉ sau đó bạn mới có được logic trừu tượng của nó.
Trong trường hợp của 99 bottles, shameless green có nghĩa là không cần phải lo ngại về việc tưởng tượng logic cho từng câu, mà không giống với những câu khác. Thay vào đó nó sử dụng một condition để in các string khác nhau:
class Bottles def verse(number) case number when 0 "No more bottles of beer on the wall, no more bottles of beer. Go to the store and buy some more, 99 bottles of beer on the wall. " when 1 "1 bottle of beer on the wall, 1 bottle of beer. Take it down and pass it around, no more bottles of beer on the wall. " when 2 "2 bottles of beer on the wall, 2 bottles of beer. Take one down and pass it around, 1 bottle of beer on the wall. " else "#{number} bottles of beer on the wall, #{number} bottles of beer. Take one down and pass it around, #{number-1} bottles of beer on the wall. " end end end
Các developers thích viết những mã code thông minh, và trong trường hợp của bài tập về 99 bottles, tôi cũng không ngoại lệ. Thay vì nó với bản thân mình rằng "OK, các câu 1 và 0 là khác nhau, tôi sẽ in ra những chuỗi khác nhau trong những trường hợp đó." Ngay lập tức tôi đã cố gằng dùng cùng một chuỗi với phép nội suy cho chúng có thể làm việc với tất cả các câu. Kết quả là một bản phác thảo với thiết kế kém và gần như là không thể hiểu được.
Khi bạn bắt đầu với shameless green, bạn bắt đầu với một cái gì đó mà có thể là rất nhiều sự trùng lặp nhưng sẽ rất dễ hiểu. Tại thời điểm đó, bạn có thể xem xét lại các feature đã hoàn thành. Điều này có vẻ trái ngược với quan điểm đầu tiên về việc viết một mã code với design tốt, nhưng mà không phải vậy. Cách mà tôi bước đầu tiếp cận với vấn đề là một giải pháp khiếm nhã nhưng khó hiểu. Shameless green là một giải pháp khiếm nhã nhưng mà dễ hiểu. Từ đó bạn sẽ có cái nhìn tổng quan hơn về chức năng bạn làm => cải thiện dần dần và sẽ tìm ra được thiết kễ mã code tốt nhất.
3. Trùng lặp code là rẻ hơn so với việc trừu tượng sai
Một lý do rất khó để bắt đầu với shameless green là nó thường include một số lượng lớn của duplication. Hãy suy nghĩ về nó theo cách này: Một cách để giải quyết vấn đề 99 bottles là viết một method với 220 dòng và trả về một chuỗi hoàn toàn khác nhau cho mỗi số truyền vào. Lần đầu tiên tôi bắt tay vào vấn đề, tôi đã đi theo hướng hoàn toàn ngược lại: zero duplication (không có sự trùng lặp). Và ngay lập tức code của tôi trở nên khó hiểu.
Khi nói đến duplication, DRYing code quá sớm sẽ làm cho việc thiết kế trở nên khó khăn hơn. Và dưới đây là lý do: Khi bạn cố gắng loại bỏ sự trùng lặp, có nghĩa là bạn đang tạo ra một khái niệm trừu tượng. Cũng giống như nỗ lực đầu tiên của tôi tại bài tập "bottles", tôi đã tạo ra các điều kiện khác nhau để trả về các giá trị dựa trên các số được truyền vào. Để sử dụng những giá trị đó trong chuỗi string, tôi đã cung cấp cho họ một cái tên. Chính vì vậy tôi đã đặt tên giá trị đó thậm chí trước khi tôi hiểu chúng là gì.
Dưới đây là ví dụ: trong bài hát "99 bottles" , câu bắt đầu với một số và só đó sẽ giảm một khi đi xuống phần thứ 2 của câu. Vì vậy tôi viết một điều kiện "giảm số xuống một" và đặt tên là next_number. Khi bạn nhận được câu thơ "1", => next_number trở thành chuỗi "No more", và "No more" thì không phải là một số. Định nghĩa một method name trước khi tôi thực thi shameless green, tôi đã đưa ra một cái tên không chính xác cho một khái niệm.
Bạn có thể tranh luận rằng một cái gì đó sai tên không phải là vấn đề lớn. Chỉ cần đổi tên method phải không? Có 2 câu trả lời: Có hoặc không. Có, method này có thể được đổi tên. Không, việc đặt tên sớm không có hậu quả gì tai hại cả. Việc chuyển từ một khái niệm trừu tượng đến một khái niệm trừu tượng khác là khó khăn hơn nhiều so với việc di chuyển code bình thường (như là duplication) đến một khái niệm trừu tượng. Khi chúng tôi thấy tất cả các yếu tố trùng lặp ngay trước mặt, chúng tôi có một cơ hội tốt để đến với một cái tên thích hợp cho sự trùng lặp đó. khi chúng tôi đặt tên cho nó, có một phần ba là không phù hợp đó là khó khăn để tua lại suy nghĩ của chúng tôi và thực thi chúng với abstraction. Thay vào đó, chúng tôi có khả năng thử và đặt những trường hợp sử dụng mới vào abstraction đã tồn tại (để nguyên cái cũ, cái mới sẽ thực thi trong abstraction đã tồn tại) => Làm cho code trở nên khó hiểu và khó thay đổi trong tương lai.
Loại bỏ duplication không phải luôn là một sự lựa chọn sai. Ví dụ, trong ví dụ shameless green bottles, 97 trong số 100 câu có thể làm việc được trong điều kiện "else" (chúng tôi không cần phải viết tất cả điều kiện cho 100 câu). Nhưng xóa duplication thì không phải lúc nào cũng tốt. DRY là một trong những khái niệm đầu tiên bạn được học khi bắt đầu coding. Nhưng những gì tôi được học từ Sandi's workshop là có rất nhiều trường hợp có code trùng lặp tốt hơn là việc không có sự trùng lặp với mã code khó hiểu và khó thay đổi trong tương lai.
4. Refactoring code cần phải được an toàn và khá nhàm chán
Phương thức của Sandi phụ thuộc vào việc có một bộ test case cho bạn biết nếu test code của bạn đang làm việc như bạn mong muốn. Rất nhiều developers đều biết câu ngạn ngữ "red, green, refactor". Nó có nghĩa là bạn bắt đầu với một test fail (red), sau đó viết code để làm cho các test case pass (chuyển sang trạng thái green), sau đó thay đổi code để làm nó tốt hơn (refactor).
Thông thường khi nhận việc refactor thì tôi sẽ thực hiện một số thay đổi nhẹ sau đó chạy lại test. Vào thời điểm đó, thông thường những thay đổi của tôi sẽ phá vỡ một cái gì đó đã làm việc trước đây. Và bây giờ tôi sẽ trở thành một thám tử để khám phá ra những gì tôi đã làm để gây ra sự phá vỡ đó. Quá trình này đôi khi là mạo hiểm và nhiều lúc cũng khá thú vị. Mạo hiểm vì tìm ra những gì tôi thực hiện mà phá vỡ những chức năng đã chạy trước đó là mất thời gian hơn là việc làm cho những thay đổi bị phá vỡ ở nơi đầu tiên. Thú vị vì tôi viết rất nhiều code, nó rất thú vị, trong khi chạy test mọi thứ đều rất thú vị. ** "Red, green, infinity green" ** Trong lớp học hướng đối tượng của Sandi, chúng tôi đi theo một mô hình nghiêm ngặt khi refactoring, nó được gọi là "red, green, infinity green". Trong quá trình này, chúng tôi bắt đầu với những test thất bại (trạng thái red), sau đó làm cho nó pass(trạng thái green) và sau đó là refactor. Nhưng trong quá trình refactor, chúng tôi sẽ chạy lại test sau mỗi lần refactor. Điều này có nghĩa là bạn không thể chuyển code sang vị trí mới và xóa code ở vị trí cũ và sau đó chạy lại test. Đầu tiên bạn phải copy code và paste nó vào trong vị trí mới, sau đó bạn chạy lại test. Nếu tất cả đều pass, bạn có thể remove các code cũ đi. Sau đó bạn chạy lại test, và nó vẫn pass bạn có thể chuyển sang những thay đổi tiếp theo. Nếu tại bất kì thời điểm nào test fail, bạn hoàn tác nó về thay đổi cuối cùng và chắc chắn rằng các test pass một lần nữa, sau đó bạn thực thi một thay đổi mới và tiếp tục quá trình thay đổi vòng lặp cho đến khi tất cả các test case đều pass. Điều này nghe có vẻ nhàm chán, nhưng nó là an toàn, chính vì thế quá trình này được Sandi gọi là "an toàn và nhàm chán". Nhưng chính sự an toàn và nhàm chán đó lại có một giá trị to lớn. Thứ nhất, bạn luôn biết bạn thay đổi những gì làm cho test của bạn thất bại qua đó có thể dễ dàng sửa đổi, không giống như khi bạn đã hoàn thành việc refactor, test fail và bạn không biết được vì sao mà nó fail. Hơn nữa việc sửa đổi một chút một và chạy test ngay lúc đó bạn có thể kiểm soát được những gì bạn làm, và rủi ro cho việc refactor trở nên thấp nhất. Trong trường hợp refactor lớn, test fail lúc đó có khả năng xảy ra là hủy bỏ toàn bộ những gì bạn làm vì có thể không biết được bạn đã làm gì sai.
Không phải tất cả việc viết code là an toàn và nhàm chán. Nhưng việc refactoring là một "infinity green" state, bạn phải luôn chắc chắn rằng có nhiều thời gian và năng lượng cho các task thú vị.
5. Viết mã code tốt nhất có thể ngày hôm nay và để nó hoàn toàn tự do và nó có thể sẵn sàng được xóa vào ngày hôm sau
Thực tế: developers dành rất nhiều thời gian để thay đổi mã code hiện có hơn là việc họ viết mới từ đầu. Trong khi chúng tôi dành nhiều thời gian để suy nghĩ về tính những tính năng mới thú vị hơn thì thực tế hầu hết thời gian đó chúng tôi đều dành cho việc thay đổi những thứ đã có sẵn. Ngay cả những người mới làm việc trên codebase thì cũng hiếm khi thấy họ viết code mà không cần thay đổi code đã có sẵn. Thông thường mã hiện tại đã thể hiện một cái gì đó mà chúng ta đã viết. Có lẽ được viết vài tháng trước, vài ngày trước hay thậm chí là vài giờ trước.
Để trở thành một object-oriented design chuyên nghiệp như Sandi Metz, bạn phải viết code hoàn toàn không có gì đính kèm (code được viết một cách độc lập). Chúng tôi đã nghĩ rằng "Tôi đã dành rất nhiều thời gian tâm huyết để viết ra nó, nó đã chạy, rất đẹp và tôi không thể xóa nó đi". Nhưng mà yêu cầu thay đổi có nghĩa là code của bạn cần phải thiết kế khác. Không có những điều như là perfect code, chỉ là code để phục vụ cho việc chạy trong ngày hôm nay. Bạn không thể nào đoán trước được nhu cầu của ngày mai. Bạn cũng không cần phải xây dựng abstraction quá sớm. Nhưng một khi yêu cầu mới đến mà đòi hỏi bạn phải hoàn toàn suy nghĩ một giải pháp mới bạn cần phải suy nghĩ lại thiết kế để có được mã code có thể dễ dàng thay đổi mãi mãi.