![]() |
USOL Vietnam .NET Center Nguyễn Hữu Đạt và Phạm Trọng Tài |
Nội dụng: Giới thiệu một ví dụ cụ thể giúp bạn tiếp cận và hiểu refactoring (tái cơ cấu) là gì? Thông qua ví dụ, bạn có thể hiểu được refactoring được thực hiện như thế nào, hiệu quả của nó ra sao? Nếu bạn thực sự quan tâm, xin hãy đọc cuốn sách : "Refactoring : Improving the Design of Existing Code" by Martin Fowler
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:
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)
{
numbers[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;
}
}
}
List<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, bạn cần viết test cho phần code đó. Phần tests này là yếu tố cần thiết bởi vì quá trình refactoring có thể phát sinh bugs. Mỗi khi bạn thực hiện một lần refactor, bạn nên thực hiện test lại chương trình một lần, để đảm bảo chương trình không bị false. Tests tốt làm giảm thời gian cần thiết để tìm bugs. Bạn nên thực hiện test-refactor-test-refactor… Khi đó, nếu bugs phát sinh, thì cũng không quá khó để tìm ra bugs đó.
Trong ví dụ này, bạn có thể thêm đoạn test-code 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;
}
}
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.
bool[] numbers = new bool[maxNumber + 1];
for (int i = 0; i < numbers.Length; ++i)
{
numbers[i] = true;
}
Đoạn code khởi tạo list các số ban đầu ( list A ). Ta nên extract nó thành một phương thức khác, sử dụng “Extract Mothod”.
Kết quả:
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;
}
Bạn đã thực hiện refactor, trước khi thực hiện tiếp, bạn nên thực hiện test lại chương trình. Hãy nhớ rằng, luôn thực hiện test sau mỗi lần thực hiện refactor.
List<int> l = new List<int>();
for (int k = 2; k <= maxNumber; ++k)
{
if (numbers[k])
{
l.Add(k);
}
}
return l.ToArray();
Thực hiện xuất ra list kết quả chứa các số nguyên tố (list B). Ta cũng tách nó ra thành 1 method 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();
}
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 code 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++;
}
}
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++;
}
}
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;
}
}
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 method. 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);
}
return GetPrimeNumbersArray(numbers);
}
private void RemoveMultiple(bool[] numbers, int j)
{
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;
}
}
Bạn nên đặt tên biến, tham số truyền vào sao cho code 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;
}
}
Như bạn thấy, phương thức GetPrimenumbers() đã trở nên ngắn gọn, dễ đọc hơn nhất nhiều. Tuy nhiên, bạn có nghĩ rằng, chương trình này đã thực sự đẹp chưa? Bạn có cần refactor nữa 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 biến 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. Chú ý rằng, bạn cần 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.
Before:
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];
for (int i = 0; i < numbers.Length; ++i)
{
numbers[i] = true;
}
return numbers;
}
}
Affter:
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;
}
}
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 đó. Bạn 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, bạn có đoạn code sau:
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 (!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 int[] GetPrimeNumbersArray()
{
List<int> l = new List<int>();
for (int k = 2; k <= maxNumber; ++k)
{
if (numbers[k - 2])
{
l.Add(k);
}
}
return l.ToArray();
}
}
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 chuyển nó sang 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++)
{
numbers[i * number - 2] = false;
}
}
Chuyển thành
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;
}
Tươ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 method, và đặt tên cho nó. Ta viết được method sau:
private bool Remains(int number)
{
return numbers[number - 2];
}
Thay trong chương trình, ta có hình ảnh của code:
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;
}
private int[] GetPrimeNumbersArray()
{
List<int> l = new List<int>();
for (int k = 2; k <= maxNumber; ++k)
{
if (Remains(k))
{
l.Add(k);
}
}
return l.ToArray();
}
}
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”.
Ở 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;
}
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();
}
}
Ta 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);
}
}
}
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 code 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ì GetRemainNumbers() 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 IEnumerable<int> GetRemainNumbers()
{
for (int k = 2; k <= maxNumber; ++k)
{
if (Remains(k))
{
yield return k;
}
}
}
Trong quá trình refactor, việc đặt lại tên biến cần được chú ý, bởi vì bản thân tên biến có thể coi như là một lời comment hiệu quả nhất nói lên công việc của đoạn code.
Ví dụ phương thức sau
private void RemoveMultiple(Sieve sieve, int j)
{
for (int i = 2; i * j <= maxNumber; i++)
{
sieve.RemoveNumber(i * j);
}
}
Ta nên đặt lại tên tham biến 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++)
{
if (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
{
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;
}
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, bạn 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 code dễ đọc và dễ hiểu hơn rất nhiều. Việc sử dụng Refactoring có thể giúp ích rất nhiều trong quá trình phát triển phần mền.
![]() |
Copyright (C) 2007 USOL Vietnam. All right reserved. |