A Pragmatic Approach

Mấy bạn mới thường nghe thấy và được bảo là phải chia các code thành component code, để dễ test, dễ thay đổi.
Đặt ra câu hỏi là Làm vậy để làm gì? Được lợi ích gì? Nhược điểm là gì?


Trong lập trình, thì yêu cầu từ khách hàng có thể thay đổi liên tục. Hoặc chính bản thân mình khi code lúc đầu thấy cách này ổn, nhưng lần tới thấy nó không ổn, có cách khác hiệu quả hơn, nên thay đổi code.
--> Bản chất là nó thường thay đổi.
Do đó, cái mình cần làm là giảm thiểu công việc cần phải làm khi yêu cầu thay đổi. Thiết kế, cấu trúc dễ thay đổi.
Yêu cầu A và yêu cầu mới B, chỉ khác nhau một điểm nhỏ C, vậy làm sao để mình chỉ cần thay mỗi một phần nhỏ C, thay vì đập A đi xây mới B. Đó là lý do nó sinh ra các pattern, các best practice, chia code.

Hãy nghĩ về giá trị thay vì rule.

mình sử dụng phương pháp này, cách này có làm hệ thống dễ hay khó thay đổi hơn?

Hãy nghĩ về giá trị mà nó mang lại, thay vì cứng nhắc làm theo rule. Để xác định giá trị của nó cần thời gian và kinh nghiệm. Mình chưa biết cách đó mang lại giá trị tốt hơn hay khó hơn, nên mình sẽ cần phải hỏi người có kinh nghiệm hơn, hoặc tìm hiểu từ các nguồn khác: sách, báo, internet. Và cuối cùng không được thì mình hãy chọn cái dễ mà làm (tất nhiên phải giữ code dễ thay đổi, chứ không phải hard-code).

Mình phải có baseline để đánh giá cái nào tốt hơn cái nào.

#TODO tìm hiểu so sánh các design principle. Các cái đó mang lại giá trị gì, giải quyết vấn đề gì, và có dễ dàng thay đổi hay không.


Một cái mọi người hay lầm tưởng là về Duplicated code. Hai đoạn code giống nhau thường bị xem là một, nên được gom về chung tại một chỗ. Đây là sai lầm, 2 đoạn giống nhau nhưng nó dùng cho hai mục đích khác nhau thì nó nên được chia thành 2 thành phần riêng biệt.

Để xác định 2 đoạn có "giống nhau" hay không, có nên gom chúng lại với nhau hay không, ta nên xét theo "ý nghĩa" của chúng (ở đây có thể gọi lạ piece of knowledge).

Don't Repeat Yourself (DRY)

DRY nói về sự trùng lắp knowledge, intent. Một đoạn mô tả một thứ được đặt ở hai nơi khác nhau.
Ví dụ so sánh duplicated code và duplicated knowledge.

Duplicated code chưa chắc là duplicated knowledge, và ngược lại. Ở dưới, mặc dù hai đoạn code nhìn giống nhau, nhưng ý nghĩa của nó là validate 2 thực thể khác nhau, chúng chỉ tình cờ có cùng rule. Rule của một cái có thể bị thay đổi và không liên quan đến cái kia. Nếu gom lại thì mình đang couple chúng lại với nhau.

def validate_age(value): 
	validate_type(value, :integer) 
	validate_min_integer(value, 0) 
def validate_quantity(value): 
	validate_type(value, :integer) 
	validate_min_integer(value, 0)

Làm sao để phát hiện DRY violation?

Nếu một đoạn code, một hàm phải thay đổi, mình cũng cần phải cập nhật lại ở một số chỗ khác nữa, như hàm ở file khác, trong document, API comment, database schema thì có nghĩa là code hiện tại đang violate DRY.

Ví dụ duplication trong document (comment). Nếu logic của tính phí thay đổi, lúc này cũng phải thay đổi cả trong comment --> 2 nơi cần update.

# Calculate the fees for this account. 
# 
# * Each returned check costs $20 
# * If the account is in overdraft for more than 3 days, 
# charge $10 for each day 
# * If the average account balance is greater that $2,000 
# reduce the fees by 50% 
def fees(a) 
	f = 0 
	if a.returned_check_count > 0 
		f += 20 * a.returned_check_count 
	end 
	if a.overdraft_days > 3 
		f += 10*a.overdraft_days 
	end 
	if a.average_balance > 2_000 
		f /= 2 
	end 
	f 
end

Giải pháp: có thể không cần phải comment, đặt tên biến và cấu trúc code sao cho dễ hiểu là đủ.

def calculate_account_fees(account) 
	fees = 20 * account.returned_check_count 
	fees += 10 * account.overdraft_days if account.overdraft_days > 3 
	fees /= 2 if account.average_balance > 2_000 
	fees 
end

Ngoài ra cũng có một số violcation trong Data, khi các field giữ cùng một tin. Ví dụ dưới, giả sử length được tính bằng $length = start - end$. Thì ở đây ta có hai nơi lưu trữ cùng một thông tin. Khi cập nhật start/end ta phải cập nhật lại length, nếu quên thì dẫn đến conflict dữ liệu.

class Line 
{ 
	Point start; 
	Point end; 
	double length; 
};

Để giải quyết có thể sử dụng hàm getLength = () => start - end thì khi đó sẽ tránh được trường hợp conflict dữ liệu.
Cách này cũng sẽ không tốt nếu app mình cần hiệu suất cao. Nên có thể thay thế bằng cách đóng gói, giới hạn việc set start/end, không cho phép cập nhật trực tiếp mà phải thông qua hàm được định sẵn.

def calculateLength()
{
	length = abs( start - end);
}

def setStart(Point newStart)
{
	start = newStart;
	calculateLength();
}

Ngoài ra, một duplication mình hay gặp là interface giữa API server và frontend. Để giải quyết có thể sử dụng API SDK, import interface từ SDK rồi sử dụng.

Make it Easy to Reuse

Nếu nó không dễ sử dụng, người ta sẽ không xài nó.