LỜ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 T
Trang 1BỘ GIÁO DỤC VÀ ĐÀO TẠO
ĐẠI HỌC ĐÀ NẴNG
BÁO CÁO
LUẬN VĂN THẠC SĨ KỸ THUẬT
NGÀNH KHOA HỌC MÁY TÍNH
TÊN ĐỀ 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#
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 3MỤC LỤC
LỜI CAM ĐOAN 2
MỤC LỤC 3
DANH MỤC HÌNH ẢNH 5
MỞ ĐẦU 6
CHƯƠ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
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
IV.3 NHẬN XÉT VÀ KẾT LUẬN 95
CHƯƠNG V: KẾT LUẬN 96
V.1 ĐÁNH GIÁ KẾT QUẢ CỦA ĐỀ TÀI 96
V.2 PHẠM VI ỨNG DỤNG 96
V.3 HƯỚNG PHÁT TRIỂN 97
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
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)
trong các chương trình C#
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
umbers[i] = true;
}
int j = 2;
while (j <= (int)Math.Sqrt(maxNumber) + 1) {
for (int k = j + j; k <= maxNumber; k += j){
}
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 }))
if (!IsEqualNumbers(new PrimeNumbersGetter(6).GetPrimeNumbers(),
new int[] { 2, 3, 5 }))
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 }))
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);
}
return l.ToArray();
Ta cũng tách nó ra thành một phương thức khác
public int[] GetPrimeNumbers() {
// Other codes
return GetPrimeNumbersArray(numbers);
}
private int[] GetPrimeNumbersArray(bool[] numbers) {
List<int> l = new List<int>();
for (int k = 2; k <= maxNumber; ++k) {
if (numbers[k]) l.Add(k);
}
return l.ToArray();
}
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;
}
}
Với đoạn mã nguồn trên, câu lệnh if là không cần thiết, ta có thể bỏ đi, đƣa điều kiện lên
vòng while nhƣ sau:
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 < maxNumber) {
j++;
}
}
Ta thấy rằng, câu lệnh điều khiển trong vòng for không “đẹp” và khó đọc, ta nên sửa đổi tên biến k thành i
int j = 2;
while (j <= (int)Math.Sqrt(maxNumber) + 1) {
for (int i = 2; i * j <= maxNumber; i++){
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;
}
Thực hiện việc xóa bỏ các bội số của các số nguyên tố Do đó, có thể tách chúng ra thành một phương thức Kết quả thu được là:
public int[] GetPrimeNumbers() {
bool[] numbers = InitialNumbers();
for (int j = 2; j <= (int)Math.Sqrt(maxNumber) + 1; j++) {
if (!numbers[j]) continue;
RemoveMultiple(numbers, j);
}
}
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++) {
}
}
Đế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 12Khi đó lớp PrimeNumbersGetter
public class PrimeNumbersGetter {
private int maxNumber;
public PrimeNumbersGetter(int maxNumber) {
this.maxNumber = maxNumber;
}
public int[] GetPrimeNumbers() {
bool[] numbers = InitialNumbers(); // Other codes
}
// Other codes
private bool[] InitialNumbers() {
bool[] numbers = new bool[maxNumber + 1];
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;
numbers[i] = true;
}
}
public int[] GetPrimeNumbers() {
// Other codes
}
// Other codes, Method InitialNumbers() is nomore available
}
Việc sử dụng biến numbers theo kiểu bool[] đã được định nghĩa sẵn trong Thư viện System.Collections, đó là kiểu BitArray Do đó, ta nên chuyển khai báo của biến numbers thành kiểu BitArray Như vậy, ta sẽ loại bỏ được việc khởi tạo giá trị cho biến mảng numbers, bởi BitArray đã thực hiện điều đó Chúng ta cần lưu ý rằng biến mảng numbers lúc này chỉ đánh số từ 2 cho đến maxNumber, do đó, nó chỉ có (maxNumber - 1) phần tử, và nếu số duyệt là j thì vị trí của nó là numbers[j - 2] Sau khi sửa, ta có đoạn
mã nguồn sau: