ứng dụng kỹ thuật tái cấu trúc mã nguồn để triển khai dò tìm và cải tiến các đoạn
Trang 1MÃ XẤU TRONG CHƯƠNG TRÌNH C#
Họ tên CBHD : TS.NGUYỄN THANH BÌNH
ĐÀ NẴNG, 11/2008
Trang 2LỜI CAM ĐOAN
Tôi xin cam đoan nội dung luận văn "Ứng dụng kỹ thuật tái cấu trúc mã nguồn để triển khai dò tìm và cải tiến các đọan mã xấu trong chương trình C# ", dưới sự hướng dẫn của TS Nguyễn Thanh Bình, là công trình do tôi trực tiếp nghiên cứu
Tôi xin cam đoan các số liệu, kết quả nghiên cứu trong luận văn là trung thực và chưa từng được công bố trong bất cứ công trình nào trước đây
Tác giả
Nhiêu Lập Hòa
Trang 3CHƯƠNG I: KỸ THUẬT TÁI CẤU TRÚC MÃ NGUỒN (REFACTORING) 7
I.1 ĐỊNH NGHĨA KỸ THUẬT TÁI CẤU TRÚC MÃ NGUỒN 7
I.1.1 Ví dụ minh họa 7
I.1.2 Định nghĩa kỹ thuật tái cấu trúc mã nguồn 19
I.2 HIỆU QUẢ CỦA TÁI CẤU TRÚC MÃ NGUỒN 20
I.2.1 Refactoring cải thiện thiết kế phần mềm 20
I.2.2 Refactoring làm mã nguồn phần mềm dễ hiểu 20
I.2.3 Refactoring giúp phát hiện và hạn chế lỗi 21
I.2.4 Refactoring giúp đấy nhanh quá trình phát triển phần mềm 21
I.3 KHI NÀO THỰC HIỆN TÁI CẤU TRÚC MÃ NGUỒN 22
I.3.1 Refactor khi thêm chức năng 22
I.3.2 Refactor khi cần sửa lỗi 22
I.3.3 Refactor khi thực hiện duyệt chương trình 23
I.4 CÁC KỸ THUẬT TÁI CẤU TRÚC MÃ NGUỒN 23
I.4.1 Danh mục các kỹ thuật tái cấu trúc mã nguồn 23
I.5 NHẬN XÉT VÀ KẾT LUẬN 26
CHƯƠNG II: LỖI CẤU TRÚC (BAD SMELLS) TRONG MÃ NGUỒN 27
II.1 KHÁI NIỆM VỀ LỖI CẤU TRÚC (BAD SMELLS) 27
II.2 LỖI CẤU TRÚC VÀ GIẢI PHÁP CẢI TIẾN 27
II.2.1 Duplicated Code - Trùng lặp mã 27
II.2.2 Long Method – Phương thức phức tạp 28
II.2.3 Large Class – Qui mô lớp lớn 30
II.2.4 Long Parameter List - Danh sách tham số quá dài 31
II.2.5 Divergent Change – Cấu trúc lớp ít có tính khả biến 32
II.2.6 Shotgun Surgery – Lớp được thiết kế không hợp lý và bị phân rã 32
II.2.7 Feature Envy – Phân bố phương thức giữa các lớp không hợp lý 33
II.2.8 Data Clumps – Gôm cụm dữ liệu 34
II.2.9 Primitive Obsession – Khả năng thể hiện dữ liệu của lớp bị hạn chế 34
II.2.10 Switch Statements – Khối lệnh điều kiện rẽ hướng không hợp lý 36
II.2.11 Lazy Class – Lớp được định nghĩa không cần thiết 38
II.2.12 Speculative Generality – Cấu trúc bị thiết kế dư thừa 38
II.2.13 Temporary Field – Lạm dụng thuộc tính tạm thời 39
II.2.14 Message Chains –Chuỗi phương thức liên hoàn khó kiểm soát 39
II.2.15 Middle Man – Quan hệ ủy quyền không hợp lý/logic 39
II.2.16 Inapproprite Intimacy - Cấu trúc thành phần riêng không hợp lý 41
II.2.17 Alternative Classes with Different Interfaces - Đặc tả lớp không rõ ràng 41 II.2.18 Incomplete Library Class – Sử dụng thư viện lớp chưa được hòan chỉnh 41 II.2.19 Data Class – Lớp dữ liệu độc lập 42
II.2.20 Refused Bequest – Quan hệ kế thừa không hợp lý/logic 43
Trang 4II.2.21 Comments – Chú thích không cần thiết 43
II.3 NHẬN XÉT VÀ KẾT LUẬN 44
CHƯƠNG III: NỀN TẢNG NET VÀ NGÔN NGỮ LẬP TRÌNH C# 45
III.1 TỔNG QUAN VỀ NỀN TẢNG NET 45
III.1.1 Định nghĩa NET 45
III.1.2 Mục tiêu của NET 45
III.1.3 Dịch vụ của NET 45
III.1.4 Kiến trúc của NET 46
III.2 NGÔN NGỮ LẬP TRÌNH C# 47
III.2.1 Tổng quan về ngôn ngữ lập trình C# 47
III.2.2 Đặc trưng của các ngôn ngữ lập trình C# 47
III.3 MÔI TRƯỜNG PHÁT TRIỂN ỨNG DỤNG VISUAL STUDIO NET 48
CHƯƠNG IV: ỨNG DỤNG KỸ THUẬT TÁI CẤU TRÚC MÃ NGUỒN ĐỂ DÒ TÌM VÀ CẢI TIẾN CÁC ĐOẠN MÃ XẤU TRONG CHƯƠNG TRÌNH C# 49
IV.1 GIẢI PHÁP VÀ CÔNG CỤ HỖ TRỢ REFACTOR 49
IV.1.1 Đặc tả giải pháp triển khai 49
IV.1.2 Một số công cụ và tiện ích hỗ trợ việc dò tìm và cải tiến mã xấu 50
IV.1.3 Thử nghiệm minh họa các công cụ hỗ trợ refactor trong VS.Net 57
IV.1.4 Nhận xét và đánh giá 80
IV.2 ỨNG DỤNG KỸ THUẬT TÁI CẤU TRÚC MÃ NGUỒN ĐỂ DÒ TÌM VÀ CẢI TIẾN CÁC ĐOẠN MÃ XẤU TRONG CHƯƠNG TRÌNH C# 81
IV.2.1 Thực hiện kỹ thuật tái cấu trúc mã nguồn trên chương trình thực tế 82
IV.2.2 Phân tích và đánh giá kết quả thực hiện 94
V.3.1 Triển khai áp dụng trên các ngôn ngữ khác 97
V.3.2 Thử nghiệm xây dựng một refactoring tool tích hợp vào VS.NET 97
TÀI LIỆU THAM KHẢO 98
Trang 5DANH MỤC HÌNH ẢNH
H.4.2: Trình chức năng refactor tích hợp trong VS.NET 50 H.4.3: Trình chức năng refactor của Visual Assit X for VS.NET 51 H.4.4: Trình chức năng refactor của C# Refactory for VS.NET 52 H.4.5: Trình chức năng refactor của NET Refactor for NET 53 H.4.6: Trình chức năng refactor của CodeIT.Once for NET 54 H.4.7: Trình chức năng refactor của JetBrances ReShape 55 H.4.8: Trình chức năng refactor của DevExpress Refactor!™ Pro 56 H.4.9: Minh họa kỹ thuật Change Signature trong JetBrains ReSharper 58 H.4.10: Kết quả minh họa kỹ thuật Change Signature 58 H.4.11: Minh họa kỹ thuật Convert Method to Property của CodeIT.Once 60 H.4.12: Minh họa kỹ thuật Convert Method to Property của ReSharper 61 H.4.13: Kết quả kỹ thuật Convert Method to Property 61 H.4.14: Minh họa kỹ thuật Decompose/Simplify Conditional 63 H.4.15: Kết quả kỹ thuật Decompose/Simplify Conditional 63 H.4.16: Minh họa kỹ thuật Encapsulate Field của Refactor trong VS.NET 65 H.4.17: Minh họa kỹ thuật Encapsulate Field của Visual Assit X for NET 66
H.4.19: Minh họa kỹ thuật Extract Interface của Refactor trong VS.NET 68 H.4.20: Minh họa kỹ thuật Extract Interface của CodeIT.Once 69
H.4.22: Minh họa kỹ thuật Extract Method của Refactor trong VS.NET 71
H.4.24: Minh họa kỹ thuật Inline Variable của CodeIT.Once for NET 73
H.4.26: Minh họa kỹ thuật Promote Local Variable to Parameter của VS.NET 75 H.4.27: Minh họa kỹ thuật Promote Local Variable to Parameter của CodeIT.Once 75 H.4.28: Minh họa kỹ thuật Promote Local Variable to Parameter của ReSharper 76 H.4.29: Kết quả kỹ thuật Promote Local Variable to Parameter 76 H.4.30: Minh họa kỹ thuật Rename Variables của Refactor trong VS.NET 78 H.4.31: Minh họa kỹ thuật Rename Variables của Visual Assit X 79
H.4.33: Sơ đồ lớp của chương trình khi chưa refactoring 82 H.4.34: Màn hình kết quả chạy chương trình khi chưa refactoring 84 H.4.35: Sơ đồ lớp của chương trình sau khi refactoring 91 H.4.36: Màn hình kết quả chạy chương trình sau khi refactoring 93
Trang 6MỞ ĐẦU
Trong qui trình phát triển phần mềm hiện nay, một thực tế đang tồn tại ở các công ty sản xuất phần mềm là các lập trình viên thường xem nhẹ việc tinh chỉnh mã nguồn và kiểm thử Ngoài lý do đơn giản vì đó là một công việc nhàm chán, khó được chấp nhận đối với việc quản lý vì sự tốn kém và mất thời gian, còn một nguyên nhân khác là chúng ta không có những phương pháp và tiện ích tốt hỗ trợ cho những việc này Điều này dẫn đến việc phần lớn các phần mềm không được kiểm thử đầy đủ và phát hành với các nguy cơ lỗi tiềm ẩn
Phương thức phát triển phần mềm linh hoạt[15] bắt đầu xuất hiện vào đầu những năm 90 với mục tiêu là phần mềm phải có khả năng biến đổi, phát triển và tiến hóa theo thời gian mà không cần phải làm lại từ đầu Phương thức này được thực hiện dựa trên hai
kỹ thuật chính là tái cấu trúc mã nguồn (refactoring) và kiểm thử (developer testing) Vì
thế việc nghiên cứu và ứng dụng kỹ thuật tái cấu trúc mã nguồn nhằm tối ưu hóa mã nguồn và nâng cao hiệu quả kiểm thử là một nhu cầu cần thiết trong quá trình thực hiện và phát triển phần mềm
Đề tài “Ứng dụng kỹ thuật tái cấu trúc mã nguồn để triển khai dò tìm và cải
tiến các đoạn mã xấu trong chương trình C#” được thực hiện với mục đích nghiên cứu
cơ sở lý thuyết kỹ thuật tái cấu trúc mã nguồn và áp dụng để triển khai việc dò tìm và cải tiến mã xấu (lỗi cấu trúc) trong các chương trình hiện đại và phổ biến hiện nay (C#) Toàn bộ nội dung của luận văn bao gồm các chương:
Chương 1: Kỹ thuật tái cấu trúc mã nguồn (refectoring)
Chương 2: Mã xấu (bad smells) và giải pháp cải tiến dựa trên refactoring Chương 3: Nền tảng NET và ngôn ngữ lập trình C#
Chương 4: Ứng dụng kỹ thuật tái cấu trúc mã nguồn để dò tìm và cải thiện mã xấu trong các chương trình C#
Chương 5: Kết luận
Trang 7CHƯƠNG I: KỸ THUẬT TÁI CẤU TRÚC MÃ NGUỒN (REFACTORING)
I.1 ĐỊNH NGHĨA KỸ THUẬT TÁI CẤU TRÚC MÃ NGUỒN
I.1.1 Ví dụ minh họa
Phương thức tiếp cận và tìm hiểu hiệu quả nhất với một khái niệm hay một kỹ thuật mới trong tin học là thông qua các ví dụ minh họa [11] Với ví dụ dưới đây, chúng ta sẽ hiểu refactoring là gì cũng như cách thực hiện và hiệu quả của nó trong qui trình công nghệ phát triển phần mềm
Bài toán ví dụ: Chương trình trả lại kết quả danh sách các số nguyên tố
(Bài toán này sử dụng thuật toán Eratosthenes)
Nội dung thuật toán Eratosthenes:
- Viết một danh sách các số từ 2 tới maxNumbers mà ta cần tìm Gọi là list A - Viết số 2, số nguyên tố đầu tiên, vào một list kết quả Gọi là list B
- Xóa bỏ 2 và bội của 2 khỏi list A
- Số đầu tiên còn lại trong list A là số nguyên tố Viết số này sang list B - Xóa bỏ số đó và tất cả bội của nó khỏi list A
- Lặp lại các bước 4 and 5 cho tới khi không còn số nào trong list A
Chương trình khởi đầu:
public class PrimeNumbersGetter { private int maxNumber;
public PrimeNumbersGetter(int maxNumber){ this.maxNumber = maxNumber;
}
public int[] GetPrimeNumbers() { // Use Eratosthenes's sieve bool[] numbers = new bool[maxNumber + 1];
for (int i = 0; i < numbers.Length; ++i){ umbers[i] = true;
}
int j = 2;
while (j <= (int)Math.Sqrt(maxNumber) + 1) { for (int k = j + j; k <= maxNumber; k += j){ numbers[k] = false;
} j++;
while (!numbers[j]) { j++;
if (j > maxNumber) break; }
} }
Trang 8List<int> l = new List<int>();
for (int k = 2; k <= maxNumber; ++k) { if (numbers[k]) l.Add(k);
}
return l.ToArray(); }
Trước khi refactoring, chúng ta cần viết kiểm thử cho phần mã nguồn đó Phần kiểm thử này là yếu tố cần thiết bởi vì quá trình refactoring có thể phát sinh lỗi Mỗi khi chúng ta thực hiện một lần refactoring, chúng ta nên thực hiện kiểm thử lại chương trình một lần, để đảm bảo chương trình không bị lỗi Kiểm thử tốt làm giảm thời gian cần thiết để tìm lỗi Chúng ta nên thực hiện xen kẽ việc kiểm thử và refactoring để mỗi khi có lỗi phát sinh, thì cũng không quá khó để tìm ra lỗi đó
Trong ví dụ này, chúng có thể thêm đoạn mã kiểm thử như sau:
public class Program {
public static void Main(string[] args) {
if (!IsEqualNumbers(new PrimeNumbersGetter(4).GetPrimeNumbers(), new int[] { 2, 3 })) return;
if (!IsEqualNumbers(new PrimeNumbersGetter(5).GetPrimeNumbers(),
new int[] { 2, 3, 5 })) return;
if (!IsEqualNumbers(new PrimeNumbersGetter(6).GetPrimeNumbers(),
new int[] { 2, 3, 5 })) return;
if (!IsEqualNumbers(new PrimeNumbersGetter(100).GetPrimeNumbers(), new int[] { 2, 3, 5, 7,
11, 13, 17, 19, 23, 29, 31, 37, 41, 43,47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97 })) return;
Console.WriteLine("Success!"); }
private static bool IsEqualNumbers(int[] numbers1, int[] numbers2){ if (numbers1.Length != numbers2.Length) return false;
for (int i = 0; i < numbers1.Length; ++i) {
if (numbers1[i] != numbers2[i]) return false; }
}
return true; }
Ta nhận thấy rằng phương thức này quá dài, và nó xử lý rất nhiều công việc khác nhau Trong trường hợp này, nên sử dụng kĩ thuật “Extract Method” trong các kĩ thuật refactoring nhằm tạo ra các phương thức nhỏ hơn, dễ đọc và dễ bảo trì khi có yêu cầu thay đổi chương trình
Với đoạn mã nguồn khởi tạo list các số ban đầu ( list A ):
Trang 9bool[] numbers = new bool[maxNumber + 1]; for (int i = 0; i < numbers.Length; ++i){
numbers[i] = true; }
Ta nên trích xuất nó thành một phương thức khác, sử dụng “Extract Method”
public int[] GetPrimeNumbers() {
bool[] numbers = InitialNumbers();
// Other codes }
private bool[] InitialNumbers(){
bool[] numbers = new bool[maxNumber + 1]; for (int i = 0; i < numbers.Length; ++i){ numbers[i] = true;
}
return numbers; }
Sau khi thực hiện việc refactoring như trên, chúng ta nên nhớ rằng phải thực hiện chạy lại chương trình kiểm thử để đảm báo rằng việc refactoring không làm thay đổi tính đúng đắn của chương trình
Tương tự với đoạn mã nguồn thực hiện xuất ra danh sách kết quả chứa các số nguyên tố (list B)
List<int> l = new List<int>();
for (int k = 2; k <= maxNumber; ++k) { if (numbers[k]) l.Add(k);
Trang 10Bây giờ chúng ta sẽ tinh chỉnh ở phần mã nguồn còn lại, đó là vòng lặp while
int j = 2;
while (j <= (int)Math.Sqrt(maxNumber) + 1){ for (int k = j + j; k <= maxNumber; k += j){ numbers[k] = false;
} j++;
while (!numbers[j]){ j++;
if (j > maxNumber) break; }
numbers[k] = false; }
j++;
while (!numbers[j] && j < maxNumber) {
j++; }
numbers[i * j] = false; }
j++;
while (!numbers[j] && j < maxNumber) { j++;
} }
Với vòng while ở trên, mục đích chỉ là duyệt danh sách các phần tử trong list A, nên ta có thể chuyển sang sử dụng vòng lặp for nhƣ sau:
for (int j = 2; j <= (int)Math.Sqrt(maxNumber) + 1; j++) { if (!numbers[j]) continue;
for (int i = 2; i * j <= maxNumber; i++) { numbers[i * j] = false;
} }
Trang 11Với đoạn mã nguồn
for (int i = 2; i * j <= maxNumber; i++) { numbers[i * j] = false;
private void RemoveMultiple(bool[] numbers, int j) { for (int i = 2; i * j <= maxNumber; i++) {
numbers[i * j] = false; }
}
Tiếp tục với đoạn mã nguồn
private void RemoveMultiple(bool[] numbers, int j) { for (int i = 2; i * j <= maxNumber; i++) {
numbers[i * j] = false; }
Ta nên đặt tên biến, tham số truyền vào sao cho mã nguồn trở nên dễ đọc nhất
private void RemoveMultiple(bool[] numbers, int number) { for (int i = 2; i * number <= maxNumber; i++) { numbers[i * number] = false;
} }
Đến đây ta thấy rằng phương thức GetPrimenumbers() đã trở nên ngắn gọn, dễ đọc hơn nhất nhiều Tuy nhiên, chúng ta cũng cần nghĩ rằng chương trình này đã thực sự đẹp chưa và có cần refactor nữa hay không?
Ta nhận thấy rằng biến numbers được sử dụng là tham số để truyền vào một số phương thức Do đó, ta nên chuyển khai báo biến numbers thành biến thành viên của lớp Khi đó, các phương thức sẽ sử dụng trực tiếp biến thành viên này, chứ không phải sử dụng tham số truyền vào Khi chuyển biến numbers thành biến thành viên, thì phương thức InitialNumbers() không cần nữa, mà ta sẽ chuyển khởi tạo biến này trong constructor của lớp Khi đó chúng ta cần phải xóa hết các tham số truyền vào trong các phương thức sử dụng biến numbers
Trang 12public int[] GetPrimeNumbers() {
bool[] numbers = InitialNumbers(); // Other codes }
// Other codes
private bool[] InitialNumbers() {
bool[] numbers = new bool[maxNumber + 1]; for (int i = 0; i < numbers.Length; ++i) {
numbers[i] = true; }
return numbers; }
}
Sẽ đƣợc chỉnh sửa thành
public class PrimeNumbersGetter { private int maxNumber; private bool[] numbers;
public PrimeNumbersGetter(int maxNumber) { this.maxNumber = maxNumber;
this.numbers = new bool[maxNumber + 1]; for (int i = 0; i < numbers.Length; ++i) {
numbers[i] = true; }
Trang 13public class PrimeNumbersGetter { private int maxNumber; private BitArray numbers;
public PrimeNumbersGetter(int maxNumber) { this.maxNumber = maxNumber;
this.numbers = new BitArray(maxNumber - 1, true); }
public int[] GetPrimeNumbers() {
for (int j = 2; j <= (int)Math.Sqrt(maxNumber) + 1; j++) { if (!numbers[j - 2]) continue;
RemoveMultiple(j); }
return GetPrimeNumbersArray(); }
private void RemoveMultiple(int number){
for (int i = 2; i * number <= maxNumber; i++) { numbers[i * number - 2] = false;
} }
private void RemoveMultiple(int number) {
for (int i = 2; i * number <= maxNumber; i++) { numbers[i * number - 2] = false;
} }
mục đích của nó là loại bỏ các số không phải là số nguyên tố, ta nên tách nó ra với việc thêm một phương thức, và đặt tên theo đúng ý nghĩa của nó
private void RemoveMultiple(int number) {
for (int i = 2; i * number <= maxNumber; i++) {
RemoveNumber(i * number);
} }
private void RemoveNumber(int number) { numbers[number - 2] = false;
}
Trang 14Tương tự như vậy, khi duyệt để lấy ra phần tử trong list nếu để là numbers[number - 2] thì khó đọc Do đó có thể chuyển thành phương thức và đặt tên cho nó Ta viết được phương thức như sau:
private bool Remains(int number) { return numbers[number - 2]; }
Thay trong chương trình, ta có hình ảnh của mã nguồn:
public class PrimeNumbersGetter { private int maxNumber; private BitArray numbers;
public PrimeNumbersGetter(int maxNumber){ this.maxNumber = maxNumber;
this.numbers = new BitArray(maxNumber - 1, true); }
public int[] GetPrimeNumbers() { // Use Eratosthenes's sieve
for (int j = 2; j <= (int)Math.Sqrt(maxNumber) + 1; j++){ if (!Remains(j)) continue;
RemoveMultiple(j); }
return GetPrimeNumbersArray(); }
private void RemoveMultiple(int number){
for (int i = 2; i * number <= maxNumber; i++) { RemoveNumber(i * number);
} }
private bool Remains(int number) { return numbers[number - 2]; }
private void RemoveNumber(int number) { numbers[number - 2] = false;
}
Bây giờ ta xem xét việc tạo lớp mới từ những thành phần liên quan đến nhau – còn gọi là phương thức “Extract Class”
Trang 15Ở trong bài toán này , ta có thể tạo ra một lớp internal chứa dữ liệu liên quan đến danh sách các số, và các xử lý trên danh sách đó Chuyển các phương thức Remains(), RemoveNubmer(), GetPrimeNumberArray() sang lớp mới Kết quả như sau:
public class PrimeNumbersGetter { private int maxNumber;
public PrimeNumbersGetter(int maxNumber) { this.maxNumber = maxNumber;
}
public int[] GetPrimeNumbers() {
Sieve sieve = new Sieve(maxNumber); // Use Eratosthenes's sieve
for (int j = 2; j <= (int)Math.Sqrt(maxNumber) + 1; j++) { if (!sieve.Remains(j)) continue;
RemoveMultiple(sieve, j); }
return sieve.GetPrimeNumbersArray(); }
private void RemoveMultiple(Sieve sieve, int number){ for (int i = 2; i * j <= maxNumber; i++) {
sieve.RemoveNumber(i * number); }
} }
internal class Sieve {
private int maxNumber; private BitArray numbers;
internal Sieve(int maxNumber) { this.maxNumber = maxNumber;
this.numbers = new BitArray(maxNumber - 1, true); }
internal bool Remains(int number) { return numbers[number - 2]; }
internal void RemoveNumber(int number) { numbers[number - 2] = false;
return l.ToArray(); }
Trang 16Ta thấy rằng phương thức GetPrimeNumbers() mục đích chính là trả lại danh sách các số nguyên tố cần tìm Để có được danh sách các số nguyên tố, có thể có rất nhiều thuật toán Mặt khác, phương thức này lại cài đặt bên trong nó thuật toán Eratosthenes's sieve Ta nên tách riêng nó thành một lớp khác, để phương thức GetPrimeNumbers() chỉ cần có giá trị trả về theo một thuật toán nào đó.(Ở đây là thuật toán Eratosthenes) Kết quả như sau:
public class PrimeNumbersGetter { private int maxNumber;
public PrimeNumbersGetter(int maxNumber) { this.maxNumber = maxNumber;
}
public int[] GetPrimeNumbers() {
return new Eratosthenes(maxNumber).GetPrimeNumbers(); }
}
internal class Eratosthenes { private int maxNumber;
internal Eratosthenes(int maxNumber) { this.maxNumber = maxNumber; }
public int[] GetPrimeNumbers(){
Sieve sieve = new Sieve(maxNumber);
for (int j = 2; j <= (int)Math.Sqrt(maxNumber) + 1; j++){ if (!sieve.Remains(j)) continue;
RemoveMultiple(sieve, j); }
return sieve.GetPrimeNumbersArray(); }
private void RemoveMultiple(Sieve sieve, int number) { for (int i = 2; i * j <= maxNumber; i++) {
sieve.RemoveNumber(i * number); }
}
}
Ta xem xét đoạn mã nguồn sau:
for (int j = 2; j <= (int)Math.Sqrt(maxNumber) + 1; j++) { if (!sieve.Remains(j)) continue;
RemoveMultiple(sieve, j); }
Công việc của đoạn mã nguồn là tìm các phần tử là số nguyên tố đầu tiên, sau đó loại bỏ đi các bội số của chúng trong list A ban đầu Ta tạo ra method GetRemainNumbers() có chứa các số nguyên tố đầu tiên Ta nghĩ tới việc dùng foreach để duyệt các phần tử trong GetRemainNumbers() Muốn sử dụng foreach thì
Trang 17GetRemainNumbers() phải được cài đặt interface IEnumerable và sử dụng câu lệnh yield return Kết quả như sau:
public int[] GetPrimeNumbers() {
Sieve sieve = new Sieve(maxNumber);
foreach (int i in GetRemainNumbers(sieve)){
RemoveMultiple(sieve, i); }
return sieve.GetPrimeNumbersArray(); }
private IEnumerable<int> GetRemainNumbers(Sieve sieve) { for (int i = 2; i <= (int)Math.Sqrt(maxNumber) + 1; i++) {
if (sieve.Remains(i)) yield return i;
} }
Tương tự như vậy, ta xem xét phương thức GetPrimeNumberArray(), ta cũng có thể sử dụng interface Ienumerable
internal int[] GetPrimeNumbersArray() { List<int> l = new List<int>();
for (int k = 2; k <= maxNumber; ++k) { if (Remains(k)) l.Add(k);
}
return l.ToArray(); }
Chuyển thành
internal int[] GetPrimeNumbersArray() {
return new List<int>(GetRemainNumbers()).ToArray(); }
private void RemoveMultiple(Sieve sieve, int j) { for (int i = 2; i * j <= maxNumber; i++){ sieve.RemoveNumber(i * j);
} }
Trang 18Ta nên đặt lại tên tham số truyền vào, biến j thành biến number private void RemoveMultiple(Sieve sieve, int number) {
for (int i = 2; i * number <= maxNumber; i++) { sieve.RemoveNumber(i * number);
} }
Tương tự như vậy, biến đếm k trong phương thức GetRemainNumbers() cũng nên chuyển thành biến đếm i
Và đây là kết quả của quá trình refactoring:
public class PrimeNumbersGetter { private int maxNumber;
public PrimeNumbersGetter(int maxNumber) { this.maxNumber = maxNumber;
}
public int[] GetPrimeNumbers(){
return new Eratosthenes(maxNumber).GetPrimeNumbers(); }
}
internal class Eratosthenes { private int maxNumber;
internal Eratosthenes(int maxNumber){ this.maxNumber = maxNumber; }
public int[] GetPrimeNumbers() {
Sieve sieve = new Sieve(maxNumber); foreach (int i in GetRemainNumbers(sieve)) { RemoveMultiple(sieve, i);
return sieve.GetPrimeNumbersArray(); }
private IEnumerable<int> GetRemainNumbers(Sieve sieve) { for (int i = 2; i <= (int)Math.Sqrt(maxNumber) + 1; i++){ (sieve.Remains(i)) yield return i;
} }
private void RemoveMultiple(Sieve sieve, int number) { for (int i = 2; i * number <= maxNumber; i++) { sieve.RemoveNumber(i * number);
} }
}
internal class Sieve {
Trang 19private int maxNumber; private BitArray numbers;
internal Sieve(int maxNumber) { this.maxNumber = maxNumber;
this.numbers = new BitArray(maxNumber - 1, true); }
internal bool Remains(int number) { return numbers[number - 2]; }
internal void RemoveNumber(int number) { numbers[number - 2] = false;
}
internal int[] GetPrimeNumbersArray(){
return new List<int>(GetRemainNumbers()).ToArray(); }
private IEnumerable<int> GetRemainNumbers() { for (int i = 2; i <= maxNumber; ++i) { if (Remains(i)) yield return i; }
} }
Thông qua ví dụ ta trình bày trên, ta có thể thấy việc refactoring thực sự đơn giản nhưng chỉ với các bước đơn giản như vậy thôi cũng đã làm cho đoạn mã nguồn dễ đọc và dễ hiểu hơn rất nhiều Vậy refactoring là gì và hiệu quả của nó như thế nào trong quá trình phát triển phần mềm
I.1.2 Định nghĩa kỹ thuật tái cấu trúc mã nguồn
Chúng ta có hai khái niệm định nghĩa [4] khi tiếp cận thuật ngữ tái cấu trúc mã nguồn (Refectoring):
- Định nghĩa 1 (Danh từ): Sự thay đổi cấu trúc nội tại phần mềm để dễ hiểu hơn và
ít tốn chi phí để cập nhật mà không làm thay đổi ứng xử bên ngoài
- Định nghĩa 2 (Động từ): Tái cấu trúc lại phần mềm thông qua việc áp dụng các
bước refactoring mà không làm thay đổi ứng xử bên ngoài
Như vậy refactoring có phải làm sạch đoạn chương trình? Đúng như vậy, nhưng refactoring còn tiến xa hơn bởi cung cấp các kỹ thuật trong việc làm sạch đoạn chương trình hiệu quả hơn và kiểm soát hành vi Trong quá trình refactoring, chúng ta cần chú ý đến yếu tố làm sạch mã nguồn hiệu quả hơn và tối thiểu bug
Như vậy theo định nghĩa, mục tiêu đầu tiên của refactoring là làm cho chương trình dễ đọc và khi cần thiết có thể cập nhật thì vẫn không làm thay đổi hoặc có nhưng không đáng kể đến các hành vi ứng xử bên ngoài của phần mềm Như vậy có gì khác biệt
Trang 20giữa refactoring và việc tối ưu hiệu năng xử lý Cũng giống như refactoring, tối ưu hiệu năng xử lý không làm thay đổi hành vi của các thành phần nghĩa là chỉ thay đổi cấu trúc bên trong Tuy nhiên mục tiêu chúng khác nhau Tối ưu vận hành thường làm cho đoạn chương trình khó hiểu hơn, nhưng chúng ta cần phải thực hiện nó để tăng tốc độ chúng ta cần
Mục tiêu kế tiếp mà chúng ta cần lưu ý đó là refactoring không làm thay đổi ứng xử bên ngoài của phần mềm Phần mềm sẽ thực thi và xử lý các chức năng như trước Bất kỳ người dùng nào, kể cả người dùng cuối hay người lập trình không thể cảm nhận về những sự thay đổi này
I.2 HIỆU QUẢ CỦA TÁI CẤU TRÚC MÃ NGUỒN I.2.1 Refactoring cải thiện thiết kế phần mềm
Thiết kế chương trình luôn tìm ẩn nhiều rủi ro và dễ bị hư tổn Khi có sự thay đổi chương trình (thay đổi hiện thực hóa mục tiêu ngắn hạn hay sự thay đổi không được hiểu thấu đáo thiết kế chương trình) thì khả năng chương trình bị mất cấu trúc là hoàn toàn có thể xảy ra Khi việc mất cấu trúc chương trình sẽ có tác động làm cho người phát triển khó nhìn thấy thiết kế chương trình, càng khó bảo trì và nhanh chóng bị hư tổn
Trong quá trình thiết kế phầm mềm, nếu chúng ta áp dụng refactoring sẽ là một giải pháp hiệu quả vì refactoring sẽ làm gọn chương trình Công việc này được thực hiện nhằm mục đích chuyển đổi những gì thực sự không đúng chỗ về đúng vị trí Đoạn chương trình được thiết kế hợp lý thường chiếm nhiều dòng mã nguồn và khả năng trùng lặp mã nguồn Như vậy khía cạnh quan trọng của cải tiến thiết kế là loại bỏ những mã nguồn lặp Điều quan trọng của vấn đề này nằm ở những thay đổi tương lai đoạn mã nguồn Bằng việc loại bỏ sự trùng lắp, đảm bảo rằng đoạn chương trình chỉ làm một lần và chỉ một là điều thiết yếu của thiết kế tốt Khi đó phần mềm sẽ trở nên dễ hiểu và đảm bảo những hạn chế thấp nhất trong quá trình phát triển và cập nhật Refactoring giúp nâng cao khả năng tinh gọn và cải biến của chương trình
I.2.2 Refactoring làm mã nguồn phần mềm dễ hiểu
Lập trình là sự đối thoại và giao tiếp theo nhiều cách với máy tính Chúng ta viết mã lệnh để yêu cầu máy tính cần làm những gì và phản hồi chính xác những gì chúng ta bảo nó thực hiện Trong chu kỳ sống của phầm mềm sẽ có nhiều người cùng tham gia vào việc phát triển và bảo trì Khi đó yếu tố cần thiết ở đây là qui chuẩn về lập trình (coding) Việc áp dụng refactoring (thông qua việc sửa đổi định danh, từ ngữ, cách đặt tên cho các thành phần trong mã nguồn) giúp làm cho đoạn mã nguồn tuân theo qui chuẩn để có khả năng đọc được và chương trình dễ hiểu hơn Khi chưa refactoring, đoạn mã nguồn của chúng ta có thể chạy nhưng chưa được cấu trúc hoàn chỉnh Việc refactoring tuy chiếm một ít thời gian nhưng có thể làm cho đoạn mã nguồn có cấu trúc rõ ràng và dễ hiểu
Trang 21Ngoài ra việc sử dụng refactoring thông qua các thủ thuật: sắp xếp lại trật tự các dòng lệnh, các vòng lặp, các điều kiện, ràng buộc nhằm làm cho logic của mã nguồn tốt hơn, số lượng dòng lệnh được cực tiểu hóa,…
I.2.3 Refactoring giúp phát hiện và hạn chế lỗi
Refactoring giúp hiểu đoạn mã nguồn từ đó giúp chúng ta trong việc phát hiện lỗi Trong quá trình tìm lỗi, việc các lập trình viên phải đọc hàng ngàn dòng mã nguồn để tìm lỗi là điều thường xuyên xảy ra Lúc này refactoring sẽ giúp chúng ta hiểu sâu những đoạn mã nguồn làm gì, từ đó có thể có được những suy luận và phán đoán về các khả năng lỗi xảy ra và tìm đúng đến đoạn mã nguồn cần chỉnh sửa Một khi làm rõ cấu trúc chương trình chính là chúng ta làm rõ những giả định mà chúng ta mong muốn chương trình thực hiện để tránh phát sinh lỗi
Ngoài ra việc sắp đặt lại các logic luồng làm việc của mã nguồn giúp cho luồng xử lý rõ ràng hơn và tránh các sai sót có khả năng xảy ra Một lập trình viên nổi tiếng Kent Beck từng khẳng định về tính hiệu quả của refactoring trong việc phát hiện và hạn chế lỗi: „Tôi không là lập trình giỏi; tôi chỉ là lập trình tốt với thói quen Refacotring giúp tôi trở nên hiệu quả trong viết chương trình có độ chắc chắn”
I.2.4 Refactoring giúp đấy nhanh quá trình phát triển phần mềm
Refactoring giúp đấy nhanh quá trình phát triển phần mềm thông qua các hiệu quả mà nó mang lại:
- Tăng tính dùng lại: mã nguồn tốt, rõ ràng sẽ có lợi khi được sử dụng lại cho các
module khác của cùng ứng dụng hoặc được dùng như một bộ thư viện sử dụng cho nhiều ứng dụng, module khác nhau
- Tăng tính tiến hóa: một mã nguồn tốt có lợi ích và chu kỳ sống cụ thể do công
nghệ thông tin ngày càng phát triển Mã nguồn tốt có thể có thời gian sử dụng lâu hơn và khả năng tự phát triển, nâng cấp, kế thừa khi ứng dụng có nhu cầu phát triển thêm mà không phải bị vứt bỏ để viết lại từ đầu
- Tăng tính gần gũi với người dùng: có những ứng dụng hay nhưng lại phức tạp
cho người sử dụng hay người đọc Chẳng hạn như phần giao tiếp người dùng (user interface) cần được cải thiện để tăng tính dễ dùng, dễ hiểu, linh hoạt hơn và làm cho giao tiếp người dùng sử dụng được hết các khả năng của mã nguồn cung cấp Refactoring không hẳn làm thay đổi các cư xử, hoạt động bên ngài của phần mềm Chủ yếu là cải thiện phần cấu trúc bên trong nhằm làm tối ưu chức năng của phần mềm để phần mềm xử lý, nhanh hơn, tốt hơn, an toàn hơn và cố thể phù hợp với nhiều môi trường hoặc thay đổi mới cho người dùng trong quá trình sử dụng Giảm thiểu những sai sót và tăng thời gian sống cho phần mềm Là một bước không thể thiếu và có thể được áp dụng trong suốt các quá trình phát triển phần mềm
Trang 22Ngày nay Refactoring chính là một chuẩn mực coding của mọi lập trình viên khi làm việc theo nhóm, khi bắt đầu làm việc ở công ty lớn, các lập trình viên sẽ được huấn luyện và đào tạo để tuân thủ các yêu cầu làm việc: như quy tắc đặt tên biến, khi viết mã
nguồn áp dụng partern nào, xây dựng unit test ra sao
I.3 KHI NÀO THỰC HIỆN TÁI CẤU TRÚC MÃ NGUỒN
Như trình bày ở trên, việc áp dụng kỹ thuật refactoring đem lại những hiệu quả cải tiến trong qui trình phát triển phần mềm hiện đại Vậy chúng ta sẽ thực hiện nó trong những trường hợp nào và tần suất như thế nào là hợp lý?
Một lời khuyên từ các nhà phát triển phần mềm hiện đại là trong tất cả các trường hợp, chúng ta đều phải dành thời gian cho việc refactoring Refectoring là công việc mà chúng ta phải thực hiện thường xuyên trong chu kì phát triển và bảo dưỡng phần mềm Chuyên gia Don Roberts đã gợi ý luật “tam trùng (the rule of three) [4]” trong việc xem xét khi nào chúng ta nên refactoring:
- Lần đầu tiên chúng ta làm điều gì, chúng ta chỉ làm nó thôi
- Lần thứ hai chúng ta làm điều gì tương tự Chúng ta có thể chấp nhập lặp lại - Nhưng nếu lần thứ ba việc đó lặp lại, chúng ta refactor
I.3.1 Refactor khi thêm chức năng
Thời điểm phổ biến nhất để refactor là khi chúng ta muốn thêm chức năng mới vào phần mềm Lý do đầu tiên thường để refactor ở đây là giúp hiểu đoạn mã nguồn chương trình chúng ta cần thay đổi Đoạn mã nguồn này có lẽ được viết bởi nhiều người khác hay ngay cả khi chính ta viết nó lâu ngày thì chúng ta cũng cần phải suy nghĩ để hiểu những gì đoạn mã nguồn đang làm Một khi đã hiểu được những gì đoạn mã nguồn sẽ làm, điều cần thiết lúc này là chúng ta refactor mã nguồn để làm sao quá trình đọc hiểu mã nguồn được rõ ràng và nhanh hơn sau đó refactor nó Sau khi refactor, các bước xảy ra tương tự ở phần sau sẽ được bỏ qua vì chúng ta có thể hiểu nhiều và sáng tỏ đoạn mã nguồn đã quen thuộc
Một xu hướng khác để áp dụng refactoring ở đây là khi gặp một thiết kế mà không thể giúp chúng ta thêm chức năng dễ dàng Trong lập trình đôi lúc chúng ta gặp những trường hợp nhìn vào thiết kế và tự nói “ Nếu tôi thiết kế theo cách này, thì việc thêm chức năng có thể sẽ dễ dàng?” Trong trường hợp này, không quá lo lắng về sai lầm đã qua – chúng ta sẽ thực hiện bằng refactoring Thực hiện điều đó trong chừng mực để cải thiện thời gian sắp tới dễ hơn Sau khi refactor, quá trình thêm chức năng sẽ tiến triển nhanh và trôi chảy hơn
I.3.2 Refactor khi cần sửa lỗi
Điều trước tiên khi chỉnh sửa lỗi là ta phải hiểu rõ về bản chất thực thi của đoạn mã cần chỉnh sửa Để làm được việc đó, chúng ta cần phải làm cho đoạn mã dễ đọc và
Trang 23hiểu hơn Như vậy trong quá trình sửa lỗi, chúng ta nhìn và phân tích đoạn mã nguồn để hiểu và refactor để cải thiện sự hiểu biết của mình Thông thường chúng ta dự đoán qui trình hoạt động của đoạn mã hiện tại để tìm lỗi Tuy nhiên không phải lúc nào chúng ta cũng có thể phát hiện ra lỗi từ mã nguồn vì nó không rõ đủ cho chúng ta nhìn ra có lỗi Một cách để nhìn vào vấn đề này đó là nếu chúng ta nhận được một báo cáo lỗi phát sinh, đó là dấu hiệu chúng ta cần refactoring
I.3.3 Refactor khi thực hiện duyệt chương trình
Ngoài ra chúng ta còn sử dụng refactoring trong quá trình duyệt mã nguồn được viết bởi người khác.Trong quá trình duyệt chương trình, ngoài việc đọc hiểu và kiểm tra tính chính xác của đoạn mã cũng như đề xuất các đề nghị Khi đưa ra ý kiến hoặc đề nghị chúng ta xem xét hoặc chúng có thể thực hiện được dễ dàng hoặc phải refactoring Nếu vậy, tiến hành refactor Khi thực hiện vài lần, chúng ta có thể nhìn thấy rõ ràng hơn những gì đoạn mã nguồn trông giống khi thay thế Kết quả là chúng ta có thể đạt đến ý tưởng ở mức hai đó là bạn không bao giờ nhận ra bạn không refactor
I.4 CÁC KỸ THUẬT TÁI CẤU TRÚC MÃ NGUỒN
Refactoring là một trong những phương pháp nhằm nâng cao chất lượng phần mềm đã bắt đầu được nghiên cứu và ứng dụng những năm 90 trong qui trình phát triển phần mềm Qua quá trình nghiên cứu và phát triển, một tập các kỹ thuật refactoring đã được đặc tả chi tiết và phần lớn các kỹ thuật refactoring trên đã và đang dần được tích hợp vào trong các công cụ phát triển phần mềm nhằm hỗ trợ cho các nhà phát triển trong việc rút ngắn thời gian tạo nên các phần mềm có chất lượng cao và ổn định, đáp ứng tốt các yêu cầu hoạt động của hiện tại và những thay đổi cần thiết trong tương lai
I.4.1 Danh mục các kỹ thuật tái cấu trúc mã nguồn
Dưới đây là danh mục các kỹ thuật tái cấu trúc mã nguồn được liệt kê theo nhóm:
STT Kỹ thuật Refactoring Diễn giải/Mục đích sử dụng Composing Methods – Định nghĩa phương thức
1 Extract Method Định nghĩa phương thức mới dựa trên trích chọn tập đoạn mã nguồn 2 Inline Method Hợp nhất các phương thức lại với nhau
3 Inline Temp Thay thế các tham chiếu của một biến tạm bằng biểu thức 4 Replace Temp with Query
Chuyển đổi biểu thức thành phương thức Thay thế các tham chiếu của biến tạm nhận được từ biểu thức bị chuyển đổi bởi phương thức vừa tạo mới.
5 Introduce Explaining Variable Đặt/thay thế kết quả của biểu thức hoặc một phần của biểu thức vào biến tạm cùng tên 6 Split Temporary Variable Tạo mới các biến tạm riêng biệt tương ứng với từng đoạn mã 7 Remove Assignments to Parameter Sử dụng biến tạm thay thế cho việc gán giá trị mới cho tham số 8 Replace Method with Method Object Chuyển đổi phương thức thành đối tượng sở
Trang 24hữu Khi đó tất cả các biến cục bộ trở thành các thuộc tính của đối tượng
9 Substitute Algorithm Thay thế thuật toán trong thân phương thức
Moving Features Between Objects – Dịch chuyển chức năng giữa các đối tượng
10 Move Method Dịch chuyển phương thức giữa các lớp
11 Move Field Hoán đổi/dịch chuyển thuộc tính giữa các lớp 12 Extract Class Tạo một lớp mới và dịch chuyển các thuộc tính và phương thức có liên quan từ lớp cũ sang 13 Inline Class Hợp nhất các lớp riêng có quan hệ thành một lớp chung 14 Hide Delegate Tạo các phương thức trung gian truy cập gián tiếp đến lớp khác 15 Remove Middle Man Tường minh các phương thức truy cập ở mỗi
lớp (ngược với Hide Delegate)
16 Introduce Foreign Method Tạo ra một phương thức trong một lớp client có tham số là một đại diện của một lớp server 17 Introduce Local Extension Tạo một lớp mới chứa các phương thức mở rộng đi kèm với một lớp con hoặc một lớp bao
Organizing Data – Tổ chức dữ liệu
18 Self Encapsulate Field
Tạo ra các phương thức truy xuất và thiết lập giá trị các thuộc tính và sử dụng chúng thay vì truy xuất trực tiếp
19 Replace Data Value with Object Thay thế giá trị dữ liệu bằng đối tượng 20 Change Value to Reference Chuyển đổi giá trị thành tham chiếu 21 Change Reference to Value Chuyển đổi tham chiếu thành giá trị
22 Replace Array with Object Thay thế mảng bởi đối tượng với phần tử của mảng tương ứng với thuộc tính của đối tượng 23 Duplicate Observed Data Đa hình dữ liệu
24 Change Unidirectional Association to Bidirectional Chuyển đổi từ liên kết đơn ánh sang xuất song ánh trong quan hệ giữa 2 lớp 25 Change Bidirectional Association to Unidirectional Chuyển đổi từ liên kết song ánh sang xuất đơn ánh trong quan hệ giữa 2 lớp 26 Replace Magic Number with Symbolic Constant Sử dụng biến hằng thay cho giá trị số tường minh 27 Encapsulate Field Chuyển đổi thuộc tính chung thành riêng 28 Encapsulate Collection Thêm mớI hoặc xóa bỏ phương thức
29 Replace Record with Data Class Thay thế kiểu dữ liệu bảng ghi bởi lớp dữ liệu 30 Replace Type Code with Class Thay thế mã kiểu số đếm hoặc liệt kê bới một lớp mớI 31 Replace Type Code with Subclasses Thay thế mã kiểu dữ liệu thành lớp con
32 Replace Type Code with State/Strategy Thay thế mã kiểu dữ liệu thành một đối tượng tĩnh 33 Replace Subclass with Fields Biến đổi các phương thức ở các thuộc tính siêu lớp và rút gọn các lớp con
Simplifying Conditional Expressions – Đơn giản hóa các biểu thức điều kiện
34 Decompose Conditional Tạo mới các phương thức từ mệnh đề và thân xứ lý của câu lệnh điều kiện 35 Consolidate Conditional Expression Tạo mới một phương thức từ việc hợp nhất các biểu thức điều kiện 36 Consolidate Duplicate Conditional Fragments Hợp nhất và di chuyển các phân đoạn trùng lặp ra bên ngoài thân câu lệnh điều kiện 37 Remove Control Flag Sử dụng câu lệnh break hoặc return thay thế cho biến cờ hiệu trong các thân vòng lặp
Trang 2538 Replace Nested Conditional with Guard Clauses Thay thế các điều kiện rẽ nhánh bởi các mệnh đề so khớp 39 Replace Conditional with Polymorphism Thay thế phép so sánh điều kiện bởi tính chất đa hình của phương thức 40 Introduce Null Object Thay thế giá trị NULL bởi một đối tượng NULL 41 Introduce Assertion Xác nhận tính hợp lệ của dữ liệu trước khi xử lý
Making Method Calls Simpler – Đơn giản hóa việc gọi phương thức
42 Rename Method Đổi tên phương thức
43 Add Parameter Bổ sung tham số cho phương thức 44 Remove Parameter Xóa bỏ tham số của phương thức
45 Separate Query from Modifier Tách thành hai phương thức riêng biệt khi vừa truy xuất và cập nhật trên phương thức gốc 46 Parameterize Method Tạo mới một phương thức và sử dụng tham số cho các giá trị khác nhau 47 Replace Parameter with Explicit Methods Tạo mới một phương thức cho mỗi giá trị của tham số 48 Preserve Whole Object Chuyển nguyên một đối tượng thay vì các giá trị riêng lẻ 49 Replace Parameter with Method Rút gọn tham số trong phương thực
50 Introduce Parameter Object Thay thế một nhóm các tham số đồng dạng bởi một đối tượng 51 Remove Setting Method Xóa bỏ phương thức dư thừa đã được thiết lập 52 Hide Method Chuyển thành phương thức riêng
53 Replace Constructor with Factory Method Thay thế hàm dựng bởi phương thức sản xuất 54 Encapsulate Downcast Định dạng kiểu trả về trong thân phương thức 55 Replace Error Code with Exception Tạo mới một phương thức từ một phân đoạn mã để kiểm soát các trường hợp (lỗi) ngoại lệ 56 Replace Exception with Test Kiểm tra các trường hợp (lỗi) ngoại lệ có khả năng phát sinh
Dealing with Generalization – Liên hệ tổng quát hóa
57 Pull Up Field Dịch chuyển thuộc tính lên lớp cha 58 Pull Up Method Dịch chuyển phương thức lên lớp cha
59 Pull Up Constructor Body Tạo một hàm dựng chung ở lớp cha, và gọi lại nó từ các phương thức ở lớp con 60 Push Down Method Dịch chuyển phương thức về lớp con
61 Push Down Field Dịch chuyển thuộc tính về lớp con
62 Extract Subclass Trích xuất/chia tách thành các lớp con với tập các chức năng riêng 63 Extract Superclass Định nghĩa mới một lớp cha và di chuyển các chức năng chung đến lớp này từ các lớp con 64 Extract Interface Định nghĩa một giao diện từ việc trích chọn một tập con các thành viên trong lớp 65 Collapse Hierarchy Hợp nhất thành một lớp nếu 2 lớp cha và con (quan hệ kế thừa) có ít sự khác biệt 66 Form Template Method Đồng nhất các phương thức ở lớp con và dịch chuyển lên lớp cha 67 Replace Inheritance with Delegation Hoán chuyển từ tính kế thừa sang tính ủy quyền giữa hai lớp 68 Replace Delegation with Inheritance Hoán chuyển từ tính tính ủy quyền sang tính kế thừa giữa hai lớp
Other Refactorings – Một số kỹ thuật khác
Trang 2669 Tease Apart Inheritance Giảm cấp trong quan hệ kế thừa
70 Convert Procedural Design to Objects Chuyền đổi hướng thủ tục sang hướng đối tượng 71 Separate Domain from Presentation Đối tượng hóa các thể hiện
72 Extract Hierarchy Phát sinh hoặc thay đổi thứ bậc của các lớp trong quna hệ kế thừa 73 Reorder Parameters Thay đổi vị trí các tham số trong phương thức 74 Promote Local Variable to Parameter Chuyển biến cục bộ thành tham số của phương thức
75 Convert Abstract Class to Interface Chuyển đổi lớp ảo thành giao diện 76 Convert Interface to Abstract Class Chuyển đổi giao diện thành lớp ảo
77 Convert Method to Property Chuyển đổi phương thức thành đặc tính của lớp 78 Convert Property to Method Chuyển đổi đặc tính của lớp thành phương thức 79 Rename parameter Đổi tên tham số
80 Rename Local Variable Đổi tên biến cục bộ 81 Remove Redundant Conditional Xóa bỏ điều kiện dư thừa
82 Rename Type Đổi tên các thành phần trong lớp
83 Extract Variable Sử dụng biến được khởi gán trong một biểu thức được chọn
I.5 NHẬN XÉT VÀ KẾT LUẬN
Với những nội dung đã được trình bày ở trên về cơ sở lý thuyết của kỹ thuật tái cấu trúc mã nguồn (refactoring), đó là một kỹ thuật làm thay đổi cấu trúc nội tại phần mềm, làm cho phần mềm dễ hiểu hơn và ít tốn chi phí để cập nhật mà không làm thay đổi ứng xử bên ngoài
Hiện tại kỹ thuật mới này đang được áp dụng và triển khai ở các quốc gia có nền công nghiệp phần mềm phát triển (Mỹ, Nhật, Ấn Độ, ) và đang tiến đến một tiêu chuẩn trong qui trình phát triển phần mềm trong tường lai gần Trong một bài báo về tương lai của ngành công nghệ phần mềm, một chuyên gia trong lãnh vực quản lý và tư vấn các chiến lược phần mềm Alex Iskold đã đưa ra một nhận định rằng nền công nghệ phần mềm trong tương lai gần sẽ phát triển theo phương pháp phần mềm phát triển linh hoạt thay cho phương pháp mô hình thác nước đã tồn tại [15] Phương pháp phát triển phần mềm linh hoạt (Agile Development Method) ngoài việc đáp ứng khả năng tạo ra các phần mềm có sự ổn định cao còn có khả năng thích nghi và tiến hóa để thích hợp với môi trường hoạt động Phương pháp này dựa trên hai kỹ thuật chính đó là:
- Refactoring - Tái cấu trúc mã nguồn
- Developer Testing – Hoạt động kiểm thử do chính lập trình viên đảm nhận
Như vậy vấn đề nghiên cứu và ứng dụng kỹ thuật refactoring là một xu hướng tất yếu và cần thiết trong lãnh vực phát triển công nghệ phần mềm ngày nay
Trang 27CHƯƠNG II: LỖI CẤU TRÚC TRONG MÃ NGUỒN (BAD SMELLS IN CODE)
II.1 KHÁI NIỆM VỀ LỖI CẤU TRÚC (BAD SMELLS)
Trong khoa học máy tính, mã xấu hay lỗi cấu trúc (bad smells) là tất cả những dấu hiệu tồn tại trong mã nguồn của chương trình mà nó tiềm ẩn khả năng xảy ra lỗi trong quá trình hoạt động Các dấu hiệu đó có thể là: chương trình được thiết kế không logic, các phân đoạn mã nguồn có cấu trúc không đồng nhất và khó hiểu, mã nguồn trùng lắp, tên hàm và biến khó kiểm soát, lớp và phương thức phức tạp, v.v
Thông thường các dấu hiệu này sẽ được các nhà phát triển và lập trình phát hiện và tinh chỉnh qua các bước trong qui trình phát triển phần mềm dựa trên việc refactoring mã nguồn Như vậy có thể xem mã xấu là điều kiện để thực thi việc refactoring mã nguồn của một chương trình hay nói đúng hơn đó là cặp song hành: nếu một mã nguồn chương trình có bad smells thì refactoring để làm cho chương trình tốt hơn
II.2 LỖI CẤU TRÚC VÀ GIẢI PHÁP CẢI TIẾN
Dựa trên kinh nghiệm nhiều năm lập trình và nghiên cứu về refactoring, hai chuyên gia Kent Beck và Marting Fowler đã đề xuất một tập các mã xấu thường gặp[4] và giải pháp cải tiến dựa trên kỹ thuật refactoring
II.2.1 Duplicated Code - Trùng lặp mã
Nếu trong mã nguồn tồn tại những đoạn mã trùng lặp ở nhiều nơi:
- Sử dụng Extract Method để làm triệt tiêu các đoạn mã trùng lặp bên trong một lớp
- Khi hai lớp đồng kế thừa từ một lớp cha (sibling classes) có các mã nguồn trùng
lặp, áp dụng Extract Method trong cả hai lớp này sau đó dùng Pull Up Method đến
lớp cha
- Nếu tồn tại những đoạn mã tương tự nhau thì sử dụng Extract Method trên những phần tương tự nhau này Và sau đó có thể áp dụng tiếp kỹ thuật Form Template
Method
- Nếu có các phương thức cùng thực hiện một công việc với các thuật toán khác
nhau, thì chọn ra một thuật toán tốt nhất và áp dụng Substitute Algorithm
- Nếu hai lớp không có quan hệ với nhau mà có các đoạn mã trùng lặp, sử dụng
Extract Class trên một lớp và sau đó dùng thành phần lớp mới được tạo ra cho lớp
còn lại
Trang 28Ví dụ 1: Hai lớp con Salesman và Engneer đồng kế thừa từ lớp cha Employee có phương
thức getName bị trùng lặp => Sử dụng Pull Up Method
Ví dụ 2: Sử dụng Substitute Algorithm để thay mới thuật tóan trong thân phương thức
làm cho chương trình ngắn gọn và dễ hiểu hơn
String foundPerson(String[] people){ for (int i = 0; i < people.length; i++) {
if (people[i].equals ("Don")){ return "Don";
}
if (people[i].equals ("John")){ return "John";
}
if (people[i].equals ("Kent")){ return "Kent";
}
}
return ""; }
String foundPerson(String[] people){
List candidates = Arrays.asList(new String[] {"Don", "John", "Kent"});
for (int i=0; i<people.length; i++)
if (candidates.contains(people[i])) return people[i];
return ""; }
II.2.2 Long Method – Phương thức phức tạp
Một trong những yêu cầu đối với một chương trình nguồn là hạn chế các phương thức được tổ chức với số lượng dòng mã quá nhiều Điều này sẽ gây khó khăn cho việc kiểm soát và đọc hiểu mã lệnh trong quá trình cập nhật Khi đó giải pháp ở đây là chia tách để làm cho các phương thức nhỏ và tinh gọn hơn
Trang 29- Khi gặp các phương thức có nhiều dòng mã, 99% là chúng ta sử dụng kỹ thuật
Extract Method để làm ngắn các phương thức này bằng cách tạo ra một số phương
thức mới từ việc trích chọn một số đoạn mã từ phương thức ban đầu
- Nếu một phương thức đi kèm với nhiều tham số và biến tạm, sử dụng Replace
Temp with Query để triệt tiêu các biến tạm này Với danh sách dài của các tham
số, chúng ta sử dụng Introduce Parameter Object hoặc Preserve Whole Object để
làm ít chúng lại Nếu như lúc này vẫn còn nhiều biến tạm và tham số, một giải
pháp triệt để hơn là dùng Replace Method with Method Object
- Khi trích xuất một khối mã lệnh ra làm phương thức riêng, một giải pháp hiệu quả nhất trong việc xác định tên của phương thức mới tạo này sao cho phù hợp là dựa
vào thông tin chú giải về mục đích của khối mã lệnh đó
- Khi gặp các điều kiện và vòng lặp, chúng ta nên sử dụng Decompose Conditional
để tách và tạo ra các phương thức riêng
Ví dụ 1: Sử dụng kỹ thuật Extract Method để làm ngắn phương thức bằng cách tạo ra
một phương thức mới từ việc trích chọn một số đoạn mã từ phương thức ban đầu
class Program {
static void Main(string[] args){
Console.WriteLine("*** Please enter your credentials ***");
// Get user name and password
string userName; string passWord;
Console.Write("Enter User Name: "); userName = Console.ReadLine(); Console.Write("Enter Password: "); passWord = Console.ReadLine();
} }
class Program {
static void Main(string[] args){
Console.WriteLine("*** Please enter your name ***");
GetCredentials();
Console.ReadLine(); }
private static void GetCredentials(){ string userName;
string passWord;
Console.Write("Enter User Name: "); userName = Console.ReadLine(); Console.Write("Enter Password: "); passWord = Console.ReadLine(); }
}
Trang 30Ví dụ 2: Sử dụng Replace Temp with Query để chuyển đổi biểu thức thành phương
thức Thay thế các tham chiếu của biến tạm nhận được từ biểu thức bị chuyển đổi bởi phương thức vừa tạo mới
double basePrice = _quantity * _itemPrice;
II.2.3 Large Class – Qui mô lớp lớn
- Một lớp có qui mô lớn thường chứa nhiều biến số và tồn tại trùng lặp mã nguồn
Sử dụng Extract Class để đóng gói các biến thường đi chung với nhau lại Sử dụng Extract Subclass khi các phương thức kết hợp với các biến mở rộng chức năng của lớp Thông thường không phải lúc nào tất cả các biến trong một lớp cũng luôn được sử dụng , vì vậy chúng ta cũng có thể sử dụng Extract Class hoặc
Extract Subclass để trích xuất chúng nhiều lần
- Một thủ thuật hữu ích nữa là xem xét mục đích sử dụng của các lớp kết hợp với kỹ
thuật Extract Interface để tổ chức và phân chia lớp cho hợp lý
- Trong lập trình hướng đối tượng, khi gặp một lớp GUI có qui mô lớn cần chuyển dữ liệu và hoạt động xử lý đến một đối tượng có phạm vi riêng Điều này yêu cầu có sự trùng lắp dữ liệu ở hai nơi cũng như sự đồng nhất trong quá trình động bộ
Lúc này Duplicate Observed Data trong refactoring sẽ được xem xét và sử dụng
Ví dụ: Một lớp mà có một số chức năng chỉ được sử dụng cho một vài thực thể
(instances) cá biệt thì nên sử dụng Extract Subclass để tạo một lớp con đi kèm các chức
năng đó
Trang 31II.2.4 Long Parameter List - Danh sách tham số quá dài
Trong lập trình hướng thủ tục, việc trao đổi mọi thứ dữ liệu giữa các thủ tục và
hàm thông qua việc truyền tham số Một giải pháp khác tốt hơn là sử dụng dữ liệu toàn cục nhưng tiềm ẩn nhiều rủi ro trong việc kiểm soát Với lập trình hướng đối tượng thì khác, chúng ta không cần phải chuyển mọi thứ thông qua một danh sách dài các tham số khó hiểu mà chỉ là một ít dữ liệu cơ sở vừa đủ cho việc truy suất tất cả các giá trị cần thiết khác luôn được cập nhật mới
- Sử dụng Replace Parameter with Method để nhận dữ liệu từ một đối tượng đã
biết Đối tượng này có thể là một trường dữ liệu hoặc một tham số khác Sử dụng
Preserve Whole Object để thay thế một nhóm dữ liệu trong lớp bởi một thành
phần dữ liệu thay thế chung của bản thân lớp đó.Với những mục dữ liệu riêng mà
không gắn liền với một đối tượng nào cả, sử dụng Introduce Parameter Object
- Một ngoại lệ quan trọng trong việc thực hiện những thay đổi này, đó là khi chúng ta không muốn tạo ra một sự phụ thuộc từ đối tượng được gọi đến một đối tượng lớn hơn Giải pháp hợp lý trong trường hợp này là chuyển dữ liệu như các tham số, khi đó cần chú ý đến những rủi ro có liên quan Nếu danh sách tham số quá dài hoặc thường xuyên thay đổi, chúng ta cần xem xét lại cấu trúc phụ thuộc
Ví dụ 1: Sử dụng Replace Parameter with Method để rút gọn tham số phương thức
public double getPrice() {
int basePrice = _quantity * _itemPrice;
int discountLevel;
if (_quantity > 100) discountLevel = 2; else discountLevel = 1;
double finalPrice = discountedPrice (basePrice, discountLevel);
return finalPrice; }
private double discountedPrice (int basePrice, int discountLevel) { if (discountLevel == 2) return basePrice * 0.1;
else return basePrice * 0.05; }
public double getPrice() {
int basePrice = _quantity * _itemPrice;
int discountLevel = getDiscountLevel();
double finalPrice = discountedPrice (basePrice);
return finalPrice; }
private int getDiscountLevel() { if (_quantity > 100) return 2; else return 1;
}
Trang 32private double discountedPrice (int basePrice) {
if (getDiscountLevel() == 2) return basePrice * 0.1;
else return basePrice * 0.05; }
Ví dụ 2: Với những tham số là các mục dữ liệu riêng mà không gắn liền với một đối
tượng nào cả, sử dụng Introduce Parameter Object
II.2.5 Divergent Change – Cấu trúc lớp ít có tính khả biến
Cấu trúc của phần mềm phải được thiết kế sao cho dễ dàng trong việc thay đổi và cập nhật Nếu một lớp bị thay đổi theo nhiều cách khác nhau tùy thuộc vào các nguyên nhân khác nhau thì chúng ta nên chia lớp đó ra làm hai, trong đó mỗi lớp được tạo ra dựa trên yếu tố đặc trưng thuận lợi với các cách thay đổi nêu trên Trong trường hợp chia tách
này, giải pháp chính là sử dụng Extract Class
Ví dụ: Sử dụng kỹ thuật Extract Class tạo một lớp mới và dịch chuyển các thuộc tính và
phương thức có liên quan từ lớp cũ sang
II.2.6 Shotgun Surgery – Lớp được thiết kế không hợp lý và bị phân rã
Trái ngược với Divergent Change là Shotgun Surgery Mỗi khi chúng ta thực hiện một sự thay đổi trong chương trình, thông thường việc thay đổi này kéo theo một chuỗi các thay đổi nhỏ ở nhiều lớp khác nhau và diễn ra ở khắp nơi Lúc này việc phát hiện chính xác và thay đổi toàn bộ là một điều rất khó khăn không loại trừ có thể thiếu sót ở một nơi quan trọng nào đó
- Sử dụng Move Method & Move Field để dịch chuyển (thâu gôm) các phương thức
và thuộc tính từ lớp này sang lớp khác để làm cho chương trình có cấu trúc rõ hơn - Nếu chưa có lớp nào thích hợp để tích hợp vào thì tạo một lớp mới Và trong
trường hợp này, chúng ta sử dụng giải pháp Inline Class để tích hợp các thành
phần thay đổi này
Trang 33Ví dụ 1: Nếu một thuộc tính aField được sử dụng nhiều hơn trong lớp khác Class 2 thay
vì lớp chứa nó Class 1 thì chúng ta nên sử dụng Move Field để dịch chuyển phương thức
đó sang lớp kia
Ví dụ 2: Nếu hai lớp có qui mô nhỏ (không làm gì nhiều) và có quan hệ với nhau thì nên
sử dụng Inline Class để hợp nhất chúng lại một lớp
II.2.7 Feature Envy – Phân bố phương thức giữa các lớp không hợp lý
Trong lập trình hướng đối tượng, đôi lúc chúng ta gặp trường hợp một phương thức của một lớp mà nó sử dụng hoặc hỗ trợ nhiều chức năng (thuộc tính và phương
thức) cho một lớp khác hơn chính lớp chứa nó Khi đó chúng ta sẽ sử dụng Extract
Method và Move Method để trích xuất và dịch chuyển phương thức vào một vị trị lớp
thích hợp nhất
Ví dụ: Một phương thức của một lớp Class 1 mà nó sử dụng nhiều chức năng (thuộc tính
và phương thức) của một lớp khác Class 2 hơn chính nó thì sử dụng Move Method để
dịch chuyển phương thức đó sang lớp kia
Trang 34II.2.8 Data Clumps – Gôm cụm dữ liệu
Thông thường trong mã nguồn, chúng ta cũng gặp trường hợp cùng lúc ba hoặc bốn mục dữ liệu thường đi chung với nhau (nhiều thuộc tính trong lớp, các tham số truyền cho phương thức,…) và được sử dụng ở nhiều nơi Một giải pháp tốt hơn là gôm cụm chúng lại và chuyển sang một lớp đối tượng:
- Nếu các mục dữ liệu đó là các thuộc tính -> sử dụng Extract Class để định nghĩa
các lớp mới phù hợp với từng nhóm thuộc tính
- Nếu các mục dữ liệu là các tham số -> sử dụng Introduce Parameter Object hoặc
Preserve Whole Object để làm gọn hơn biểu thức tham số truyền vào và thân
chương trình xử lý
Ví dụ: Thay vì truy xuất từng giá trị thuộc tính riêng lẻ của một đối tượng và truyền các
giá trị này theo kiểu tham số cho một phương thức Chúng ta sẽ sử dụng kỹ thuật
Preserve Whole Object để truyền nguyên đối tượng đó
int low = daysTempRange().getLow(); int high = daysTempRange().getHigh(); withinPlan = plan.withinRange(low, high);
withinPlan = plan.withinRange(daysTempRange());
II.2.9 Primitive Obsession – Khả năng thể hiện dữ liệu của lớp bị hạn chế
Một trong những mã xấu thường gặp là sự gộp chung các lớp đối tượng có quan hệ với nhau thành một lớp đối tượng chung có cấu trúc phức tạp và khó hiểu Trong trường hợp này, chúng ta cần phải tùy biến cấu trúc ban đầu cho hợp lý hơn:
- Nếu một thuộc tính dữ liệu trong lớp cần được bổ sung thêm thộng tin hoặc hành
vi, sử dụng Replace Data Value with Object để chuyển đổi thuộc tính dữ liệu đó
sang một lớp đối tượng
- Nếu giá trị dữ liệu là một mã kiểu (type code) thì sử dụng Replace Type Code with
Class để chuyển sang một lớp mới
- Nếu mã kiểu (type code) là điều kiện rẻ hướng (làm ảnh hưởng hành vi của một
lớp) thì sử dụng Replace Type Code with Subclasses hoặc Replace Type Code with
State/Strategy để chuyển đổi mã kiểu này sang một đối tượng tĩnh
- Nếu là các thuộc tính ban đầu thường đi chung thì sử dụng Extract Class, và nếu đó là các danh sách tham số thì sử dụng Introduce Parameter Object hoặc Replace
Array with Object trong trường hợp một mảng dữ liệu để chuyển đổi sang một lớp
đối tượng tương ứng
Trang 35Ví dụ 1: Chúng ta có một lớp Order dùng để lưu các dữ liệu về đơn hàng của đối tượng
khách hàng trong đó customer là một thuộc tính chuỗi của lớp Trên thực tế, thì đôi lúc
người sử dụng cần biết thêm thông tin về khách hàng tương trong đơn hàng (địa chỉ, số
điện thoại,…) Lúc này chúng ta cần sử dụng kỹ thuật Replace Data Value with Object
để chuyển đổi dữ liệu customer sang một đối tượng Customer
Ví dụ 2: Sử dụng Replace Type Code with Class để thay kiểu dữ liệu số bởi một lớp
class Person {
public static final int O = 0; public static final int A = 1; public static final int B = 2; public static final int AB = 3; private int _bloodGroup;
public Person (int bloodGroup) { _bloodGroup = bloodGroup; }
public void setBloodGroup(int arg) { _bloodGroup = arg;
}
public int getBloodGroup() { return _bloodGroup; }
class BloodGroup {
public static final BloodGroup O = new BloodGroup(0); public static final BloodGroup A = new BloodGroup(1); public static final BloodGroup B = new BloodGroup(2); public static final BloodGroup AB = new BloodGroup(3); private static final BloodGroup[] _values = {O, A, B, AB}; private final int _code;
private BloodGroup (int code ) { _code = code;
}
public int getCode() { return _code; }
Trang 36
public static BloodGroup code(int arg) { return _values[arg];
} }
class Person {
public static final int O = BloodGroup.O getCode(); public static final int A = BloodGroup.A.getCode(); public static final int B = BloodGroup.B.getCode(); public static final int AB = BloodGroup.AB.getCode(); private BloodGroup _bloodGroup;
public Person (BloodGroup bloodGroup ) { _bloodGroup = bloodGroup;
}
public int getBloodGroupCode() { return _bloodGroup.getCode(); }
public void setBloodGroup(BloodGroup arg) { _bloodGroup = arg;
}
public BloodGroup getBloodGroup() { return _bloodGroup;
} }
II.2.10 Switch Statements – Khối lệnh điều kiện rẽ hướng không hợp lý
Các câu lệnh switch thường là nguyên nhân phát sinh việc trùng lặp và chúng nằm rải rác khắp ở nhiều nơi trong thân chương trình Trong trường hợp cần thêm một giá trị điều kiện rẻ nhánh mới, chúng ta phải thực hiện việc kiểm tra tất cả các câu lệnh switch này Một giải pháp thích hợp và cải tiến hơn là cần được xét đến đó là sử dụng lớp và tính chất đa hình (polymorphism) trong hướng đối tượng
- Đối với các mã kiểu, sử dụng kết hợp các kỹ thuật Extract Method, Move Method và Replace Type Code with Subclasses/State/Strategy để tạo nên các lớp mới từ
việc trích xuất các khối lệnh điều kiện rẽ hướng sau đó dịch chuyển các lớp mới tạo này vào trong các lớp mà ở đó tính đa hình được sử dụng Và cuối cùng là
Replace Conditional with Polymorphism
- Nếu đó chỉ là một vài trường hợp rẽ nhánh trong một phương thức đơn mà tính
chất đa hình không cần thiết, một giái pháp thích hợp hơn là áp dụng Replace
Parameter with Explicit Methods hoặc Introduce Null Object trong trường hợp có
một điều kiện Null
Ví dụ 1: Câu lệnh switch bên dưới lựa chọn các xử lý khác nhau tùy thuộc vào giá trị
của kiểu đối tượng Sử dụng Replace Conditional with Polymorphism để chuyển đổi
mỗi nhánh của điều kiện thành một phương thức chồng (overriding method) trong mỗi lớp con
Trang 37double getSpeed() { switch (_type) {
case EUROPEAN:
return getBaseSpeed(); case AFRICAN:
return getBaseSpeed() - getLoadFactor() * _numberOfCoconuts; case NORWEGIAN_BLUE:
return (_isNailed) ? 0 : getBaseSpeed(_voltage); }
throw new RuntimeException ("Should be unreachable"); }
Ví dụ 2: Nếu chúng ta có phương thức mà nó thực thi các mã lệnh khác nhau phụ thuộc
vào giá trị của các tham số được liệt kê thì sử dụng Replace Parameter with Explicit
Methods để tạo một phương thức riêng tương tứng với mỗi giá trị của tham số
void setValue (String name, int value) { if (name.equals("height"))
_height = value;
if (name.equals("width")) _width = value;
Assert.shouldNeverReachHere(); }
void setHeight(int arg) { _height = arg; }
void setWidth (int arg) { _width = arg; }
Trang 38II.2.11 Lazy Class – Lớp được định nghĩa không cần thiết
Một lớp đối tượng được tạo ra bao giờ cũng có những lợi ích của nó, tuy nhiên đi kèm với những lợi ích đó là những rủi ro có thể xảy ra và phí tổn trong quá trình hoạt động và bảo trì Vì vậy chúng ta cần phải cân xét hai yếu tố này khi xác định sự hiện diện của một lớp Trong quá trình refactoring, đến một lúc nào đó có thể chúng ta nhận ra rằng sự hiện diện của một lớp đối tượng A là không cần thiết, lúc này chúng ta có thể sử dụng
kỹ thuật Collapse Hierarchy hoặc Inline Class để giản lược hoặc tích hợp nó vào trong
một lớp khác
Ví dụ: Nếu 2 lớp có quan hệ kế thừa không có sự khác biệt nhau nhiều, sử dụng Collapse Hierarchy để tích hợp chúng lại với nhau (giản lược lớp con)
II.2.12 Speculative Generality – Cấu trúc bị thiết kế dư thừa
Đối với những phương thức và lớp đối tượng được thiết kế nhưng trên thực tế không thật sự dùng đến thì chúng ta nên xem xét và loại bỏ
- Sử dụng Collapse Hierarchy để loại bỏ các lớp trừu tượng (abstract classses) thật
sự ít khi được sử dụng
- Xóa bỏ tính chất ủy quyền không cần thiết giữa các lớp với kỹ thuật Inline Class - Sử dụng Remove Parameter để xóa bỏ các tham số không thật sự dùng đến trong
thân phương thức
- Sử dụng Rename Method để thay đổi tên phương thức với mục đích dễ hiểu nhất
Ví dụ: Một tham số mà không thật sự dùng đến trong thân phương thức thì nên dùng Remove Parameter để xóa bỏ
Trang 39II.2.13 Temporary Field – Lạm dụng thuộc tính tạm thời
Chúng ta cũng có thể xem xét việc từ chối định nghĩa và sử dụng các biến thành viên không được sử dụng thường xuyên bởi lớp
- Trên thực tế khi gặp trường hợp các biến mồ côi, sử dụng Extract Class để gôm
nhóm chúng cùng các mã lệnh có liên quan và đặt vào trong một lớp mới Sử dụng
Introduce Null Object nếu biến đó tham chiếu đến trường hợp đặc biệt Null
- Với các thuật toán phức tạp làm phát sinh các thuộc tính tạm trong quá trình xử lý
thì nên hạn chế việc truyền vào một danh sách tham số quá lớn
II.2.14 Message Chains –Chuỗi phương thức liên hoàn khó kiểm soát
Khi gặp một chuỗi các phương thức/thông điệp yêu cầu xử lý được cấu trúc như thế này getA().getB().getC().getD().getE().doIt(); Đây là một hình thức quan hệ kết buộc trong cấu trúc giữa các phương thức mà bất kỳ sự thay đổi nào trong mối quan hệ này cũng làm ảnh hưởng kết quả nhận được
Trong trường hợp này tùy theo quan điểm của từng người, có người vẫn chấp nhận sự hiện diện trong mã nguồn, có người thì cho rằng đó là một cấu trúc xấu cần phải
cải tiến dựa trên kỹ thuật Extract Method và Move Method
II.2.15 Middle Man – Quan hệ ủy quyền không hợp lý/logic
Tính chất ủy quyền trong hướng đối tượng rất hữu dụng, tuy nhiên đôi lúc nó cũng bị lạm dụng quá mức Nếu một lớp hoạt động như một sự ủy quyền nhưng lại thực hiện một số công việc bổ sung không hữu ích thì chúng ta cũng nên xem xét để hủy bỏ trong một số trường hợp
- Nhìn vào giao diện của một lớp, nếu thấy rằng ½ các phương thức ủy quyền đến
một lớp khác, thì hãy sử dụng Remove Middle Man để tái cấu trúc lại tính chất ủy quyền của các phương thức giữa các lớp này
- Nếu chỉ xảy ra với vài phương thức, sử dụng Inline Classs để tích hợp/hợp nhất chúng vào trong lớp gọi
- Nếu có thêm vào các hành vi xử lý, sử dụng Replace Delegation with Inheritance
để chuyển đổi quan hệ ủy quyền không hợp lý này trong một lớp con của một đối
tượng thực tế (quan hệ thừa kế)
Trang 40Ví dụ: Cải tiến mã xấu về quan hệ ủy quyền không hợp lý/logic thông qua kỹ thuật Remove Middle Man
class Person {
Department _department; public Person getManager() {
return _department.getManager(); }
class Department {
private Person _manager;
public Department (Person manager) { _manager = manager;