Mọi lập trình viên đều có một thư mục như vậy. Cái thư mục mang tên utils hoặc helpers mà bạn thường sao chép từ repo này sang repo khác. Bạn dán nó vào, dành ra hai mươi phút để xóa các tham chiếu đến các schema cơ sở dữ liệu cũ, gỡ bỏ các kiểm tra xác thực (auth checks) không còn phù hợp, và đổi tên các biến để linter mới không còn "la hét" nữa. Tôi đã từng làm điều này với một hệ thống tạo theme mà tôi tự xây dựng, Dynamic Theme Kit. Ban đầu, nó chỉ là một tính năng bên trong một ứng dụng, và trong nhiều tháng, tôi coi nó như một công cụ có thể di động. Tôi đã lầm. Sao chép mã nguồn không phải là tái sử dụng. Đó chỉ là sự trùng lặp với thêm nhiều bước trung gian.
Cái bẫy của tư duy "chỉ một dự án"
Khi bạn xây dựng một tính năng bên trong một dự án, bạn tạo ra hàng trăm giả định ngầm. Bảng màu có thể giả định một thiết lập CSS-in-JS nhất định. Thang đo khoảng cách có thể tham chiếu đến một design token từ hướng dẫn thương hiệu của công ty bạn. Nút chuyển đổi giữa chế độ sáng và tối có thể gọi một endpoint tùy chọn người dùng chỉ duy nhất backend của ứng dụng đó mới có. Những phụ thuộc này có vẻ vô hại vì khi ở trong dự án, chúng thực sự vô hại. Chúng thuộc về nơi đó.
Vấn đề bắt đầu khi bạn cố gắng tách mã nguồn đó ra. Bạn phát hiện ra rằng thành phần "có thể tái sử dụng" thực chất là một mạng lưới các chuỗi liên kết ẩn kết nối nó với mã nguồn cũ. Tôi đã học được điều này với DTK. Nó có tạo ra các biến theme, đúng vậy. Nhưng nó cũng đòi hỏi một cấu trúc thư mục cụ thể. Nó import một định nghĩa kiểu (type definition) từ đâu đó sâu trong thư mục types của ứng dụng gốc. Nó giả định sự hiện diện của một đối tượng cấu hình toàn cục (global config object) chỉ tồn tại trong repository đó. Tôi chưa bao giờ nhận ra vì trong phạm vi dự án đó, mọi thứ luôn luôn có sẵn.
Biến DTK thành một package độc lập giống như một cuộc phẫu thuật, chứ không phải là sự mở rộng. Tôi không cần thêm tính năng. Tôi cần ít sự kết nối hơn.
Trích xuất Dynamic Theme Kit
Công việc khó khăn nhất là ngồi lại với mã nguồn và tự hỏi, đối với mỗi hàm và mỗi thành phần export: cái này phục vụ cho logic tạo theme, hay nó phục vụ cho dự án? Tôi đã loại bỏ các thiết lập kiểu dáng (styling presets) có sẵn. Tôi loại bỏ giả định rằng bên sử dụng sẽ là một ứng dụng React. Tôi xóa bỏ hoàn toàn các bảng màu mặc định. Dự án gốc có một thẩm mỹ doanh nghiệp với tông màu navy và slate được cài sẵn trong các giá trị mặc định. Điều đó phải được loại bỏ. Một package không thể đi kèm với màu sắc thương hiệu của riêng bạn.
Bộ kit mới sẽ chỉ làm đúng một việc. Nó nhận một đối tượng cấu hình—một vài giá trị màu sắc, một vài con số về khoảng cách, một vài thang đo typography—và nó tạo ra các thuộc tính CSS tùy chỉnh (CSS custom properties). Chỉ vậy thôi. Nó không áp dụng chúng. Nó không quyết định chúng sẽ nằm ở đâu trong DOM của bạn. Nó không quan tâm bạn sử dụng Tailwind, Styled Components hay HTML thuần. Nó cung cấp cho ứng dụng của bạn các biến, và dự án của bạn sẽ chọn cách sử dụng chúng.
Sự ràng buộc đó ban đầu có vẻ hạn chế. Nhưng hóa ra nó lại mang đến sự tự do.
Điều gì sẽ hỏng khi bạn thực sự cố gắng tái sử dụng nó
Trước khi xuất bản bất cứ thứ gì, tôi cần bằng chứng rằng sự trừu hóa (abstraction) đó thực sự hiệu quả. Tôi đã lấy ba dự án cá nhân nhỏ từ kho lưu trữ của mình: một công cụ xem trước markdown, một ứng dụng theo dõi thói quen, và một trang landing page cho một sự kiện. Không có dự án nào dùng chung một framework hay cấu trúc thư mục. Tôi cài đặt DTK cục bộ trong từng dự án và thử tạo theme cho chúng.
Lần thử đầu tiên thất bại ngay lập tức. Tên các biến mà DTK tạo ra quá cụ thể. Nó xuất ra các token như --primary-action và --background-overlay, những cái tên ngụ ý một bố cục UI nhất định. Trong trình xem trước markdown, những cái tên đó chẳng có ý nghĩa gì cả. Không có nút hành động (action button) nào cả. Cũng không có lớp phủ (overlay) nào. Tôi đã đổi lại logic tạo tên để tạo ra các tên trung lập, mang tính cấu trúc, mô tả giá trị thay vì mô tả widget.
Tôi cũng nhận thấy rằng các giá trị mặc định của mình quá áp đặt. Khi người dùng truyền vào một cấu hình không đầy đủ, DTK sẽ lấp đầy các khoảng trống bằng các giá trị trông có vẻ ổn trong một dashboard dày đặc thông tin nhưng lại làm hỏng một trang landing page thưa thớt. Tôi đã chuyển sang các giá trị mặc định minh bạch (transparent defaults), nơi các token bị thiếu sẽ đơn giản là không được render, để dự án sử dụng có thể tự định nghĩa các giá trị dự phòng (fallbacks) của riêng mình.
Sau đó là vấn đề tài liệu hướng dẫn. Những gì có vẻ hiển nhiên với tôi—"chỉ cần truyền vào một đối tượng cấu hình"—lại rất mơ hồ với một người đang đọc README vào lúc nửa đêm. Tôi đã viết lại nó với các đối tượng thực tế, các đường dẫn tệp thực tế, và những giải thích rõ ràng về những gì xảy ra khi bạn gọi hàm so với những gì ứng dụng của bạn cần làm sau đó.
Những dự án cá nhân nhỏ này đóng vai trò như các môi trường thử nghiệm (test beds). Chúng có rủi ro thấp, nhưng chúng đã bộc lộ những lỗi thực sự mà tôi sẽ không thể phát hiện nếu chỉ nhìn chằm chằm vào mã nguồn một cách biệt lập.
Bài kiểm tra thực sự: Triển khai thực tế tại Web Weavers World
Các dự án cá nhân là những môi trường thử nghiệm (sandboxes). Chúng không có thời hạn, không có các bên liên quan, hay những đoạn CSS cũ có trước cả gói (package) của bạn. Thử thách thực sự đến khi tôi tích hợp DTK vào Web Weavers World, trang web kinh doanh của mình. Đây là một trang web đang hoạt động với các kiểu dáng sẵn có, kỳ vọng của khách hàng và các chỉ số phân tích cần phải cân nhắc. Nếu gói này làm hỏng thứ gì đó, tôi không thể chỉ đơn giản là xóa kho lưu trữ (repo) và bắt đầu lại từ đầu.
Tôi đã thêm DTK vào quy trình xây dựng (build pipeline), trỏ nó tới một cấu hình màu mới và để nó tạo ra một bộ biến CSS mới. Việc tích hợp chỉ mất một buổi chiều, chứ không phải một tuần. Đó chính là tín hiệu. Trước đây, việc thêm một chủ đề (theme) mới đồng nghĩa với việc phải viết CSS mới, lùng sục các giá trị hex được viết cứng (hardcoded) trong hàng chục tệp, và hy vọng rằng mình không bỏ lỡ một trường hợp ngoại lệ (edge case) nào. Giờ đây, tôi chỉ cần thêm một bảng màu vào tệp cấu hình, DTK sẽ tạo ra các biến, và phần còn lại của trang web sẽ sử dụng chúng. Logic của chủ đề đã chuyển từ một quy trình thủ công mong manh thành một thứ mà tôi đủ tin tưởng để bàn giao cho các cộng tác viên.
Ba câu hỏi đã thay đổi cách tôi xây dựng sản phẩm
Trải qua quá trình này đã buộc tôi phải chính thức hóa một danh sách kiểm tra (checklist) trong tâm trí mà tôi hiện đang sử dụng trước khi trừu tượng hóa (abstract) bất cứ thứ gì:
- Biến này có thực sự mang tính tổng quát không? Nếu tên gọi hoặc logic tham chiếu đến một khái niệm miền (domain concept) từ dự án gốc, nó nên được để lại phía sau.
- Thứ này thuộc về gói (package) hay ứng dụng (application)? Các quy tắc kinh doanh, nhận diện thương hiệu và các giả định về bố cục nằm ở ứng dụng. Phần hạ tầng (plumbing) tạo ra đầu ra chuẩn hóa nằm ở gói.
- Tôi đang giải quyết một vấn đề có thể tái sử dụng hay một vấn đề đặc thù của dự án? Đây là câu hỏi khó trả lời một cách trung thực nhất. Chúng ta thích nghĩ rằng các giải pháp của mình là phổ quát. Nhưng thường thì chúng chỉ mang tính cục bộ.
Việc trả lời những câu hỏi này đã buộc tôi phải đơn giản hóa thiết kế của mình, thường là bằng cách loại bỏ mã nguồn thay vì thêm vào. DTK đã dạy tôi rằng tái sử dụng không phải là một món quà bạn tự tặng cho mình. Đó là một kỷ luật mà bạn thực hành bằng cách nói không với sự tiện lợi nhất thời.
Một cách tư duy khác về Refactoring
Tôi từng đo lường việc refactor bằng việc chúng làm mã nguồn ngắn đi bao nhiêu. Ít dòng code hơn mang lại cảm giác như một sự tiến bộ. Giờ đây, tôi đo lường chúng bằng việc chúng mở ra được bao nhiêu cánh cửa. Dynamic Theme Kit không thanh lịch vì nó ngắn gọn. Nó hữu ích vì nó đã vượt qua ba dự án cá nhân không liên quan và một trang web kinh doanh đang hoạt động mà không cần phải thay đổi cấu trúc bên trong.
Đó mới là thước đo quan trọng. Mã nguồn chỉ chạy được một lần là một khoản chi phí. Mã nguồn chạy được nhiều lần là một tài sản. Trước khi bắt đầu bất kỳ tính năng nào hiện nay, tôi sẽ dừng lại. Tôi tự hỏi liệu mình có đang xây dựng thứ gì đó mà mình sẽ cần lại hay không. Nếu câu trả lời là có, tôi sẽ xây dựng nó theo một cách khác ngay từ dòng code đầu tiên. Tôi tách biệt các đầu vào (inputs). Tôi xác định các đầu ra (outputs). Tôi loại bỏ các giả định.
Một đợt refactor tốt nhất không phải là làm cho mã nguồn của bạn ngắn hơn. Nó làm cho mã nguồn của bạn hoạt động được ở những nơi mà bạn thậm chí còn chưa tưởng tượng tới.
