Developer thực dụng - Phần 2 - Phương pháp

Bản chất của thiết kế tốt

Lời khuyên thứ 14: Thiết kế tốt dễ dàng thay đổi hơn thiết kế tồi**

  • Một vật được thiết kế tốt nếu nó mang lại cảm giác thuận tiện cho người sử dụng chúng. Tương tự, với code thì nó phải mang lại sự thuận tiện khi thay đổi (nguyên tắc ETC - easier to change)

  • Mọi nguyên tắc thiết kế đều là một bản đặc biệt của ETC.

    • Như việc decoupling - tách rời những thành phần có mối quan tâm khác nhau, thì việc cập nhật những thành phần đó sẽ dễ dàng hơn.
    • Nguyên tắc single responsibility - một thay đổi ở requirement chỉ ảnh hưởng đến một module duy nhất.
    • Tại sao đặt tên - lại quan trọng, vì những cái tên dễ hiểu sẽ làm code dễ đọc hơn, và bạn phải đọc code trước khi có thể thay đổi code.
  • Vì ETC là giá trị, không phải là luật lệ, bạn nên suy nghĩ đến ETC trước khi bắt tay làm một việc gì đó. Tự hỏi “thứ tôi vừa làm khiến hệ thống dễ thay đổi hay khó thay đổi hơn?”. Thường thì, common sense của bạn sẽ chính xác

  • Nếu không biết hướng đi: bạn có thể thứ (1) cố gắng làm thứ bạn xây dựng có thể thay thế - tách rời và gắn kết (2) ghi chú về những lựa chọn bạn có hiện tại, và những thay đổi có thể xảy đến, và đến khi thay đổi code, bạn có thể xem lại và nhận xét

DRY - Sự trùng lặp xấu xa

  • Kiến thức của hệ thống thường không bền và dễ bị thay đổi, có thể thay đổi sau buổi họp, hoặc có sự thay đổi trong luật pháp … Những điều này làm developer phải luôn cập nhật và điều chỉnh hệ thống lại cho phù hợp, khiến chúng ta luôn ở trong trạng thái bảo trì hệ thống
  • Khi thực hiện những thay đổi kể trên, chúng ta rất dễ để cho sự trùng lặp kiến thức xảy ra, khiến cho việc bảo trì hệ thống biến thành ác mộng
  • Và cách duy nhất để phát triển hệ thống một cách bền vững, dễ bảo trì và dễ hiểu, là áp dụng nguyên tắc DRY: bất kể phần kiến thức nào cũng chỉ có một nơi biểu đạt, không mơ hồ, có tính chính xác cao trong hệ thống
  • Khi việc lặp lại xảy ra, khi thay đổi một chỗ, bạn phải nhớ thay đổi chỗ nào khác nữa. Vấn đề chính không phải bạn có nhớ hay không, mà là khi nào bạn sẽ quên
  • Và việc trùng lặp không chỉ xảy ra với code, nó còn xảy ra với các tính huống khác

Lời khuyên thứ 15: DRY - Don’t repeat yourself - Đừng lặp lại chính việc mình đã làm

DRY vượt khỏi bối cảnh code

  • DRY còn là về sự lặp lại của kiến thức, của những ý định. Đó là việc mô tả một vấn đề ở 2 nơi khác nhau, và còn có thể có biểu đạt ở 2 cách khác nhau
  • Cách đánh giá: khi một vấn đề nhỏ cần được cập nhật code, bạn có phải đi đổi ở nhiều nơi, ở nhiều định dạng khác nhau? Bạn phải đổi code, tài liệu, cấu trúc database … . Nếu có, thì code bạn chưa DRY

DRY trong code

Ví dụ dễ thấy nhất:

Điều đầu tiên là sự trùng lặp của việc kiểm tra số dương / số âm để in ra cho đúng định dạng, ta có thể chuyển nó ra một hàm khác

Điều lặp lại tiếp theo là sự lặp lại của quy định định dạng số tiền Debit / Credit, ta có thể tái sử dụng hàm vừa viết

Điều tiếp theo nữa là gì? Nếu khách hàng yêu cầu thêm 1 khoảng trắng giữa label và số, ta phải đổi 5 dòng codes, vậy ta có thể cập nhật như sau

Lúc này, nếu muốn đổi định dạng số, ta chỉ cần đổi hàm FormatAmount, còn muốn đổi fomat dòng, ta chỉ cần đổi hàm PrintLine

Tuy nhiên, không phải sự trùng lặp code nào cũng là sự trùng lặp kiến thức

Ví dụ ở một trang thương mại điện tử, khách hàng yêu cầu bạn phải kiểm tra tuổi của user, và số lượng món hàng mà user đặt hàng. Quy định nó phải là số nguyên dương

1
2
3
4
5
6
7
8
9
public bool ValidateAge(int value)
{
return value >= 0;
}

public bool ValidateQuantity(int value)
{
return value >= 0;
}

Sẽ có lầm tưởng 2 hàm trên vi phạm nguyên tắc DRY bởi thân hàm đều giống nhau. Điều đó không đúng, bởi dù cho code có giống nhau, nhưng kiến thức nó biểu đạt không hề trùng lặp. Hai hàm trên kiểm tra 2 đối tượng hoàn toàn khác nhau và có quy định kiểm tra giống nhau. Đó là sự trùng hợp, không phải sự trùng lặp.

Trùng lặp trong tài liệu

Ý định của một hàm chỉ nên biểu đạt trong thân hàm, việc thêm comment phía trên hàm sẽ dễ làm 2 phần này mất đi sự đồng bộ về mặt nội dung

Chỉ nên comment trên hàm để biểu đạt ý nghĩa hoặc lý do hàm đó cần phải tồn tại

Trùng lặp trong data structure

Việc trùng lặp này dễ xảy ra trong data structure phổ biến như đơn hàng (giá trị đơn hàng là tổng các món trong đơn hàng), dữ liệu cá nhân (tên đầy đủ là kết hợp giữa họ và tên)

Trong quá trình phát triển, có thể cân nhắc việc vi phạm DRY để tăng hiệu năng ứng dụng. Khi đưa data structure cho bên thứ ba sủ dụng, phải giới hạn việc vi phạm này trong phạm vi của data structure bằng cách dùng kỹ thuật như getter, setter

Trùng lặp trong thể hiện

Việc sử dụng những service, thư viện bên ngoài là điều thường xảy ra khi phát triển phần mềm, dẫn đến hệ thống của bạn phải có kiến thức về những service bên ngoài. Khi đó, sự trùng lặp kiến thức là không dễ tránh khỏi. Trường hợp service bên ngoài có thay đổi nào mà hệ thống của bạn không biết, hệ thống của bạn sẽ gặp lỗi

Sự trùng lặp này không thể tránh khỏi, nhưng có thể được giảm thiểu

Trùng lặp API nội bộ

Đối với API nội bộ, hãy tìm kiếm một công cụ trung gian để thực hiện việc tạo tài liệu, tạo mock API, và API client, hỗ trợ xuất nhiều ngôn ngữ khác nhau

Trùng lặp API bên ngoài

Các API bên ngoài thường được miêu tả bằng chuẩn OpenAPI, bạn có thể tận dụng nó để import vào công cụ API

Nếu API đó chưa có mô tả OpenAPI, bạn có thể tạo 1 bản mô tả và đăng lên mạng, sẽ giúp được người có cùng nhu cầu với bạn, và còn có thể được trợ giúp trong việc bảo trì

Trùng lặp về data source

Hầu hết các data source đều cho phép xem data schema, nên có thể tận dụng persistent framework / ORM để tạo ra các class data access dùng cho việc truy cập data source

Trùng lặp giữa lập trình viên

Sự trùng lặp bắt nguồn từ lập trình viên thường khó nhận biết nhất

Cần xây dựng một đội nhóm mạnh, gắn kết, với nền tảng giao tiếp tốt, sử dụng các kỹ thuật như stand-up meeting, slack để hỗ trợ và lưu giữ lịch sử giao tiếp

Giao cho một người chức danh thủ thư của project, chức năng chính là tạo điều kiện cho việc trao đổi các kiến thức, như có một repository trung tâm chuyên lưu giữ những đoạn script hữu ích, có một quy trình review code theo lịch biểu, hoặc qua những PR

Lời khuyên thứ 16: Nên để việc tái sử dụng trở nên dễ dàng hơn

Hãy tạo điều kiện cho một môi-trường-tái-sử-dụng phát triển, nơi mà bạn có thể dễ dàng tái sử dụng những thứ có sẵn thay vì phải phát triển lại. Nếu thất bại, bạn sẽ có nguy cơ sa vào sự trùng lặp kiến thức

Hệ thống trực giao - orthogonality

Trực giao là một khái niệm quan trọng nếu muốn xây dựng một hệ thống dễ dàng thiết kế, xây dựng, test và mở rộng

Trực giao là gì

Trực giao là một khái niệm hình học, hai đường thẳng được gọi là trực giao khi chúng tạo thành một góc vuông, theo góc nhìn vector học, thì hai đường thằng này độc lập với nhau

Theo góc nhìn tin học, thì trực giao là một cách biểu đạt của việc decoupling - tính độc lập. Hai thành phần được gọi là độc lập khi thay đổi ở thành phần này không ảnh hưởng tới những thành phần khác

Ở một hệ thống được thiết kế tốt, thì database code sẽ trực giao với giao diện người dùng: thay đổi ở database sẽ không làm ảnh hưởng tới giao diện, việc thay đổi giao diện không làm ảnh hưởng gì tới database, hoặc giao diện sẽ không bị ảnh hưởng khi thay đổi qua một loại database khác

Lợi ích của sự trực giao

Hệ thống không có tính trực giao sẽ khó thay đổi và kiểm soát hơn. Khi những thành phần trong hệ thống có tính phụ thuộc với nhau, một bản sửa lỗi đơn giản cũng kéo theo sự thay đổi ở nhiều thành phần

Lời khuyên thứ 17: Những thành phần không liên quan thì không được ảnh hưởng tới nhau

Khi các thành phần được thiết kế độc lập với nhau, miễn là bạn không thay đổi interface mà thành phần đó public, bạn có thể chắc chắn rằng việc thay đổi ở một thành phần sẽ không ảnh hưởng tới những thành phần khác trong hệ thống

Điều này mang lại 2 lợi ích chính là: tăng năng suất và giảm rủi ro

Tăng năng suất

  • Những thay đổi được cục bộ hóa, nên thời gian phát triển và test sẽ giảm bớt
  • Khuyến khích việc tái sử dụng
  • Việc kết hợp chức năng của những thành phần với nhau bằng cách tái sử dụng sẽ làm tăng năng suất

Giảm rủi ro

  • Những đoạn code bị lỗi được cục bộ hóa, việc sửa đổi hoặc thay thế sẽ trở nên dễ dàng hơn
  • Hệ thống bớt mong manh hơn. Những thay đổi và sửa lỗi chỉ giới hạn ở một khu vực nhất định
  • Có thể được test ổn hơn, vì đã test trên những thành phần nhỏ rồi
  • Không bị ràng buộc với một nhà cung cấp thứ ba, một sản phẩm hoặc nền tảng nào, bởi giao tiếp tới những hệ thống bên ngoài được giới hạn trong những thành phần nhỏ trong hệ thống

Thiết kế

Việc thiết kế một hệ thống trực giao là một vấn đề quen thuộc của giới lập trình, có thể gọi nó với những cái tên như modular, component-based, hoặc layered (mô hình nhiều lớp)

Ở mô hình layered, các thành phần được gom vào các lớp khác nhau, mỗi lớp sẽ cung cấp một bộ abstraction để lớp khác có thể sử dụng

Nên tự hỏi hệ thống của bạn độc lập thế nào với những thay đổi ở thế giới thực. Nếu bạn sử dụng số điện thoại để làm định danh cho một đối tượng nào đó, thì sẽ thế nào nếu số điện thoại đó bị đổi định dạng? Mấu chốt là, những thuộc tính ở thế giới thực là những thứ bạn không thể kiểm soát được, và có thể bị thay đổi vì bất kỳ lý do gì, nên đừng bao giờ dựa vào những thuộc tính bạn không thể kiểm soát

Thư viện và các bộ công cụ

Cẩn thận khi chọn lựa thư viện hoặc bộ công cụ ở bên thứ ba vào hệ thống để đảm bảo tính trực giao. Ví dụ nếu một thư viện về data persistence cần bạn khởi tạo hoặc lấy dữ liệu theo một cách đặc biệt nào đó, thì thư viện đó không có tính trực giao. Giữ những ràng buộc thừa do bên thứ ba quy định, không cho chúng xâm nhập code của bạn, là một cách tốt để dễ thay đổi nhà cung cấp trong tương lai

Viết code

Khi có code mới được viết ra, bạn có nguy cơ phá vỡ tính trực giao hiện có. Một số phương pháp để theo dõi và duy trì tính trực giao:

  • Giữ code được decoupled: viết những đoạn code-modules không phơi bày những gì không quan trọng, hoặc phụ thuộc vào implementation của những modules khác. Nếu bạn cần thay đổi một object, hãy để chính nó làm việc đó
  • Tránh xa global data: bất cứ khi nào code của bạn trỏ đến một global data nào, thì code đó đã bị ràng buộc với component chứa global data đó. Nói chung là, code của bạn sẽ dễ bảo trì hơn khi bạn truyền vào bất cứ dữ liệu nào mà module cần một cách tường minh, ở OOP, bạn sẽ truyền ở constructor chẳng hạn. Và hãy cẩn thận với singleton, nó có thể tạo ra những liên kết không cần thiết
  • Tránh xa những function tựa tựa nhau: sẽ có những function tựa tựa nhau kiểu giống đầu và đuôi nhưng thân giữa lại khác. Đây là dấu hiệu code có vấn đề về mặt kiến trúc. Cần xem qua Strategy Pattern để tối ưu việc này

Cần có thói quen kiểm tra và cải tiến code để đảm bảo kiến trúc và tính trực giao, và quá trình này được gọi là refactoring

Testing

Một hệ thống trực giao sẽ dễ test hơn. Vì giao tiếp giữa các thành phần đã được giới hạn và chuẩn mực hóa, mỗi module trong hệ thống sẽ có một bộ test riêng. Đây là điều tốt bởi thực hiện unit test những module riêng rẽ sẽ dễ định nghĩa và thực hiện hơn integration test. Sẽ tốt hơn nếu những unit test này được chạy tự động theo định kỳ.

Việc viết unit test cũng là một cách để kiểm tra hệ thống của bạn có tính trực giao hay không. Nếu bạn phải import một lượng lớn component của hệ thống để viết unit test, thì module bạn đang test chưa tách biệt lắm với hệ thống

Việc fix bug cũng là một cách để kiểm tra tính trực giao. Bạn chỉ thay đổi một module hay thay đổi rải rác ở nhiều module? Bản fix có sửa được vấn đề hoàn toàn không, hay lại có một vấn đề khác lại nổi lên?

Tài liệu

Tính trực giao cũng áp dụng cho việc soạn tài liệu. Hai thành phần trực giao là nội dung và cách trình bày. Việc trực giao là khả năng bạn có thể thay đổi kiểu cách của tài liệu mà không phải thay đổi nội dung.

Sống với tính trực giao

Tính trực giao mối liên quan gần gũi với nguyên tắc DRY. Áp dụng DRY sẽ giúp giảm thiểu sự trùng lặp có trong hệ thống, trong khi tính trực giao sẽ giảm bớt mối ràng buộc giữa các thành phần trong hệ thống với nhau. Kết hợp 2 nguyên tắc đó sẽ giúp bạn xây dựng một hệ thống linh hoạt, dễ hiểu, dễ test và bảo trì

Khả năng đảo ngược

Có một thực tế phũ phàng rằng thế giới luôn thay đổi, luôn có hơn nhiều hơn một cách để giải quyết vấn đề, luôn có nhiều hơn một nhà cung cấp cho một dịch vụ nào đó. Bằng cách ứng dụng nguyên tắc tái sử dụng - DRY, phân tách - decoupling và đưa cấu hình ra bên ngoài code - external configuration, chúng ta sẽ hạn chế tạo ra các thay đổi quan trọng và không thể đảo ngược

Lời khuyên thứ 18: Không có quyết định nào là cuối cùng

Có rất nhiều kiến trúc ở server-side được giới thiệu và trở nên thịnh hành (chạy trên một server, nhiều server, nhiều server sau load balancer, container, serverless …). Vì vậy việc lên kế hoạch đón đầu hết tất cả những kiến trúc có thể có trong tương lai là điều không thể. Việc bạn có thể làm là làm ứng dụng của bạn dễ thay đổi nhất có thể: ẩn API bên thứ ba bằng lớp abstraction của riêng mình, tách code thành những component nhỏ …

Lời khuyên thứ 19: Các xu hướng công nghệ đến rồi đi, chỉ ứng dụng của bạn là trường tồn

Viên đạn dẫn đường

Ngoài đời thực, việc nạp những viên đạn dẫn đường - tracing bullet giữa những viên đạn thường giúp người cầm súng điều chỉnh hướng đạn khi bắn, sao cho bắn trúng được mục tiêu luôn di chuyển

Trong phát triển hệ thống, thay vì dành nhiều thời gian phân tích toàn bộ các yêu cầu, tìm hiểu tất cả những cái chưa biết, và ràng buộc môi trường. Thì người developer thực dụng áp dụng nguyên tắc của viên đạn dẫn đường để nhận được phản hồi tức thì khi mục tiêu luôn thay đổi

Nguyên tắc viên đặn dẫn đường hữu dụng trong việc xây dựng những hệ thống chưa từng được làm trước đó

Những đoạn code phát quang

Viên đạn dẫn đường hiệu quả vì nó hoạt động trên cùng một môi trường với viên đạn thật. Người bắn thấy được phản hồi từ viên đạn ngay lập tức, và đó cũng là một giải pháp ít tốn chi phí

Trong việc lập trình, chúng ta sẽ cần tìm các điểm giúp ta đi từ yêu cầu chức năng tới hệ thống thực tế một cách nhanh, rõ ràng, và có thể lặp lại được. Đó là những yêu cầu chức năng quan trọng, những thứ định rõ được hệ thống, đó là những nơi bạn còn đang băn khoăn và những nơi có nguy cơ cao. Tìm ra những điểm đó và đó là những nơi đầu tiên bạn cần code.

Lời khuyên thứ 20: Sử dụng viên đạn dẫn đường để tìm thấy mục tiêu

Ví dụ ở một hệ thống có nhiều lớp: UI, Auth, Business Logic, Data Model, Database. Làm cách nào để tích hợp các lớp đó lại với nhau? Cách đơn giản là làm một tính năng có thể trải dài trên tất cả các lớp, sau khi hoàn tất tính năng đó thì bạn cũng sẽ có sẵn khung của dự án, tạo tiền đề để phát triển các chức năng tiếp theo.

Ví dụ đó nói lên những đoạn code phát quang không phải là viết rồi vứt, đó là code mà bạn sẽ giữ lại. Nó bao gồm cấu trúc, code kiểm tra lỗi, tài liệu … như code chạy trên production, nó chỉ chưa đủ chức năng. Khi bạn đã có một cấu trúc kết nối tất cả các lớp với nhau, việc còn lại chỉ là kiểm tra cấu trúc đã giải quyết được mục tiêu chưa. Nếu giải quyết rồi thì việc thêm chức năng là điều đơn giản.

Những đoạn code phát quang có những lợi ích:

  • Người dùng sớm thấy được cách hệ thống hoạt động
  • Developer thấy được cấu trúc làm việc
  • Bạn có được một nền tảng để tích hợp
  • Bạn có được một thứ gì đó để demo
  • Bạn cảm giác được mọi thứ đang được phát triển

Viên đạn dẫn đường không phải lúc nào cũng trúng đích

Đó là điều phần lớn sẽ xảy ra. Đợt bắn đầu tiên không phải lúc nào cũng trúng đích. Nên việc cần làm là điều chỉnh cho đến khi trúng thì thôi.

Một vài lần thử đầu tiên không đúng theo ý của khách hàng, điều bạn cần làm là điều chỉnh sao cho đúng hơn. Vì bạn sử dụng những phương pháp lập trình tinh gọn (DRY …) nên việc chỉnh sửa sẽ nhanh và không tốn nhiều chi phí.

Những đoạn code phát quang vs Bản mẫu (prototyping)

Bạn sẽ làm bản mẫu khi cần khám phá một phần nhỏ trong hệ thống, như UI hoặc một thuật toán, một cấu trúc nào đó. Khi làm xong và kiểm thử nó chạy đúng, thường thì bạn sẽ bỏ chúng đi và xây dựng lại ở hệ thống thật.

Còn những đoạn code phát quang có mục tiêu khác, đó là: bạn cần biết ứng dụng tổng thể sẽ hoạt động như thế nào, bạn cần cho người dùng xem thao tác thực tế trên ứng dụng, và developer cần biết cấu trúc dự án để có thể thêm tính năng. Những đoạn code phát quang giống như là framework của hệ thống

Bản mẫu sẽ tạo ra những đoạn code có thể bị bỏ đi. Code phát quang tinh gọn và hoàn chỉnh, tạo nên sườn của hệ thống. Hãy coi bản mẫu là quá trình do thám và thu thập tin tình báo, trước khi viên đạn phát quan được bắn đi.

Bản mẫu và ghim chú

Ngôn ngữ nghiệp vụ

Canh đo thời gian