Cách tránh thiết kế giải pháp quá phức tạp
- Xác định đúng vấn đề trước khi thiết kế giải pháp
- Phân biệt yêu cầu thực tế với giả định về tương lai
- Đánh giá độ phức tạp bằng giá trị mà nó tạo ra
- Bắt đầu từ giải pháp nhỏ nhất đáp ứng đầy đủ nhu cầu
- Kiểm chứng trước khi đầu tư vào những phần phức tạp
- Ưu tiên khả năng thay đổi thay vì cố dự đoán mọi thay đổi
- Chỉ tối ưu khi đã xác định được nút thắt thực sự
- Đặt giới hạn rõ ràng cho tính linh hoạt và khả năng mở rộng
- Dùng tiêu chí loại bỏ để kiểm soát thiết kế
- Chấp nhận độ phức tạp khi chính vấn đề đòi hỏi nó
Vì vậy, cách tránh một giải pháp quá phức tạp không phải là luôn chọn phương án đơn giản nhất. Cần phân biệt giữa độ phức tạp cần thiết và độ phức tạp do dự đoán, phòng xa hoặc tối ưu quá sớm. Nếu bỏ bớt một thành phần làm giải pháp không còn đáp ứng yêu cầu quan trọng, thành phần đó có lý do tồn tại. Ngược lại, nếu nó chủ yếu phục vụ một nhu cầu chưa được xác nhận, giá trị của nó cần được xem xét lại.
Xác định đúng vấn đề trước khi thiết kế giải pháp
Nhiều giải pháp trở nên phức tạp ngay từ đầu vì phạm vi của vấn đề chưa được xác định rõ. Khi câu hỏi ban đầu còn mơ hồ, người thiết kế thường cố bao phủ nhiều trường hợp để tránh bỏ sót. Kết quả là phạm vi giải pháp lớn hơn phạm vi vấn đề thực tế.
Trước khi lựa chọn công nghệ, cấu trúc hay quy trình, cần làm rõ ba yếu tố: kết quả cần đạt, đối tượng chịu tác động và điều kiện mà giải pháp phải đáp ứng. Một yêu cầu chỉ nên được đưa vào thiết kế khi có thể chỉ ra nó hỗ trợ kết quả nào.
Ví dụ, nếu mục tiêu thực tế chỉ là giúp một nhóm nhỏ theo dõi trạng thái công việc, một hệ thống với nhiều tầng phân quyền, quy trình phê duyệt phức tạp và khả năng tùy biến cho hàng chục loại tổ chức có thể vượt xa nhu cầu hiện tại. Các khả năng đó không sai về mặt kỹ thuật; vấn đề nằm ở chỗ chưa có đủ nhu cầu để biện minh cho chi phí của chúng.
Một câu hỏi hữu ích là: nếu bỏ thành phần này đi, mục tiêu chính có còn đạt được không? Nếu câu trả lời là có, thành phần đó chưa chắc cần xuất hiện trong phiên bản hiện tại.

Phân biệt yêu cầu thực tế với giả định về tương lai
Một nguồn phổ biến của overengineering là thiết kế cho những nhu cầu “có thể sẽ xuất hiện”. Khả năng mở rộng, cấu hình linh hoạt hay kiến trúc tổng quát đều có giá trị khi chúng phục vụ một yêu cầu đủ chắc chắn. Nhưng chúng cũng tạo thêm mã nguồn, trạng thái, phụ thuộc, điểm lỗi, tài liệu và công việc bảo trì.
Vấn đề không phải là hoàn toàn bỏ qua tương lai. Cần phân biệt ba nhóm nhu cầu:
· Nhu cầu đang tồn tại và phải được giải quyết
· Nhu cầu tương lai có căn cứ đủ mạnh để ảnh hưởng quyết định hiện tại
· Khả năng giả định nhưng chưa có bằng chứng cho thấy cần hỗ trợ
Hai nhóm đầu có thể tác động đến thiết kế. Nhóm cuối nên được ghi nhận như một giả định thay vì tự động biến thành chức năng hoặc kiến trúc.
Điểm quan trọng là chi phí của một tính năng chưa dùng không kết thúc khi tính năng đó được xây xong. Nó còn phải được hiểu, kiểm thử, bảo trì và xem xét mỗi khi hệ thống thay đổi. Vì vậy, “làm sẵn để sau này dùng” chỉ hợp lý khi chi phí làm sớm thực sự thấp hơn đáng kể so với việc bổ sung sau này và xác suất nhu cầu xuất hiện đủ cao.
Đánh giá độ phức tạp bằng giá trị mà nó tạo ra
Không nên đánh giá một thiết kế chỉ bằng số lượng thành phần. Một giải pháp nhiều thành phần vẫn có thể hợp lý nếu mỗi thành phần giải quyết một ràng buộc quan trọng. Ngược lại, một giải pháp nhìn có vẻ gọn vẫn có thể phức tạp nếu người sử dụng phải thực hiện nhiều bước hoặc đội vận hành phải xử lý nhiều ngoại lệ.
Mỗi phần bổ sung nên được cân đối giữa giá trị và chi phí phức tạp.
Giá trị có thể đến từ việc:
· Đáp ứng một yêu cầu bắt buộc
· Giảm một rủi ro đáng kể
· Loại bỏ thao tác hoặc chi phí có ý nghĩa
· Đáp ứng tải, độ tin cậy hoặc mức an toàn đã xác định
· Giải quyết một trường hợp sử dụng có bằng chứng thực tế
Trong khi đó, chi phí phức tạp không chỉ là thời gian xây dựng. Nó còn bao gồm thời gian học, kiểm thử, vận hành, chẩn đoán lỗi, thay đổi và phối hợp giữa các thành phần.
Một tính năng có lợi nhưng chỉ tạo giá trị nhỏ trong khi làm tăng đáng kể chi phí vận hành vẫn có thể là một lựa chọn không tốt. Do đó, câu hỏi đúng không phải “thành phần này có hữu ích không?” mà là “lợi ích của nó có đủ lớn để trả cho độ phức tạp mà nó tạo ra không?”
Bắt đầu từ giải pháp nhỏ nhất đáp ứng đầy đủ nhu cầu
“Nhỏ nhất” ở đây không có nghĩa là sơ sài. Một giải pháp tối thiểu vẫn phải đáp ứng các yêu cầu quan trọng về chức năng, độ tin cậy, an toàn hoặc hiệu năng nếu những yêu cầu đó thuộc bản chất vấn đề.
Cách tiếp cận phù hợp là tìm tập thành phần nhỏ nhất có thể giải quyết trọn vẹn nhu cầu đã được xác nhận. Sau đó chỉ thêm thành phần khi xuất hiện một khoảng thiếu cụ thể.
Quy trình này có thể thực hiện bằng cách:
1. Xác định kết quả bắt buộc
2. Chọn phương án trực tiếp nhất để đạt kết quả đó
3. Kiểm tra các điều kiện mà phương án chưa đáp ứng
4. Chỉ bổ sung cơ chế cho những khoảng thiếu thực sự
5. Kiểm tra lại sau mỗi lần bổ sung xem giá trị tăng thêm có tương xứng với độ phức tạp mới hay không
Cách làm này khác với việc thiết kế một kiến trúc tổng quát trước rồi cố đưa vấn đề vào kiến trúc đó. Độ phức tạp được thêm theo nhu cầu thay vì được đưa vào như một khoản dự phòng.
Kiểm chứng trước khi đầu tư vào những phần phức tạp
Khi một thành phần đắt hoặc khó đảo ngược được xây dựng dựa trên giả định, nên kiểm chứng giả định trước khi hoàn thiện giải pháp.
Nếu chưa chắc người dùng cần một chức năng, có thể thử một phiên bản đơn giản. Nếu chưa biết lưu lượng có thực sự yêu cầu kiến trúc phân tán, có thể đo tải trước. Nếu chưa chắc một quy trình cần tự động hóa hoàn toàn, có thể chạy thủ công trong phạm vi nhỏ để quan sát các trường hợp thực tế.
Mục tiêu của bước này là chuyển quyết định từ “chúng ta nghĩ sẽ cần” sang “dữ liệu hiện có cho thấy cần”.
Đặc biệt nên kiểm chứng sớm những giả định khiến thiết kế phải:
· Thêm nhiều thành phần hoặc dịch vụ
· Tạo thêm một lớp trừu tượng
· Hỗ trợ nhiều cấu hình
· Xây cơ chế mở rộng chưa được sử dụng
· Tự động hóa một quy trình nhiều ngoại lệ
· Tối ưu cho quy mô chưa xuất hiện
Nếu giả định không được xác nhận, có thể tránh được cả chi phí xây dựng lẫn chi phí duy trì độ phức tạp sau đó.
Ưu tiên khả năng thay đổi thay vì cố dự đoán mọi thay đổi
Một lý do khiến đội ngũ thiết kế quá mức là lo rằng bổ sung chức năng sau này sẽ khó. Họ cố giải quyết rủi ro đó bằng cách dự đoán hàng loạt nhu cầu tương lai và xây trước các điểm mở rộng.
Có một lựa chọn khác: làm cho giải pháp hiện tại dễ thay đổi.
Hai mục tiêu này không giống nhau. Xây trước mọi khả năng làm tăng diện tích của hệ thống ngay bây giờ. Giữ hệ thống dễ thay đổi tập trung vào cấu trúc rõ ràng, ranh giới trách nhiệm hợp lý, kiểm thử phù hợp và hạn chế phụ thuộc không cần thiết.
Đây cũng là giới hạn quan trọng của nguyên tắc đơn giản hóa. Không nên viện lý do “chưa cần” để bỏ qua những đặc tính giúp một hệ thống duy trì được khả năng thay đổi. Một giải pháp ngắn hạn nhưng quá cứng, khiến mỗi thay đổi nhỏ đều phải sửa nhiều nơi, chỉ chuyển độ phức tạp từ hiện tại sang tương lai.
Vì vậy, cần tránh cả hai cực: xây quá nhiều cho tương lai và xây một giải pháp dùng một lần đến mức gần như không thể thay đổi.
Chỉ tối ưu khi đã xác định được nút thắt thực sự
Tối ưu quá sớm cũng có thể làm giải pháp phức tạp hơn vấn đề. Một kiến trúc tối ưu cho tải lớn, độ trễ cực thấp hoặc khả năng mở rộng đặc biệt thường phải đánh đổi bằng nhiều thành phần và nhiều điều kiện vận hành hơn.
Trước khi tối ưu, cần xác định rõ:
· Chỉ số nào đang không đạt yêu cầu
· Ngưỡng cần đạt là bao nhiêu
· Thành phần nào thực sự tạo ra giới hạn
· Phương án tối ưu có cải thiện đúng giới hạn đó hay không
· Chi phí phức tạp mới có đáng với mức cải thiện hay không
Nếu chưa có một giới hạn đo được hoặc một ràng buộc cụ thể, việc tối ưu thường dựa trên dự đoán. Khi đó, người thiết kế có nguy cơ trả chi phí chắc chắn hôm nay để giải quyết một vấn đề chưa chắc xuất hiện.
Đặt giới hạn rõ ràng cho tính linh hoạt và khả năng mở rộng
“Linh hoạt” thường được xem là lợi ích tuyệt đối, nhưng mỗi loại linh hoạt đều có giá. Hệ thống cho phép thay đổi mọi hành vi thông qua cấu hình chẳng hạn sẽ phải xử lý nhiều tổ hợp trạng thái hơn một hệ thống có quy tắc cố định.
Do đó, không nên đặt mục tiêu chung chung như “hệ thống phải linh hoạt” hoặc “phải scale được”. Cần chuyển chúng thành những điều kiện cụ thể: cái gì cần thay đổi, ai cần thay đổi, trong phạm vi nào và mức tải nào thực sự phải hỗ trợ.
Khi điều kiện được xác định, thiết kế có thể tạo đúng điểm linh hoạt cần thiết mà không biến mọi thành phần thành một cơ chế tổng quát.
Nguyên tắc tương tự áp dụng cho khả năng mở rộng. Thiết kế cho mức tăng trưởng có căn cứ khác với thiết kế cho mọi mức tăng trưởng có thể tưởng tượng. Độ phức tạp nên tăng cùng bằng chứng về nhu cầu, không tăng cùng khả năng tưởng tượng của người thiết kế.
Dùng tiêu chí loại bỏ để kiểm soát thiết kế
Khi giải pháp bắt đầu lớn lên, việc hỏi “còn thiếu gì?” thường chỉ làm xuất hiện thêm chức năng. Một cách kiểm soát hiệu quả hơn là thường xuyên hỏi “cái gì có thể bỏ?”
Một thành phần nên được xem xét lại nếu:
· Không liên kết được với một nhu cầu đã xác nhận
· Chỉ phục vụ một kịch bản giả định
· Có giải pháp đơn giản hơn tạo cùng kết quả cần thiết
· Chi phí duy trì lớn hơn giá trị thực tế
· Chưa có người hoặc quy trình nào sử dụng
· Chỉ tồn tại để hỗ trợ một abstraction khác cũng chưa cần thiết
Việc loại bỏ cũng cần có giới hạn. Không nên xóa một thành phần chỉ vì nó chưa tạo giá trị trực tiếp cho người dùng nếu nó cần thiết cho an toàn, tính đúng đắn, khả năng quan sát, bảo trì hoặc một ràng buộc bắt buộc khác.
Tiêu chí cuối cùng vẫn là sự cần thiết đối với vấn đề, chứ không phải số lượng thành phần ít nhất.
Chấp nhận độ phức tạp khi chính vấn đề đòi hỏi nó
Không phải mọi giải pháp phức tạp đều là overengineering. Một vấn đề có nhiều ràng buộc thực sự có thể cần một giải pháp tương ứng.
Ví dụ, yêu cầu về bảo mật, khả năng chịu lỗi, tính nhất quán dữ liệu, tuân thủ hoặc quy mô có thể buộc thiết kế phải có thêm lớp kiểm soát. Loại bỏ chúng chỉ để kiến trúc trông đơn giản hơn sẽ tạo ra một giải pháp thiếu năng lực chứ không phải một giải pháp tốt hơn.
Ranh giới nằm ở nguồn gốc của độ phức tạp:
Độ phức tạp hợp lý xuất phát từ yêu cầu, rủi ro và điều kiện đã được xác định.
Độ phức tạp không cần thiết xuất phát từ giả định chưa kiểm chứng, dự phòng quá mức, tổng quát hóa sớm hoặc tối ưu khi chưa có nút thắt.
Do đó, mục tiêu không phải giảm độ phức tạp bằng mọi giá mà là bảo đảm mỗi phần phức tạp đều có một lý do đủ mạnh để tồn tại.
Một thiết kế cân đối bắt đầu bằng vấn đề cụ thể, triển khai phần nhỏ nhất đủ giải quyết vấn đề đó và tăng độ phức tạp theo bằng chứng. Khi một tính năng, abstraction, cơ chế mở rộng hoặc tối ưu mới được đề xuất, hãy kiểm tra ba câu hỏi: nhu cầu nào đòi hỏi nó, giá trị nào nó tạo ra và điều gì xảy ra nếu chưa xây nó?
Nếu chưa trả lời được ba câu hỏi này bằng yêu cầu, rủi ro hoặc dữ liệu đủ rõ, lựa chọn hợp lý thường là chưa thêm độ phức tạp. Cách tiếp cận đó không chống lại thiết kế chuyên sâu; nó giữ cho độ sâu của thiết kế tương xứng với độ sâu của vấn đề.
