Coding – Đôi khi nguyên tắc là đừng quá nguyên tắc

Mình từng là đứa khá ám ảnh với việc viết code theo nguyên tắc (và có phần hơi cứng nhắc), nên trong bài viết này, mình sẽ chia sẻ những trải nghiệm hay ho đi ngược lại rập khuôn thường ngày. Cùng mình khám phá một vài trường khi quá tuân thủ nguyên tắc trong việc coding hóa ra lại không nên cho lắm nhé.

Khi một hàm làm nhiều hơn một việc

Xét một bài toán đơn giản: Viết hàm (hoặc các hàm) hỗ trợ tìm số lớn nhất và số nhỏ nhất trong mảng số không rỗng. Dĩ nhiên, bạn dễ dàng bắt tay ngay để viết một hàm tìm số lớn nhất và một hàm tìm số nhỏ nhất trong một mảng, mỗi lần như vậy cần một lần duyệt mảng và so sánh tất cả phần tử như sau:

// Hàm tìm số lớn nhất trong mảng
// Logic tương tự với tìm số nhỏ nhất
function getMaxInArr(arr) {
    max = arr[0]
    for num in arr {
        if num > max {
            max = num
        }
    }
    return max
}

Nhưng nếu mình nói có cách khiến bạn giảm số lần duyệt xuống một nửa và giảm cả tổng số lần so sánh bằng cách thực hiện như hàm dưới đây thì sao?

// Hàm tìm số lớn nhất và nhỏ nhất trong mảng
function getMaxAndMinInArr(arr) {
    max = arr[0]
    min = arr[0]
    for num in arr {
        if num > max {
            max = num
        } else if num < min {
            min = num
        }
    }
    return max, min
}

Dễ thấy, hàm trên bạn đã làm hơn một việc, nhưng chỉ cần một vòng lặp duy nhất để duyệt mảng, và đảm bảo tối ưu số lần check điều kiện hơn (vì khi đã gắn max thì không cần check min) so với việc dùng 2 hàm con getMaxgetMin.

Hiện tại các ngôn ngữ đã hỗ trợ việc có thể trả về nhiều giá trị trong một hàm, hoặc cung cấp một số cấu trúc dữ liệu có thể trả về nhiều giá trị (như C# có tuple, Java có Pair trong một số thư viện, Golang, Python support luôn cả việc trả về nhiều output…).

Và đây là lúc mình nhận ra đôi khi nguyên tắc mỗi hàm làm duy nhất một việc trong trường hợp này lại không chuẩn chỉ cho lắm.

Việc chia thành quá nhiều hàm con sẽ khiến code bạn gọn ghẽ hơn, nhưng đôi khi cũng có thể dẫn đến vài tác dụng phụ đau đầu khác như cách đặt tên hàm mới, viết thêm unit test không cần thiết với hàm không có tính tái sử dụng, giảm performance, các hàm con có thể phải truyền thêm các param chung…

Ngược lại, khi quyết định cài đặt một hàm cần xử lí nhiều hơn một việc, hãy cân nhắc đến độ phức tạp để tránh cho code trở nên quá dài và rối, hậu quả nhãn tiền là sẽ gây đau đầu cho đồng nghiệp (hoặc cho bạn ở tương lai) lúc đọc lại nữa.

Một chút comment khi code hơi phức tạp nhé

Khi đặt tên là nghệ thuật

Thực sự việc đặt tên cho một trường (field), một biến (variable), một hàm (function), một class hay một file chưa bao giờ là chuyện dễ dàng, và dĩ nhiên theo từng ngôn ngữ hay công nghệ sẽ có những chuẩn mực và nguyên tắc đặt tên (naming convention) riêng.

Nhưng đôi khi có những nguyên tắc cũng nên cân nhắc chỉnh sửa hoặc không cần phải tuân theo.

Trước đây khi nhắc đến việc đặt tên biến hay hàm là kiểu boolean thường sẽ bắt đầu bằng is như isCredentialValid, isUserEnable,… nhưng đôi khi việc này lại gây cứng nhắc trong việc đọc code một chút.

Ví dụ về một bài toán kiểm tra dữ liệu user có tồn tại, mình sẽ thoải mái hơn khi đọc theo cách đặt tên thứ ba (dĩ nhiên mình đọc cùng từ khóa if đứng trước nữa):

// Case 1
if isUserExisting(...) {}

// Case 2
if doesUserExist(...) {}

// Case 3
if userExists(...) {}

Đôi khi trong một số trường hợp ta nên “phá luật” một chút để code có thể tường minh hơn trong việc đọc, nhưng dĩ nhiên có một số những convention trong dự án nếu có thể quy về theo các chuẩn đã đề ra từ trước thì mình vẫn khuyên các bạn nên thực hiện theo.

Sau đây là một ví dụ mở khác về việc kiểm tra áp dụng mã giảm giá cho đơn hàng, đáp án của ví dụ này sẽ hoàn toàn phụ thuộc vào bạn và cách thống nhất của team trong việc code, vì nghệ thuật là không cứng nhắc và đa dạng mà =P

// Case 1
if isDiscountApplicableForOrder(...) {}

// Case 2
if shouldApplyDiscountForOrder(...) {}

// Case 3
if orderCanApplyDiscount(...) {}

Ngược lại, có những quy ước bạn vẫn cần phải đồng nhất trong cách đặt tên, như khi thao tác với database, hãy đảm bảo naming các phương thức đọc dữ liệu bắt đầu với get, read hay retrieve, thêm bắt đầu với insert, create hay add…, đừng sáng tạo hay phá cách vào những lúc này.

Đặt tên cũng đau đầu phết

Khi design pattern là không cần thiết

Design pattern thực sự là một nghệ thuật, tuy nhiên sẽ khá cồng kềnh khi áp dụng và có thể phản tác dụng nếu sử dụng không đúng, những bài toán chỉ đơn giản có thể xử lý bằng việc chia nhỏ function để tái sử dụng kết hợp với error handling vốn có sẵn trong ngôn ngữ, mình nghĩ cũng không nên suy nghĩ quá nhiều về việc apply design pattern một cách vô tội vạ như Chain Of Responsibility hay Command pattern thêm vào làm gì.

Mặt khác, các ngôn ngữ và framework theo thời gian đã hỗ trợ nhiều khả năng giúp ta đỡ tốn thời gian code lại nhiều design pattern (như sự ra đời của object/struct initialization giúp bạn không cần dùng Builder pattern nữa).

Ví dụ trực quan về lạm dụng design pattern

Dù vậy cũng tồn tại những kiến trúc code, mô hình (về bản chất vẫn là design pattern) đến nay vẫn còn nổi tiếng và được áp dụng rộng rãi như MVC, MVVM, Repository, Dependency Injection…, đây là những “tay to” mà bạn có thể xem xét để áp dụng trong project nếu có thể.

Khi giết gà thì không cần dao mổ trâu

Trong giới lập trình tồn tại thuật ngữ gọi là overkill – khi bạn sử dụng một tính năng, công nghệ hay kĩ thuật nào đó phức tạp cho một vấn đề đơn giản, sau đây là một số tip nhỏ mà mình đã học “hửi” từ nhiều anh/chị khác để né overkill:

  • Nếu bạn cần một số hàm hỗ trợ các bài toán đơn giản và có thể tái sử dụng nhiều nơi và logic không phụ thuộc vào các object hay class khác, hãy tạo các static method trong class Helper (hoặc Util) thay vì sử dụng Dependency Injection.
  • Như phần trên, đừng lạm dụng design pattern cho các vấn đề đơn giản hoặc vốn đã được ngôn ngữ hay công nghệ hỗ trợ.
  • Đừng tốn thời gian phân tích và viết code xử lí cho những trường hợp đã được đảm bảo không xảy ra, ví dụ khi hàm xử lí của bạn đã được đảm bảo sẽ được sử dụng với input array không rỗng và luôn khác null, thời gian cho các dòng code cho các câu lệnh điều kiện, log hay error handling vào đó là thừa thãi.
  • Không nên optimize function chỉ để tăng performance xử lí từ 1s xuống 0.9s nhưng lại khiến code quá cồng kềnh, khó hiểu, áp dụng giải thuật phức tạp nhưng thiếu kiểu chứng.
  • Góc cá nhân: khi làm game bằng DirectX, mình đã được hướng dẫn nên để tất cả các thuộc tính của các class là public vì việc truy cập thông qua getter, setter nếu chỉ để thực hiện get/set dữ liệu là khá cồng kềnh, đặc biệt khi làm game thì việc render mỗi khung hình cần khối lượng xử lí rất lớn và phải luôn thường xuyên truy cập vào thuộc tính của các đối tượng.

Và còn nhiều ví dụ nữa, bạn sẽ nhận ra những gì bạn làm sẽ hơi hướng “overkill” – là khi bạn quá tốn sức áp dụng những kĩ thuật hay công nghệ phức tạp một cách không cần thiết và còn gây khó hiểu cho đồng nghiệp, hoặc khi dùng các công nghệ phức tạp chỉ để xây dựng ứng dụng đơn giản nhưng lại không đủ resource (thời gian, nhân lực, budget, trình độ) để triển khai, lúc đó hãy xem xét “quay đầu là bờ” để tránh hậu họa.

Không sao cả vì mình cũng thế thôi 😀

Kết

Vạn vật luôn thay đổi, công nghệ trong lập trình hay requirement cũng vậy, vì code của bạn sẽ luôn cần được thêm mới hay maintain theo thời gian, hãy cân nhắc khi nào nên rập khuôn và khi nào nên mềm dẻo một chút trong việc giải quyết vấn đề nhé =P

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s