Wednesday, 8 August 2012

Viết code - Refactor [Part 1]

Lập trình cực đơn giản, ai cũng có thể lập trình được thậm chí cả "DUMMY" cũng có thể lập trình được trong vòng 21 ngày :)) [1]

Vậy nhưng để viết được một đoạn code tốt (theo nhiều tiêu chuẩn khác nhau) thì lại là cả 1 nghệ thuật. Những phương pháp tối ưu giải quyết một vấn đề CỤ THỂ nếu là vấn đề nhiều người gặp phải thì được đút kết thành các "pattern".
Chúng ta thường có thể đưa ngay ra những giải pháp "nóng" để giải quyết vấn đề. Nhưng nó hiều khi không phải giải pháp tối ưu (thậm chí là tối ưu cục bộ :D ) [2]

Việc viết lại 1 đoạn code cho đẹp, tiện, tốt, tối ưu hơn gọi là refactor. Và sau đây tớ sẽ lấy ví dụ về 3 đoạn code hôm nay tớ viết. Sẽ không dùng 1 ngôn ngữ riêng biệt nào hết mà tớ sẽ dùng kiểu "giả mã HVN" =))

Vấn đề:
- Cần đọc 1 file config để lấy nội dung trong đó.
- Viết 1 số hàm lọc những gì cần thiết lấy từ file config nói trên.



Đoạn code đọc file config (không đổi):
func read_config(filename):
{
 ....
return a_structure_contain_all_content_from_config
}

Còn sau đây là đoạn code lấy thông tin cần thiết  từ giá trị trả về của hàm trên để sử dụng vào các mục đích khác nhau:

Code1: BAD
func i_will_use_parsed_config(username):
{
parsed = read_config("config")
do_some_thing(username, parsed)
}

func me_too(passwd):
{
parsed = read_config("config")
do_other_thing(passwd, parsed)

}

Phân tích đoạn Code 1:
Mỗi lần gọi 2 hàm này là bạn sẽ phải đọc file config 1 lần -> chậm. Khi file config đổi tên, bạn sẽ phải sửa tay từng hàm.

Code2: Still BAD

func i_will_use_parsed_config(username, parsed):
{

do_some_thing(username, parsed)

}


func me_too(passwd, parsed):
{
do_other_thing(passwd, parsed)}


Phân tích:
Mỗi lần gọi 2 hàm trên, bạn sẽ phải gán(pass) tham số parsed.
Ưu điểm: chỉ phải đọc file config 1 lần rồi gán kết quả đó cho các hàm sử dụng
Nhược điểm: nếu 2 hàm trên là 2 hàm dùng trực tiếp (không có hàm nào gọi nó) thì còn tạm chấp nhận được. Nếu có 1 hàm sử dụng 1 trong 2 hàm trên, nó sẽ phải gán tham số có vẻ không liên quan gì
Ví dụ :
vẽ_con_chim(dài, rộng, màu, parsed_config)
vẽ_đàn_chim(số_con, dài, rộng, màu, parsed_config)

rõ ràng là rất vô lý, và việc gõ lại parsed_config rất nhàm chán, hàm của bạn cũng phải gán quá nhiều tham số.

Code3 : BETTER

global parsed = read_config("config")

func i_will_use_parsed_config(username):
{
do_some_thing(username, parsed)

}


func me_too(passwd):
{

do_other_thing(passwd, parsed)

}


Phân tích:

Đoạn code này có thể coi là tốt trong trường hợp cụ thể này, nhưng có thể không tốt trong trường hợp khác. Ở đây ta sử dụng 1 biến Global, như vậy chỉ cần đọc file config 1 lần và cũng không cần phải gán các tham số "VÔ LÝ" cho các hàm nữa. Thế nhưng phương án này PHỤ THUỘC VÀO NGÔN NGỮ BẠN ĐANG DÙNG, và cũng PHỤ THUỘC VÀO BẠN ĐỊNH LÀM GÌ VỚI NỘI DUNG BIẾN parsed.
Nếu bạn chỉ đọc file config để lấy thông tin (chứ KHÔNG thay đổi gì nội dung của nó sau khi đọc ) thì sử dụng biến global là hợp lý. Còn khi bạn có ý định thay đổi nội dung của parsed , việc dùng global rất dễ sinh lỗi, khó kiểm soát và debug.
Sử dụng biến global vốn "nổi tiếng" là bad practice [3]. Thế nhưng trong những trường hợp nhất định, nó chính là giải pháp tối ưu nhất (NẾU KHÔNG NGƯỜI TA SINH RA NÓ (VÀ TIẾP TỤC GIỮ ĐẾN GIỜ )-  LÀM GÌ?)

Trên đây là 1 ví dụ về quá trình refactor lại đoạn code tớ viết hôm nay :D
Mọi người hãy cùng phân tích điểm tốt/ xấu của 3 đoạn code trên , nhóe >:P

[1] http://norvig.com/21-days.html
[2] http://www.python.org/dev/peps/pep-0020/
[3] http://c2.com/cgi/wiki?GlobalVariablesAreBad