4 Nguyên tắc Thiết kế Phần mềm tôi đã học được một cách 'đau đớn'

0 0 0

Người đăng: Coding Cat

Theo Viblo Asia

Nếu có hai 'nguồn sự thật'(source of truth), thì một cái gần như chắc chắn sai. Và vâng, cứ thoải mái lặp lại đi.

Tháng trước, tôi cuối cùng cũng cho ra mắt thành công một service quy mô lớn do mình tự tay xây dựng và thiết kế. Trong quá trình thiết kế và triển khai, tôi nhận thấy danh sách các "quy tắc" dưới đây cứ lặp đi lặp lại trong nhiều tình huống khác nhau.

Những quy tắc này phổ biến đến mức tôi dám khẳng định rằng ít nhất một trong số chúng sẽ hữu ích cho một dự án mà bất kỳ kỹ sư phần mềm nào đọc bài này đang thực sự làm việc. Còn nếu bạn chưa thể áp dụng ngay bây giờ, tôi hy vọng những nguyên tắc này sẽ là một bài tập tư duy thú vị để bạn có thể bình luận hoặc trực tiếp phản biện bên dưới.

Một điều tôi muốn lưu ý ở đây là, dĩ nhiên — mỗi "nguyên tắc" đều có thời điểm và hoàn cảnh áp dụng riêng. Sự tinh tế (nuance) luôn là điều cần thiết. Đây là những kết luận mà tôi thường có xu hướng hướng đến nói chung, bởi lẽ rất nhiều khi, điều ngược lại lại là mặc định mà tôi thường thấy khi review code hay đọc các bản design docs.


1. Duy trì một "source of truth" duy nhất.

Nếu có hai "nguồn sự thật", một cái gần như chắc chắn sai. Nếu chưa sai, thì... rồi cũng sẽ sai thôi.

Về cơ bản, nếu bạn đang cố gắng duy trì một phần state ở hai vị trí khác nhau trong cùng một service... thì đừng làm thế. Tốt hơn hết là hãy cố gắng tham chiếu cùng một state bất cứ khi nào có thể. Ví dụ, nếu bạn đang phát triển một ứng dụng frontend và có số dư ngân hàng được lấy từ server, tôi đã gặp quá nhiều lỗi sync bugs đến mức tôi luôn muốn lấy số dư đó từ server. Nếu có một số dư nào đó được "derive" (tính toán) từ số dư gốc, chẳng hạn như "số dư khả dụng" (spendable balance) so với "tổng số dư" (total balance) (ví dụ, một số ngân hàng yêu cầu bạn duy trì số dư tối thiểu), thì "số dư khả dụng" đó nên được tính toán "on-the-fly" (tức thì) thay vì lưu trữ riêng. Nếu không, bạn sẽ phải cập nhật cả hai số dư mỗi khi có giao dịch phát sinh.

Nhìn chung, nếu có một mảnh dữ liệu được "derive" từ một giá trị khác, thì giá trị đó nên được "derive" chứ không nên lưu trữ. Việc lưu trữ giá trị đó sẽ dẫn đến các lỗi đồng bộ hóa (synchronization bugs). (Vâng, tôi biết điều này không phải lúc nào cũng khả thi. Luôn có những yếu tố khác tác động, chẳng hạn như chi phí của việc "derive". Suy cho cùng, đây là một sự đánh đổi - tradeoff.)

2. Please Repeat Yourself

Chúng ta đã nghe nhiều về DRY (Don't Repeat Yourself) rồi, và bây giờ tôi xin giới thiệu với các bạn PRY (Please Repeat Yourself).

Rất nhiều lần tôi đã thấy những đoạn code nhìn tương tự nhau lại cố gắng được trừu tượng hóa (abstracted out) thành một class "tái sử dụng được". Vấn đề là, cái class "tái sử dụng được" này cứ thế được thêm một method, rồi một constructor đặc biệt, rồi thêm vài method nữa, cho đến khi nó trở thành một "Frankenstein" khổng lồ của code, phục vụ nhiều mục đích khác nhau và mục đích ban đầu của việc trừu tượng hóa không còn tồn tại nữa.

Một hình ngũ giác có thể trông tương tự như một hình lục giác, nhưng vẫn có đủ sự khác biệt để chúng hoàn toàn không giống nhau.

Tôi cũng từng "mắc tội" dành quá nhiều thời gian cố gắng biến mọi thứ trở nên tái sử dụng được, trong khi một chút lặp lại code (code duplication) vẫn hoạt động hoàn hảo. (Vâng, bạn sẽ phải viết nhiều test hơn và nó không "gãi đúng chỗ ngứa" của việc refactoring, nhưng thôi kệ đi.)

3. Đừng lạm dụng mocks.

Mocks. Tôi có một mối quan hệ "yêu-ghét" với mocks. Câu nói tâm đắc nhất của tôi từ một cuộc thảo luận trên Reddit về bài đăng này là: "Với mocks, chúng ta đánh đổi độ chính xác của test để đổi lấy sự dễ dàng khi viết test."

Mocks rất tuyệt vời khi tôi cần viết unit tests để test nhanh một thứ gì đó mà không muốn "đụng chạm" đến code "chuẩn prod". Mocks lại không tuyệt chút nào khi prod bị lỗi, bởi vì hóa ra — một thứ bạn đã mock bị hỏng ở tầng sâu hơn trong stack, mặc dù "tầng sâu hơn trong stack" đó thuộc sở hữu của một team khác. Điều đó không quan trọng, vì service của bạn bị lỗi nên đó là trách nhiệm của bạn phải sửa nó.

Viết test rất khó. Ranh giới giữa unit tests và integration tests mờ nhạt hơn bạn nghĩ. Việc biết cái gì nên mock và cái gì không nên mock là rất chủ quan.

Việc tìm ra lỗi trong quá trình phát triển tốt hơn nhiều so với việc tìm thấy chúng khi đã ở trên môi trường prod. Khi tôi tiếp tục viết phần mềm, tôi cố gắng tránh xa mocks nếu có thể. Việc các bài test có phần "nặng đô" hơn một chút hoàn toàn xứng đáng để đổi lấy độ tin cậy cao hơn nhiều. Nếu code reviewer của tôi thực sự yêu cầu mocks, tôi thà viết nhiều test hơn (và thậm chí có thể là redundant) còn hơn là bỏ qua việc viết test. Ngay cả khi tôi không thể sử dụng một dependency thật trong test, tôi vẫn sẽ thử các lựa chọn khác trước khi dùng mocks, chẳng hạn như một local server.

Tựa đề bài viết "Testing on the Toilet" của Google có một ghi chú đáng chú ý về điều này từ năm 2013. Họ lưu ý rằng việc lạm dụng mocks gây ra các vấn đề sau:

  • Tests có thể khó hiểu hơn vì giờ đây bạn có thêm đoạn code này mà người khác phải hiểu, cùng với code sản xuất thực tế.
  • Tests có thể khó bảo trì hơn vì bạn phải "chỉ dẫn" cho một mock cách cư xử, điều này đồng nghĩa với việc bạn làm lộ các chi tiết triển khai (implementation details) vào test của mình.
  • Nhìn chung, các bài test cung cấp ít sự đảm bảo hơn vì độ tin cậy của phần mềm bạn giờ đây chỉ được đảm bảo NẾU các mocks của bạn hoạt động giống hệt như các triển khai thực tế (real implementations) của bạn (điều này khó đảm bảo và thường xuyên bị mất đồng bộ).

4. Hạn chế tối đa mutable state.

Máy tính cực kỳ NHANH. Trong "cuộc chơi tối ưu hóa" (optimization game), việc ngay lập tức thêm caching và lưu trữ mọi thứ vào database là cực kỳ phổ biến. Tôi nghĩ đây có lẽ là trạng thái cuối cùng của hầu hết các sản phẩm và dịch vụ phần mềm thành công. Dĩ nhiên, hầu hết các service sẽ cần một loại state nào đó, nhưng điều quan trọng là phải tìm ra cái gì thực sự cần thiết về mặt lưu trữ so với cái gì có thể được "derive" "on-the-fly".

Trong "phiên bản v1" của một thứ gì đó, tôi nhận thấy rằng việc hạn chế tối đa mutable state sẽ giúp bạn tiến xa. Nó cho phép bạn phát triển nhanh hơn vì bạn không phải lo lắng về sync bugs, dữ liệu xung đột (conflicting data) và stale state. Nó cũng giúp bạn phát triển từng chức năng nhỏ (piece-by-piece) thay vì giới thiệu quá nhiều cùng một lúc. Máy móc ngày nay đủ nhanh để thực hiện một vài phép tính redundant hoàn toàn không sao cả. Nếu máy móc được cho là sắp "thay thế chúng ta", thì chúng thừa sức xử lý thêm vài "đơn vị công việc" tính toán nữa.

Bình luận

Bài viết tương tự

- vừa được xem lúc

Hãy sử dụng ESLint cho dự án của bạn!

. Bài viết gốc: https://manhhomienbienthuy.bitbucket.io/2018/May/20/we-should-use-eslint-in-project.html (đã xin phép tác giả ).

0 0 83

- vừa được xem lúc

Code đạo - Đạo của người viết code (6 Đạo quan trọng nhất)

Môn cờ tướng có một thể loại bài học mở đầu rất hay, gọi là "Kỳ đạo", đại ý cũng như "Tiên học lễ - Hậu học văn" - trước khi học đánh cờ thì ông phải học về đạo cờ trước đã. Vậy nếu có món "Code đạo"

0 0 65

- vừa được xem lúc

Coding Conventions và các chuẩn viết code trong PHP

1. Convention là gì .

0 0 93

- vừa được xem lúc

SOLID - nguyên lý bạn nên biết để trở thành dev có tâm, có tầm

Nguyên tắc SOLID trong lập trình là một tập hợp các nguyên tắc thiết kế phần mềm. Các nguyên tắc này giúp tạo ra mã nguồn linh hoạt, dễ bảo trì và mở rộng về sau.

0 0 18