4 Dấu Hiệu Thiết Kế Phần Mềm Tệ — Và Cách Khắc Phục Từng Cái

Bạn đã bao giờ ước tính một thay đổi nhỏ mất hai ngày, nhưng cuối cùng mất cả hai tuần? Hay vừa sửa bug ở module thanh toán thì báo cáo kho hàng lại bỗng dưng ngừng hoạt động? Đó không phải do lập trình viên kém — đó là dấu hiệu của thiết kế phần mềm có vấn đề.
Nội dung bài viết
1. Rigidity — Sự cứng nhắc của hệ thống
2. Fragility — Hệ thống dễ vỡ
3. Immobility — Không thể tái sử dụng
4. Viscosity — Làm đúng cách quá khó
5. Tổng kết và bài học thực tiễn

Trong nhiều năm làm việc với phần mềm, tôi đã tạo ra không ít sản phẩm có vấn đề. Khi đang viết code, rất khó nhận ra điểm mù của chính mình. Nhưng thời gian sẽ đặt mọi thứ vào đúng góc nhìn — và từ đó, bạn học được những bài học thực sự có giá trị.

Dưới đây là 4 dấu hiệu cụ thể cho thấy phần mềm của bạn đang được thiết kế sai hướng, kèm theo nguyên nhân và giải pháp thực tế cho từng trường hợp.

 
01

Rigidity — Hệ Thống Cứng Nhắc

Rigidity là xu hướng phần mềm trở nên khó thay đổi, dù chỉ là những chỉnh sửa nhỏ nhất. Một hệ thống bị coi là cứng nhắc khi một thay đổi ở module A kéo theo hàng loạt thay đổi bắt buộc ở các module B, C, D phụ thuộc vào nó.

Triệu chứng nhận biết: Bạn ước tính một thay đổi mất 2 ngày, nhưng thực tế mất đến 2 tuần — vì "cái này kéo cái kia, cái kia lại kéo cái nọ."

Nguyên nhân: Thường là do excessive coupling — mọi thứ bị gắn kết quá chặt đến mức bạn không thể di chuyển một phần mà không ảnh hưởng đến toàn bộ hệ thống.

Ví dụ thực tế: Bạn có một class OrderProcessor với một khối switch khổng lồ để tính phí vận chuyển. Nếu là "UPS" thì làm X, "FedEx" thì làm Y. Khi cần thêm "DHL", bạn buộc phải sửa trực tiếp vào class đó — rồi recompile, retest toàn bộ module đơn hàng.

Giải pháp: Strategy Pattern / Open-Closed Principle Tạo một interface ShippingStrategy. Mỗi hãng vận chuyển (UPS, FedEx, DHL) tự implement interface đó. OrderProcessor chỉ nhận một strategy và gọi calculate(). Để thêm hãng mới, bạn chỉ cần tạo class mới — không chạm vào code cũ.
 
02

Fragility — Hệ Thống Dễ Vỡ

Fragility là xu hướng phần mềm bị hỏng ở nhiều nơi mỗi khi có một thay đổi. Khác với Rigidity, vấn đề ở đây không phải là nỗ lực thực hiện thay đổi, mà là sự thiếu kiểm soát đối với các hiệu ứng phụ ngoài ý muốn.

Triệu chứng nhận biết: Bạn sửa một bug trong module thanh toán, và bỗng dưng chức năng xuất báo cáo kho hàng ngừng hoạt động — dù hai thứ này nhìn qua tưởng chẳng liên quan gì.

Nguyên nhân: Xuất phát từ các hidden dependencies — phụ thuộc ẩn — hoặc logic quá đan xen khiến các module biết quá nhiều về cơ chế nội bộ của nhau.

Ví dụ thực tế: Bạn có một biến toàn cục hoặc Singleton lưu "System Configuration." Một lập trình viên đổi định dạng ngày tháng trong cấu hình đó để phục vụ một report cụ thể. Đột nhiên, module Payroll ngừng xử lý thanh toán — vì nó kỳ vọng định dạng cũ. Hệ thống bị vỡ ở chỗ hoàn toàn không liên quan đến thay đổi ban đầu.

Giải pháp: Encapsulation và Interface Segregation Đừng để mọi nơi truy cập vào một object toàn cục. Hãy chia nhỏ cấu hình thành các interface riêng biệt như PayrollConfig, ReportConfig. Mỗi module chỉ nhìn thấy phần nó cần. Kết quả: thay đổi trong báo cáo chỉ ảnh hưởng đến interface báo cáo — Payroll được bảo vệ hoàn toàn.
 
03

Immobility — Không Thể Tái Sử Dụng

Immobility là tình trạng phần mềm không thể được tái sử dụng ở các dự án khác, thậm chí ở các phần khác của cùng một dự án. Điều này xảy ra khi thiết kế bị gắn chặt với môi trường đến mức chi phí tách một tính năng ra còn tốn kém hơn viết lại từ đầu.

Triệu chứng nhận biết: Bạn cần một hàm validation đã được implement ở module khác. Nhưng khi cố copy nó, bạn nhận ra nó đang kéo theo cả database, giao diện người dùng và ba thư viện ngoài không liên quan.

Nguyên nhân: Thiết kế không tách biệt business rules khỏi implementation details — như database, framework hay giao diện người dùng.

Ví dụ thực tế: Bạn viết một thuật toán xuất sắc để validate mã số thuế hoặc CMND. Nhưng thuật toán đó nằm bên trong class UserRegistrationForm — class này kế thừa từ một thư viện UI như React hoặc Android SDK và gọi thẳng vào database để kiểm tra trùng lặp.

Giải pháp: Layered Architecture / Clean Architecture Tách thuật toán ra thành một Use Case hoặc POJO thuần túy — code này không được biết gì về button hay database. Database được truyền vào qua interface theo nguyên tắc Dependency Inversion. Kết quả: bạn có thể copy file validation đó sang bất kỳ dự án nào mà không kéo theo "rác" từ UI hay database.
 
04

Viscosity — Làm Đúng Cách Quá Khó

Viscosity là sự cản trở mà hệ thống tạo ra khi bạn cố gắng làm mọi thứ theo cách đúng đắn. Nó tồn tại dưới hai hình thức:

Software Viscosity: Thêm một "hack" tạm thời dễ hơn nhiều so với việc giữ kiến trúc sạch đúng thiết kế ban đầu.

Environment Viscosity: Môi trường phát triển quá chậm hoặc kém hiệu quả — compile lâu, test chạy hàng giờ — khiến lập trình viên bị cám dỗ bỏ qua quy trình chính thức.

Triệu chứng nhận biết: Bạn cần thêm một field vào form. Cách đúng đắn: tạo database migration, cập nhật entity, DTO, mapper — mất 1 tiếng. Cách dơ: nhét dữ liệu vào trường text generic "ghi chú" — mất 5 phút. Khi môi trường chậm, bạn luôn chọn cách dơ.
Giải pháp: Tự động hóa và Refactor Infrastructure Nếu vấn đề là môi trường compile chậm, hãy đầu tư vào máy mạnh hơn hoặc chia nhỏ dự án thành các module độc lập. Nếu vấn đề là phía software, dùng các công cụ như Lombok, AutoMapper hay MapStruct để giảm boilerplate. Nguyên tắc cốt lõi: làm đúng cách phải gần như nhanh bằng làm sai cách. Khi làm đúng trở nên dễ dàng, lập trình viên sẽ tự nhiên đi theo hướng đúng.
 

Tổng Kết — Những Điều Cần Ghi Nhớ

Bốn dấu hiệu trên — Rigidity, Fragility, Immobility và Viscosity — không chỉ là khái niệm lý thuyết. Chúng là những "mùi hôi" thực sự báo hiệu hệ thống của bạn đang cần được chú ý và cải thiện.

Thiết kế cho sự thay đổi Hệ thống cứng nhắc là hệ thống chậm. Dùng các pattern như Strategy để tách logic khỏi cách thực thi.
Bảo vệ ranh giới của từng module Tránh global state và hidden dependencies — đây là nguồn gốc chính của Fragility.
Tách business logic khỏi framework Business logic của bạn phải có thể "sống" ở bất kỳ đâu. Nếu nó bị kẹt trong UI component, hệ thống đang bất động.
Làm cách đúng phải là cách dễ Nếu làm đúng quá tốn công, hệ thống sẽ dần đầy hack. Đầu tư vào môi trường phát triển là đầu tư vào chất lượng dài hạn.

Điều quan trọng cần nhớ: thiết kế tệ không phải bản án chung thân. Nhận diện được những dấu hiệu này là bước đầu tiên để refactor và tiến về phía một kiến trúc incremental, lành mạnh và bền vững hơn.

Bạn đang gặp phải dấu hiệu nào trong số này ở dự án hiện tại? Dấu hiệu nào bạn thấy khó xử lý nhất? Chia sẻ trong phần bình luận — tôi đọc và trả lời tất cả.

SE
Biên soạn bởi đội ngũ Software Engineering
Chúng tôi chia sẻ kiến thức thực tế về kiến trúc phần mềm, clean code và các nguyên tắc thiết kế giúp đội ngũ kỹ thuật xây dựng sản phẩm bền vững hơn.