Skip to content

Refactor

sinhhn edited this page Jun 20, 2017 · 5 revisions

Refactor là gì

Refactor nôm na là quá trình thay đổi thiết kế của ứng dụng mà không làm ảnh hưởng đến logic của chương trình. Trong phát triển phần mềm, chúng ta nên refactor một cách thường xuyên. Điều này đảm bảo cho chương trình sẽ dễ bảo trì và mở rộng hơn.

Bắt đầu Refactor

Bài toán

Giả sử chúng ta xây dựng một ứng dụng thuê movie idol như sau:

  • Mỗi khách hàng sẽ thuê 1 số movie nào đó
  • Có 3 loại movie: CHILDRENS, REGULAR, NEW_RELEASE
  • Tuỳ thuộc vào số ngày thuê, mỗi loại movie có 1 giá thuê và hệ số tích điểm khác
    • Nếu movie là CHILDRENS 3 ngày đầu hết 1.5$. Các ngày tiếp theo đó thì mỗi ngày 1.5$ và được 1 point.
    • Nếu movie là REGULAR thì hai ngày đầu hết 2$. Các ngày tiếp theo hết 1.5$ và được 1 point.
    • Nếu movie là NEW_RELEASE thì mỗi ngày hết 3$. Nếu số ngày thuê > 1 thì được 2 point ngược lại là 1 point.
  • Chương trình cần in ra tổng số tiền cần trả và số point người ấy đạt được.

Xác định bài toán

Dựa vào yêu cầu trên, ta có thể phân tích bài toán như sau:

1

Cài đặt:

  • Movie class
public class Movie {
    public static final int CHILDRENS = 2;
    public static final int REGULAR = 0;
    public static final int NEW_RELEASE = 1;
    private String _title;
    private int _priceCode;

    public Movie(String title, int priceCode) {
        _title = title;
        _priceCode = priceCode;
    }

    public int getPriceCode() {
        return _priceCode;
    }

    public void setPriceCode(int arg) {
        _priceCode = arg;
    }

    public String getTitle() {
        return _title;
    }
} 
  • Rental class

class Rental {
    private Movie _movie;
    private int _daysRented;

    public Rental(Movie movie, int daysRented) {
        _movie = movie;
        _daysRented = daysRented;
    }

    public int getDaysRented() {
        return _daysRented;
    }

    public Movie getMovie() {
        return _movie;
    }
}
  • Customer class
public class Customer {
    private String _name;
    private Vector _rentals = new Vector();

    public Customer(String name) {
        _name = name;
    }

    public void addRental(Rental arg) {
        _rentals.addElement(arg);
    }

    public String getName() {
        return _name;
    }


    public String statement() {
        double totalAmount = 0;
        int frequentRenterPoints = 0;
        Enumeration rentals = _rentals.elements();
        String result = "Rental Record for " + getName() + "\n";
        while (rentals.hasMoreElements()) {
            double thisAmount = 0;
            Rental each = (Rental) rentals.nextElement();
            switch (each.getMovie().getPriceCode()) {
                case Movie.REGULAR:
                    thisAmount += 2;
                    if (each.getDaysRented() > 2)
                        thisAmount += (each.getDaysRented() - 2) * 1.5;
                    break;
                case Movie.NEW_RELEASE:
                    thisAmount += each.getDaysRented() * 3;
                    break;
                case Movie.CHILDRENS:
                    thisAmount += 1.5;
                    if (each.getDaysRented() > 3)
                        thisAmount += (each.getDaysRented() - 3) * 1.5;
                    break;
            }
            frequentRenterPoints++;
            if ((each.getMovie().getPriceCode() == Movie.NEW_RELEASE) && each.getDaysRented() > 1)
                frequentRenterPoints++;
            result += "\t" + each.getMovie().getTitle() + "\t" + String.valueOf(thisAmount) + "\n";
            totalAmount += thisAmount;

        }
        result += "Amount owed is " + String.valueOf(totalAmount) + "\n";
        result += "You earned " + String.valueOf(frequentRenterPoints) + "frequent renter points";
        return result;
    }
}

Biểu đồ tuần tự của thiết kế trên:

sequence-1

Phân tích thiết kế

Trước hết, bạn nghĩ về thiết kế trên như thế nào. Rõ ràng nó đảm bảo điều yêu cầu đặt ra. Tuy nhiê, bạn hãy để ý đến hàm statement. Nó đang làm quá nhiều thứ mà đáng nhẽ ra nó nên được thực hiện bởi những hàm riêng lẻ của các class khác nhau. Ban đầu thì có thể bạn thấy việc viết code không clean nó không có vấn đề gì cả. Tuy nhiên, vấn đề sẽ thực sự xảy ra khi bạn cần thay đổi, mở rộng hoặc sửa chữa nó. Chẳng hạn như chương trình trên, giả sử bạn muốn statement trả lại file HTML để up lên web thì sao. Bạn sẽ không tái sử dụng được logic nào ở trên cả. Ngoài cách copy và sửa những gì cần sửa. Cơ mà theo cách đó vẫn không ổn chút nào cả. Khi bạn copy statement ở trên thành htmlStatement và chẳng may logic cần thay đổi về cách tính point hoặc tính số tiền thì thế nào. Bạn phải sửa cả hai hàm nói trên. Điều này thật tồi tệ phải không.

Bước đầu tiên của Refactoring

Khi bắt đầu thực hiện Refactoring chúng ta hãy bắt tay vào với đoạn logic dài dòng. Nhiều khả năng ta sẽ chia được đoạn này thành nhiều hàm con cho ngắn gọn. Ta bắt đầu với hàm statement nói trên:

    public String statement() {
        double totalAmount = 0.0;
        int frequentRenterPoints = 0;
        Enumeration rentals = _rentals.elements();
        String result = "Rental Record for " + getName() + "\n";
        while (rentals.hasMoreElements()) {
            double thisAmount = 0;
            Rental each = (Rental) rentals.nextElement();
            // Start here
            switch (each.getMovie().getPriceCode()) {
                case Movie.REGULAR:
                    thisAmount += 2;
                    if (each.getDaysRented() > 2)
                        thisAmount += (each.getDaysRented() - 2) * 1.5;
                    break;
                case Movie.CHILDRENS:
                    thisAmount += 1.5;
                    if (each.getDaysRented() > 3)
                        thisAmount += (each.getDaysRented() - 3) * 1.5;
                    break;
                case Movie.NEW_RELEASE:
                    thisAmount += each.getDaysRented() * 3;
                    break;
            }
            // End here
            frequentRenterPoints++;
            if ((each.getMovie().getPriceCode() == Movie.NEW_RELEASE) && each.getDaysRented() > 1)
                frequentRenterPoints++;
            result += "\t" + each.getMovie().getTitle() + "\t" + String.valueOf(thisAmount) + "\n";
            totalAmount += thisAmount;

        }
        result += "Amound owed is " + String.valueOf(totalAmount) + "\n";
        result += "you earned " + String.valueOf(frequentRenterPoints) + " frequent points";
        return result;
    }

Trước hết, hãy để ý đoạn switch ở trên. Trong đoạn này, logic của chúng ta xoay quanh eachthisAmount. Trong 2 biến này, each không bị thay đổi bởi logic ở trên. Ngược lại thisAmount thì có. Đến đây chúng ta hãy nhớ rằng: Đối với những biến không thay đổi giá trị thì chúng ta có thể pass nó như là parameter cho một method được. Còn với những biến bị thay đổi giá trị thì cần phải cẩn thận. Tiếp tục để ý rằng thisAmount được thay đổi trong mỗi điều kiện của switch vì vậy ta có thể xem nó như 1 giá trị được trả về từ logic nằm trong switch. Vì vậy ta có thể extract như sau:

    public String statement() {
        double totalAmount = 0;
        int frequentRenterPoints = 0;
        Enumeration rentals = _rentals.elements();
        String result = "Rental Record for " + getName() + "\n";
        while (rentals.hasMoreElements()) {
            Rental each = (Rental) rentals.nextElement();
            double thisAmount = getAmountFor(each);
            frequentRenterPoints++;
            if ((each.getMovie().getPriceCode() == Movie.NEW_RELEASE) && each.getDaysRented() > 1)
                frequentRenterPoints++;
            result += "\t" + each.getMovie().getTitle() + "\t" + String.valueOf(thisAmount) + "\n";
            totalAmount += thisAmount;

        }
        result += "Amount owed is " + String.valueOf(totalAmount) + "\n";
        result += "You earned " + String.valueOf(frequentRenterPoints) + "frequent renter points";
        return result;
    }
    private double getAmountFor(Rental rental) {
        double thisAmount = 0;
        switch (rental.getMovie().getPriceCode()) {
            case Movie.REGULAR:
                thisAmount += 2;
                if (rental.getDaysRented() > 2)
                    thisAmount += (rental.getDaysRented() - 2) * 1.5;
                break;
            case Movie.NEW_RELEASE:
                thisAmount += rental.getDaysRented() * 3;
                break;
            case Movie.CHILDRENS:
                thisAmount += 1.5;
                if (rental.getDaysRented() > 3)
                    thisAmount += (rental.getDaysRented() - 3) * 1.5;
                break;
        }
        return thisAmount;
    }

Ta lại thấy rằng, hàm getAmountFor được extract ra ở trên nhận tham số là 1 đối tượng của Rental và trả về 1 giá trị double vì vậy nó chẳng liên quán gì đến class Customer ở đây. Thế nên, ta hãy move nó về với Rental và tiến hành sửa tên cho dễ hiểu:

class Rental {
    private Movie _movie;
    private int _daysRented;

    public Rental(Movie movie, int daysRented) {
        _movie = movie;
        _daysRented = daysRented;
    }

    public int getDaysRented() {
        return _daysRented;
    }

    public Movie getMovie() {
        return _movie;
    }

    public double getCharge() {
        double thisAmount = 0;
        switch (getMovie().getPriceCode()) {
            case Movie.REGULAR:
                thisAmount += 2;
                if (getDaysRented() > 2)
                    thisAmount += (getDaysRented() - 2) * 1.5;
                break;
            case Movie.NEW_RELEASE:
                thisAmount += getDaysRented() * 3;
                break;
            case Movie.CHILDRENS:
                thisAmount += 1.5;
                if (getDaysRented() > 3)
                    thisAmount += (getDaysRented() - 3) * 1.5;
                break;
        }
        return thisAmount;
    }
}

public class Customer {
    private String _name;
    private Vector _rentals = new Vector();

    public Customer(String name) {
        _name = name;
    }

    public void addRental(Rental arg) {
        _rentals.addElement(arg);
    }

    public String getName() {
        return _name;
    }


    public String statement() {
        double totalAmount = 0;
        int frequentRenterPoints = 0;
        Enumeration rentals = _rentals.elements();
        String result = "Rental Record for " + getName() + "\n";
        while (rentals.hasMoreElements()) {
            Rental each = (Rental) rentals.nextElement();
            double thisAmount = each.getCharge();
            frequentRenterPoints++;
            if ((each.getMovie().getPriceCode() == Movie.NEW_RELEASE) && each.getDaysRented() > 1)
                frequentRenterPoints++;
            result += "\t" + each.getMovie().getTitle() + "\t" + String.valueOf(thisAmount) + "\n";
            totalAmount += thisAmount;

        }
        result += "Amount owed is " + String.valueOf(totalAmount) + "\n";
        result += "You earned " + String.valueOf(frequentRenterPoints) + "frequent renter points";
        return result;
    }
}

Lúc này class diagram trở thành:

cd-2

Tiếp tục phân tích hàm getCharge của class Rental nói trên. Trong hàm này, thisAmount không bị thay đổi và dùng để tính totalAmount. Vì vậy, ta có thể sửa 1 chút nữa:

    public String statement() {
        double totalAmount = 0;
        int frequentRenterPoints = 0;
        Enumeration rentals = _rentals.elements();
        String result = "Rental Record for " + getName() + "\n";
        while (rentals.hasMoreElements()) {
            Rental each = (Rental) rentals.nextElement();
            frequentRenterPoints++;
            if ((each.getMovie().getPriceCode() == Movie.NEW_RELEASE) && each.getDaysRented() > 1)
                frequentRenterPoints++;
            result += "\t" + each.getMovie().getTitle() + "\t" + String.valueOf(each.getCharge()) + "\n";
            totalAmount += each.getCharge();
        }
        result += "Amount owed is " + String.valueOf(totalAmount) + "\n";
        result += "You earned " + String.valueOf(frequentRenterPoints) + "frequent renter points";
        return result;
    }

Có thể bạn sẽ đặt câu hỏi về việc performance vì hàm getCharge sẽ được gọi 2 lần. Tuy nhiên, việc sử dụng nhiều biến tạm nên là ít nhất có thể để tránh gây rắc rối.

Lại tiếp tục phân tích hàm getCharge. Tương tự như trên frequentRenterPoints được tính trong logic của if thế nên ta có thể extract nó thành 1 hàm:

Clone this wiki locally