Chapter 10: Extracting Unrelated Subproblems

Kĩ thuật là tất cả về việc chia nhỏ vấn đề lớn thành những vấn đề nhỏ hơn và đưa ra các giải pháp cho vấn đề đó lại với nhau. Áp dụng nguyên tắc này làm code dễ dàng đọc hơn.

Lời khuyên của chương này là chủ động xác định và trích xuất các bài toán con không liên quan. Điều đó có nghĩa:

  1. Nhìn vào 1 hàm và khối code và tự hỏi “Mục đích ở mức high-level của đoạn code này?”
  2. Với mỗi dòng code, hỏi “Nó có làm trực tiếp mục đích đó? Không thì nó đang giải quyết các vấn đề con không liên quan cần thiết để đáp ứng mục đích đó?”
  3. Nếu đủ các dòng đang giải quyết các vấn đề con không liên quan, trích xuất code này vào trong các hàm tách biệt.

Trích xuất code vào các hàm riêng việt có lẽ là việc bạn làm hàng ngày. Nhưng trong chương này, chúng ta quyết định tập trung vào trường hợp riêng và trích xuất vào các vấn đề con không liên quan” nơi mà trích xuất code không biets tại sao nó lại được gọi.

Như bạn thấy, đây là 1 kĩ thuật dễ dàng để áp dựng nhưng cải thiện code của bạn đáng kể. Nhưng vì 1 số lý do nào đó, các lập trình viên không sử dụng đủ kỹ thuật này. Bí quyết (Mẹo) này chủ động nhìn cho các vấn đề con không liên quan.

Trong chương này chúng ta sẽ cùng xem xét 1 vài ví dụ minh họa kĩ thuật này để giải quyết các vấn đề khác nhau bạn có thể gặp phải.

Introductory Example: findClosestLocation()

Mục đích chính của đoạn code JavaScript dưới đây là tìm kiếm vị trí gần nhất của 1 điểm” (đừng chú ý quá vào phần địa lý, phần in nghiêng):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Return which element of 'array' is closest to the given latitude/longitude.
// Models the Earth as a perfect sphere.
var findClosestLocation = function (lat, lng, array) {
var closest;
var closest_dist = Number.MAX_VALUE;
for (var i = 0; i < array.length; i += 1) {
// Convert both points to radians.
var lat_rad = radians(lat);
var lng_rad = radians(lng);
var lat2_rad = radians(array[i].latitude);
var lng2_rad = radians(array[i].longitude);

// Use the "Spherical Law of Cosines" formula.
var dist = Math.acos(Math.sin(lat_rad) * Math.sin(lat2_rad) +
Math.cos(lat_rad) * Math.cos(lat2_rad) *
Math.cos(lng2_rad - lng_rad));
if (dist < closest_dist) {
closest = array[i];
closest_dist = dist;
}
}
return closest;
};

Đa số code trong vòng lặp làm những việc cho các vấn đề con không liên quan:
tính toán khoảng cách hình cầu giữa 2 điểm lat/long*. Vì rất nhiều code làm việc này, ý nghĩa hơn bạn nên trích xuất nó thành 1 hàm riêng với tên spherical_distance():
1
2
3
4
5
6
7
8
9
10
11
var spherical_distance = function (lat1, lng1, lat2, lng2) {
var lat1_rad = radians(lat1);
var lng1_rad = radians(lng1);
var lat2_rad = radians(lat2);
var lng2_rad = radians(lng2);

// Use the "Spherical Law of Cosines" formula.
return Math.acos(Math.sin(lat1_rad) * Math.sin(lat2_rad) +
Math.cos(lat1_rad) * Math.cos(lat2_rad) *
Math.cos(lng2_rad - lng1_rad));
};

Bây giờ đoạn code còn lại trở thành:
1
2
3
4
5
6
7
8
9
10
11
12
var findClosestLocation = function (lat, lng, array) {
var closest;
var closest_dist = Number.MAX_VALUE;
for (var i = 0; i < array.length; i += 1) {
var dist = spherical_distance(lat, lng, array[i].latitude, array[i].longitude);
if (dist < closest_dist) {
closest = array[i];
closest_dist = dist;
}
}
return closest;
};

Đoạn code này đã dễ dàng đọc hơn vì người đọc có thể tập trung vào mục đích cao nhất của nó mà không bị phân tâm vào việc tính khoảng cách địa lý.

Như 1 phần thưởng bổ sung, spherical_distance() sẽ dễ dàng để test cô lập. Và spherical_distance() là 1 loại hàm có thể được sử dụng lại trong tương lai. Điều này lý giải vì sao nó là các vấn đề con “không liên quan” - nó hoàn toàn tự chứa chính nó và không biết ứng dụng sử dụng nó như thế nào.

Pure Utility Code (Phần này kiểu hepler, codebase cung cấp các code tiện ích ấy)

Đây là bộ trung tâm của các nhiệm vụ cơ bản đa số chương trình cần làm, như các thao tác chuỗi, sử dụng hash tables và đọc/ghi files.

Thường xuyên, các “tiện ích cơ bản” này đã thực thi các built-in libraries trong ngôn ngữ lập trình. Ví dụ, nếu bạn muốn đọc nội dung file trong PHP, bạn có thể sử dụng hàm file_get_contents("filename") hoặc với Python, bạn có thể sử dụng open("filename").read().

Nhưng đôi khi bạn phải tự điền vào chỗ trống. Trong C++, ví dụ, không có cách ngắn gọn nào đọc 1 file đầu vào. Thay vào đó, bạn chắn chắn phải viết code như sau:

1
2
3
4
5
6
7
8
9
10
11
12
13
ifstream file(file_name);

// Calculate the file's size, and allocate a buffer of that size.
file.seekg(0, ios::end);
const int file_size = file.tellg();
char* file_buf = new char [file_size];

// Read the entire file into the buffer.
file.seekg(0, ios::beg);
file.read(file_buf, file_size);
file.close();

...

Đây là ví dụ cơ bản để hiểu các vấn đề không liên quan nên được trích xuất vào 1 hàm riêng với tên gọi ReadFileToString(). Bây giờ phàn còn lại codebase của bạn có thể gọi như C++ đã có hàm ReadFileToString().

Nói chung nếu bạn thấy mình đang nghĩ “Tôi muốn thư viện của chúng ta có hàm XYZ()”, thì hãy viết nó! (Giả định là nó chưa tồn tại). Theo thời gian, bạn sẽ xây dựng được 1 tập các mã tiện ích có thể sử dụng trên các dự án.

Other General-Purpose Code

Khi debugging JavaScript, các lập trình viên thường sử dụng alert() để hiển thị 1 thông báo chứa các thông tin họ cần, nó là phiên bản “printf() debugging” của Web. Ví dụ, hàm sao gọi submits dữ liệu tới server sử dụng Ajax và sau đó hiển thị trực tiếp kết quả nhận được từ server:

1
2
3
4
5
6
7
8
9
10
11
12
13
ajax_post({
url: 'http://example.com/submit',
data: data,
on_success: function (response_data) {
var str = "{\n";
for (var key in response_data) {
str += " " + key + " = " + response_data[key] + "\n";
}
alert(str + "}");

// Continue handling 'response_data' ...
}
});

Mục đích chính của hàm này là Tạo một lời gọi Ajax trên server và xử lý response”. Nhưng rất nhiều code lại đang xử lý vấn đề con không liên quan, Pretty-print a dictionary*. Vậy nên dễ dàng tách đoạn code này vào 1 hàm như format_pretty(obj):
1
2
3
4
5
6
7
var format_pretty = function (obj) {
var str = "{\n";
for (var key in obj) {
str += " " + key + " = " + obj[key] + "\n";
}
return str + "}";
};

Unexpected Benefits

Có rất nhiều lý do mà trích xuất format_pretty() là 1 ý tưởng tốt. Nó làm lời gọi code đơn giản hơn và format_pretty() có những chức năng tiện dụng xung quanh.

Nhưng có những lý do tuyệt vời khác không rõ ràng: dễ dàng cải tiến ormat_pretty() khi mà code là của chính nó. Khi bạn đang làm việc với 1 hàm nhỏ hơn không liên quan, cảm giác dễ dàng để thêm tính năng, cải thiện khả năng đọc code, xem xét các trường hợp cạnh và nhiều hơn nữa.

Đây là 1 vài trường hợp format_pretty(obj) không xử lý:

  • Nó mong đợi obj là 1 đối tượng. Nếu thay vì đó nó là 1 chuỗi (hoặc undefined) đoạn code hiện tại có thể ném ra 1 ngoại lệ
  • Nó mong đợi mỗi giá trị trong obj là kiểu đơn giản. Nếu thay vì nó chứa các object lồng nhau, đoạn code hiện tại sẽ hiển thị [object Object] có vẻ không đẹp.

Trước khi bạn tách format_pretty() từ hàm chủ của nó, nó sẽ cảm thấy rất nhiều việc cần phải cải thiện. (Trên thực tế, việc in đệ quy đối tượng lồng nhau rất khó mà không có 1 hàm tách biệt)

Nhưng bây gườ, thêm hàm này thì việc đó sẽ dễ dàng. Hãy cùng xem cách cải tiến đoạn code đó:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var format_pretty = function (obj, indent) {
// Handle null, undefined, strings, and non-objects.
if (obj === null) return "null";
if (obj === undefined) return "undefined";
if (typeof obj === "string") return '"' + obj + '"';
if (typeof obj !== "object") return String(obj);

if (indent === undefined) indent = "";

// Handle (non-null) objects.
var str = "{\n";
for (var key in obj) {
str += indent + " " + key + " = ";
str += format_pretty(obj[key], indent + " ") + "\n";
}
return str + indent + "}";
};

Đoạn code trên đã sửa được những thiết sót được liệt kê ở trên và tạo đầu ra như sau:
1
2
3
4
5
6
7
8
9
10
11
{
key1 = 1
key2 = true
key3 = undefined
key4 = null
key5 = {
key5a = {
key5a1 = "hello world"
}
}
}

Create a Lot of General-Purpose Code

Hàm ReadFileToString()format_pretty() là những ví dụ tuyệt vời cho các vấn đề con không liên quan. Nó rất cơ bản và áp dụng rộng rãi và được sử dụng lại trong các dự án. Codebase thường xuyên có những thư mục đặc biệt dành cho những đoạn code như vậy (ví dụ như util/) do đó bạn có thể dễ dàng chia sẻ.

Code cho mục đích chung là tốt vì nó đã hoàn toàn tách rời phần còn lại của project. Code như vậy dễ dàng phát triển, dễ dàng kiểm thử và dễ dàng để hiểu. Nếu tất cả các code của bạn chỉ nên như vậy!!

Suy nghĩ nhiều hơn về sức mạnh của các thư viện và hệ thống bạn sử dụng, như SQL databases, JavaScript libraries, và HTML. Bạn không phải lo lắng về những thứ bên trong nó - các codebase này hoàn toàn độc lập với project của bạn. Và kết quả, phần còn lại codebase project bản bạn sẽ nhỏ.

Càng nhiều dự án của bạn, bạn có thể tách ra làm thư viện bị cô lập thì càng tốt, bởi vì phần còn lại của mã của bạn sẽ nhỏ hơn và dễ suy nghĩ hơn.

IS THIS TOP-DOWN OR BOTTOM-UP PROGRAMMING?
Top-down programming is a style where the highest-level modules and functions are designed first and the lower-level functions are implemented as needed to support them.

Bottom-up programming tries to anticipate and solve all the subproblems first and then build the higher-level components using these pieces.

This chapter isn’t advocating one method over the other. Most programming involves a combination
of both. What’s important is the end result: subproblems are removed and tackled separately.

Project-Specific Functionality

Về ý tưởng, các vấn đề con bạn trích xuất có thể hoàn toàn không theo dự án. Nhưng thậm chí như vậy, nó vẫn okay. Phá vỡ các vấn đề con vẫn là làm việc tuyệt vời.

Đây là 1 ví dụ từ 1 website đánh giá kinh doanh. Đoạn code Python tạo đối tượng Business và set name, urldata_creadted cho nó:

1
2
3
4
5
6
7
8
9
10
11
business = Business()
business.name = request.POST["name"]

url_path_name = business.name.lower()
url_path_name = re.sub(r"['\.]", "", url_path_name)
url_path_name = re.sub(r"[^a-z0-9]+", "-", url_path_name)
url_path_name = url_path_name.strip("-")
business.url = "/biz/" + url_path_name

business.date_created = datetime.datetime.utcnow()
business.save_to_database()

url giả định là bản “clean” của name. Ví dụ, nếu name là “A.C. Joe’s Tire & Smog, Inc.” thì url sẽ là “/biz/ac-joes-tire-smog-inc”.

Vấn đề con không liên quan ở đây là: biến tên thành 1 URL hợp lệ. Chúng ta có thể trích xuất đoạn code này:

1
2
3
4
5
6
7
8
CHARS_TO_REMOVE = re.compile(r"['\.]+")
CHARS_TO_DASH = re.compile(r"[^a-z0-9]+")

def make_url_friendly(text):
text = text.lower()
text = CHARS_TO_REMOVE.sub('', text)
text = CHARS_TO_DASH.sub('-', text)
return text.strip("-")

Bây giờ đoạn code ban đầu có các mẫu “thông thường” nhiều hơn:
1
2
3
4
5
business = Business()
business.name = request.POST["name"]
business.url = "/biz/" + make_url_friendly(business.name)
business.date_created = datetime.datetime.utcnow()
business.save_to_database()

Đoạn code này yêu cầu ít thời gian hơn để đọc vì bạn không bị mất tập trung vào các biểu thức chính quy và các xử lý string.

Bạn nên đặt code cho make_url_friendly() ở chỗ nào? Nó dường như là 1 hàm khác chung, do đó hợp lý thì nó nên đặt trong thư mục riêng biệt util/. Mặt khác, biểu thức chính quy này được thiết kế với tên doanh nghiệp U.S, do đó có thể đoạn code nên trong file nó đang sử dụng. Thực tế điều đó không quan trọng, và bạn có thể dễ dàng chuyển thư mục sao đó. Điều quan trọng ở đây mà hàm make_url_friendly() được tách ra.

Simplifying an Existing Interface

Mọi người yêu thích khi thư viện có gian diện rõ ràng - 1 trong số đó là có vài tham số, không cần cài đặt niều và thường ít cần tìm hiểu. Nó làm cho code của bạn nhìn thanh lịch: đơn giản và mạnh mẽ cùng 1 thời điểm.

Nhưng nếu interface bạn đang sử dụng không rõ ràng, bạn có thể làm có các hàm “bọc” nó

Ví dụ làm việc với browser cookie trong JavaScript. Về khái niệm, cookies là 1 tập các cặp key/value. Nhưng interface mà browser cung cấp chỉ có chuỗi document.cookie như sau:

1
name1=value1; name2=value2; ...

Để tìm cookie bạn muốn, bạn buộc phải chuyển chuỗi khổng lồ này. Đây là 1 ví dụ code đọc giá trị từ cookie với tên “max_results”:
1
2
3
4
5
6
7
8
var max_results;
var cookies = document.cookie.split(';');
for (var i = 0; i < cookies.length; i++) {
var c = cookies[i];
c = c.replace(/^[ ]+/, ''); // remove leading spaces
if (c.indexOf("max_results=") === 0)
max_results = Number(c.substring(12, c.length));
}

Wow, 1 đoạn mã xấu xí. Tõ ràng, có 1 hàm get_cookie() chờ đợi chúng ta làm để chúng ta chỉ cần viết:

1
var max_results = Number(get_cookie("max_results"));

Tạo hoặc thay đổi giá trị cookie thậm chí là xa lạ. Bạn phải sẻ document.cookie giá trị với cú pháp:
1
document.cookie = "max_results=50; expires=Wed, 1 Jan 2020 20:53:47 UTC; path=/";

Câu lệnh nhìn như nó đang ghi đè tất cả các cookies đã tồn tại khác nhưng (magically), nó không làm vậy.

Một interface lý tưởng để set 1 cookie sẽ như sau:

1
set_cookie(name, value, days_to_expire);

Xóa bỏ 1 cookie cũng không trực quan: bạn phải set cookie về hết hạn trong quá khứ. Thay vì đó, một interface lý tưởng nhìn đơn giản như sau:
1
delete_cookie(name);

Bài học ở đây là bạn không bao giờ phải giải quyết cho một giao diện mà ít hơn lý tưởng. Bạn có thể luôn luộn tạo các hàm bọc để ẩn các chi tiết xấu của 1 interface bạn bị mắc kẹt.

Reshaping an Interface to Your Needs

Rất nhiều code trong 1 chương trình chỉ hỗ trợ code khác - ví dụ, thiết lập đầu vào cho 1 hàm hoặc xử lý đầu ra. Code “keo dán” này thường không làm gì với logic thực tế trong chương trình của bạn. Những đoạn mã như vậy là 1 ứng cử viên tuyệt vời để kéo ra vào 1 hàm riêng biệt.

Ví dụ, hãy nói bạn có một Python dictionary chứa thông tin nhạy cảm của user như { "username": "...", "password": "..." } và bạn cần đưa tất cả thông tin đó vào trong URL. Bởi vì nó nhạy cảm, bạn quyết định mã hóa từ điển này, sử dụng Cipher class.

Nhưng Cipher mong đợi chuỗi các bytes làm đầu vào, không phải 1 dictionary. Và Cipher trả về chuỗi bytes, nhưng chúng ta cần URL-safe. Cipher cũng nhận vào 1 vào tham số thêm và khá cồng kềnh để sử dụng.

Những gì bắt đầu như 1 nhiệm vụ đơn giản, trong đó có rất nhiều code kết dính:

1
2
3
4
5
6
7
user_info = { "username": "...", "password": "..." }
user_str = json.dumps(user_info)
cipher = Cipher("aes_128_cbc", key=PRIVATE_KEY, init_vector=INIT_VECTOR, op=ENCODE)
encrypted_bytes = cipher.update(user_str)
encrypted_bytes += cipher.final() # flush out the current 128 bit block
url = "http://example.com/?user_info=" + base64.urlsafe_b64encode(encrypted_bytes)
...

Mặc dù vấn đề chúng ta đang giải quyết là mã hóa thông tin user vào 1 URL, phần lớn đoạn code này chỉ làm mã hóa đối tượng Python vào chuỗi URL thân thiện. Sẽ đơn giản để thêm vấn đề con:
1
2
3
4
5
6
def url_safe_encrypt(obj):
obj_str = json.dumps(obj)
cipher = Cipher("aes_128_cbc", key=PRIVATE_KEY, init_vector=INIT_VECTOR, op=ENCODE)
encrypted_bytes = cipher.update(obj_str)
encrypted_bytes += cipher.final() # flush out the current 128 bit block
return base64.urlsafe_b64encode(encrypted_bytes)

Sau đó, kết quả code thực thi logic thực sự trong chương trình chỉ đơn giản:

1
2
user_info = { "username": "...", "password": "..." }
url = "http://example.com/?user_info=" + url_safe_encrypt(user_info)

Taking Things Too Far: Đưa mọi thứ đi quá xa

Như chúng ta đã nói từ đầu chương, mục đích của chúng ta là “tích cực xác định và trích xuất các vấn đề con không liên quan”. Chúng ta nói “tích cực” vì đa số người viết code không đủ tích cực. Nhưng nó có thể nhận được quá mức và đưa mọi thứ đi quá xa.

Ví dụ, đoạn code từ phần trước có thể phá vỡ nhiều hơn, như sau:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
user_info = { "username": "...", "password": "..." }
url = "http://example.com/?user_info=" + url_safe_encrypt_obj(user_info)

def url_safe_encrypt_obj(obj):
obj_str = json.dumps(obj)
return url_safe_encrypt_str(obj_str)

def url_safe_encrypt_str(data):
encrypted_bytes = encrypt(data)
return base64.urlsafe_b64encode(encrypted_bytes)

def encrypt(data):
cipher = make_cipher()
encrypted_bytes = cipher.update(data)
encrypted_bytes += cipher.final() # flush out any remaining bytes
return encrypted_bytes

def make_cipher():
return Cipher("aes_128_cbc", key=PRIVATE_KEY, init_vector=INIT_VECTOR, op=ENCODE)

Cho tất cả các tiny functions này thực sự làm tổn thương khả năng đọc vì người đọc phải theo dõi quá nhiều và theo dõi con đường sự thực thi đòi hỏi phải nhảy xung quanh.

Đây là 1 chi phí nhỏ (nhưng hữu hình) khả năng đọc code của việc thêm 1 hàm mới vào code của bạn. Trong trường hợp trước, không có gì đạt được để bù đắp chi phí này. Nó có lý thêm các hàm nhỏ hơn này nếu chúng cần bởi các phần khác trong project của bạn. Nhưng cho đến lúc đó, không có nhu cầu.

Summary

1 cách đơn giản để nghĩ về chương này là tách biệt các chung mã (generic code) ra khỏi code cụ thể của project.. Hóa ra, đa số code đều là chung. Bằng cách xây dựng 1 bộ thư viện lớn và các helper functions để giải quyết các vấn đề chung, những gì còn lại sẽ thực sự là mã chính nhỏ cái mà làm cho chương trình của bạn trở nên độc đáo.

Lý do chính kĩ thuật này giúp các lập trình viên tập trung vào các vấn đề nhỏ hơn, xác định rõ ràng hơn cái mà được tách biệt ra khỏi phần còn lại trong project của bạn. Kết quả là, giải quyết các vấn đề không liên quan có xu hướng kỹ lưỡng và đúng đắn hơn. Bạn cũng có thể sử dụng lại chúng sau đó.

FURTHER READING: ĐỌC THÊM

Martin Fowler’s Refactoring: Improving the Design of Existing Code (Fowler et al., Addison-Wesley Professional, 1999) describes the “Extract Method” of refactoring and catalogs many other ways to refactor your code.

Kent Beck’s Smalltalk Best Practice Patterns (Prentice Hall, 1996) describes the “Composed Method Pattern,” which lists a number of principles for breaking down your code into lots of little functions. In particular, one of the principles is “Keep all of the operations in a single method at the same level of abstraction.”

These ideas are similar to our advice of “extracting unrelated subproblems.” What we discussed in this chapter is a simple and particular case of when to extract a method.

Author

Ming

Posted on

2020-05-24

Updated on

2021-04-10

Licensed under

Comments