Go to .NET Center USOL Vietnam
.NET Center
Nguyễn Hữu Đạt và Phạm Trọng Tài

Nhập môn Refactoring

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:

  1. Viết một danh sách các số từ 2 tới maxNumbers mà bạn muốn tìm. Gọi là list A.
  2. Viết số 2, số nguyên tố đầu tiên, vào một list kết quả. Gọi là list B.
  3. Xóa bỏ 2 và bội của 2 khỏi list A.
  4. Số đầu tiên còn lại trong list A là số nguyên tố. Viết số này sang list B.
  5. Xóa bỏ số đó và tất cả bội của nó khỏi list A.
  6. 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)
        {
            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();
    }
}

1. Bước đầu tiên khi thực hiện refactoring

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;
    }
}

2. Phân tích và phân phối lại phương thực GetPrimeNumbers()

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.

2.1 Extract method đầu tiên, ta hãy xem đoạn code

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.

2.2 Tương tự như vậy, đoạn code

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();
}

2.3 Bây giờ ta chú ý đến phần code 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 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++;
    }
 } 

2.4 Ta thấy rằng, câu lệnh điều khiển trong vòng for không “đẹp”, khó đọc, ta nên sửa như sau, đổ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++;
    }
} 

2.5 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;
     }
}

2.6 Đoạn code :

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;
    }
}

2.7 Đổi tên biến:

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?

3. Di chuyển phạm vi của biến.

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.
}

4. Sử dụng BitArray trong System.Collections

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();
    }
}

5. Bạn chú ý đến đoạn code

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;
}

6. Bước 6:

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();
    }
}

7. Bước tiếp theo:

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();
    }
}

8. Bước tiếp theo:

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);
        }
    }
}

9. Ta xem xét đoạn code 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 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;
        }
    }
}

10. Bước tiếp theo

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;
        }
    }
}

11. Đặt lại tên biến.

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.

Go to .NET Center
Copyright (C) 2007 USOL Vietnam. All right reserved.