重構代碼(應如寫詩)

背景

最近公司做了個項目,深深體會到架構設計以及代碼優化有多麼的重要。
回頭看自己的代碼都覺得特別混亂,有時候還要看很久才能看懂,可擴展性特別差,完全是爲了完成需求而編碼的。說得形象一點就像修水管,最後全部都漏水了。
個人覺得代碼重構非常有必要,寫程序不但要給機器運行,更讓人看的明白。
寫代碼如寫詩一樣才行。

實例

  • 一個圖書館出租書的程序。
  • 計算每一個讀者的消費金額並且打印詳情清單。
  • 打印信息:
  • 讀者租了哪些書、租期多長、根據租借時間和書的類型算出費用。
  • 書分類:普通讀本、少兒讀本、新書
  • 計算費用,以及計算積分。積分根據書的種類是否爲新書而又有所不同。
常見代碼

按照實例需求,經常都是類似這樣子寫代碼的,如下:
Book書本類
主要是關於書名稱和分類信息。


/**
 * 書本
 */
public class Book {

    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 Book() {
    }

    public Book(String title, int priceCode) {
        this.title = title;
        this.priceCode = priceCode;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public int getPriceCode() {
        return priceCode;
    }

    public void setPriceCode(int priceCode) {
        this.priceCode = priceCode;
    }
}

Rental 租借信息
主要是寫租借信息,包括書和租借天數的關係。

/**
 * 租借信息
 */
public class Rental {

     private Book book;
     private int daysRented;//租借天數

    public Rental() {
    }

    public Rental(Book book, int daysRented) {
        this.book = book;
        this.daysRented = daysRented;
    }


    public Book getBook() {
        return book;
    }

    public void setBook(Book book) {
        this.book = book;
    }

    public int getDaysRented() {
        return daysRented;
    }

    public void setDaysRented(int daysRented) {
        this.daysRented = daysRented;
    }
}

Customer 讀者類
主要寫租借費用計算以及租借的書的關係。


/**
 * 讀者
 */
public class Customer {

    private String name;
    private List<Rental> rentals = new ArrayList();

    public Customer() {
    }

    public Customer(String name) {
        this.name = name;
    }

    //添加租書信息
    public void addRental(Rental rental) {
        rentals.add(rental);
    }

    //生成訂單
    public String generateOrder() {

        double total = 0;//計算租借總數量
        int frequentRenterPoints = 0;//計算積分
        String result = "Rental Record for "+getName()+"\n";
        for (Rental rental : rentals) {
            double thisAmount = 0;
            switch (rental.getBook().getPriceCode()){
                case Book.REGULAR:
                    thisAmount += 2;
                    if (rental.getDaysRented() > 2){
                        thisAmount += (rental.getDaysRented() - 2) *1.5;
                    }
                    break;
                case Book.NEW_RELEASE:
                        thisAmount += rental.getDaysRented()*3;
                    break;
                case Book.CHILDRENS:
                    thisAmount += 1.5;
                    if (rental.getDaysRented() > 3){
                        thisAmount += (rental.getDaysRented() - 3) *1.5;
                    }
                    break;
            }

            frequentRenterPoints++;
            if ((rental.getBook().getPriceCode() == Book.NEW_RELEASE) && rental.getDaysRented() >1){
                frequentRenterPoints++;
            }

            if ((rental.getBook().getPriceCode() == Book.NEW_RELEASE) && rental.getDaysRented() >1) {
                frequentRenterPoints++;
            }

            result += "\t"+rental.getBook().getTitle() + "\t"+String.valueOf(thisAmount)+"\n";
            total +=thisAmount;

        }

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

        return result;
    }


    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

測試類:

/**
 * 一個圖書館出租書的程序。
 * 計算每一個讀者的消費金額並且打印詳情清單。
 * 打印信息:
 * 讀者租了哪些書、租期多長、根據租借時間和書的類型算出費用。
 * 書分類:普通讀本、少兒讀本、新書
 * 計算費用,以及計算積分。積分根據書的種類是否爲新書而又有所不同。
 *
 */
public class Test {

    public static void main(String[] args) {
        Customer customer = new Customer();
        Book book = new Book("Java入門到放棄", Book.NEW_RELEASE);
        Book book1 = new Book("python入門到放棄", Book.CHILDRENS);
        Book book2 = new Book("golang入門到放棄", Book.REGULAR);

        customer.addRental(new Rental(book,8));
        customer.addRental(new Rental(book1,4));
        customer.addRental(new Rental(book2,6));

        customer.setName("zero");
        System.out.println(customer.generateOrder());

    }
}
第一次重構

首先:分析一下上面的代碼

    1. 結構:

總體結構

  • 2.假如某天產品跑過來(弄死她吧),需要你增加書的分類規則或者計費規則的時候,上面的代碼你怎麼做呢。估計又寫個差不多方法,然而你會發現其實邏輯跟上面的代碼非常相似的。還有更好的辦法不?

接着:直接看下面的代碼重構唄
Book類:
將按照書的不同類型,按照不同價格統計的方法移動到Book類中,因爲這個按理應該屬於Book類中的。

public class Book {

    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 Book() {
    }

    public Book(String title, int priceCode) {
        this.title = title;
        this.priceCode = priceCode;
    }


    //1.提取統計錢的方法
    public double getCharge(int daysRented) {
        double result = 0;
        switch (getPriceCode()) {
            case Book.REGULAR:
                result += 2;
                if (daysRented > 2) {
                    result += (daysRented - 2) * 1.5;
                }
                break;
            case Book.NEW_RELEASE:
                result += daysRented * 3;
                break;
            case Book.CHILDRENS:
                result += 1.5;
                if (daysRented > 3) {
                    result += (daysRented - 3) * 1.5;
                }
                break;
        }
        return result;
    }



    //1.提取計算會員積分的方法
    public int getFrequentRenterPoints(int daysRented) {
        if ((getPriceCode() == Book.NEW_RELEASE) && daysRented > 1) {
            return 2;
        }
        return 1;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public int getPriceCode() {
        return priceCode;
    }

    public void setPriceCode(int priceCode) {
        this.priceCode = priceCode;
    }
}

Rental 類:
主要是調用提取統計錢和積分的方法。

public class Rental {

    private Book book;
    private int daysRented;

    public Rental() {
    }

    public Rental(Book book, int daysRented) {
        this.book = book;
        this.daysRented = daysRented;
    }

    //1.提取統計錢的方法
    public double getCharge() {
        return book.getCharge(daysRented);
    }

    //1.提取計算會員積分的方法
    public int getFrequentRenterPoints() {
        return book.getFrequentRenterPoints(daysRented);
    }


    public Book getBook() {
        return book;
    }

    public void setBook(Book book) {
        this.book = book;
    }

    public int getDaysRented() {
        return daysRented;
    }

    public void setDaysRented(int daysRented) {
        this.daysRented = daysRented;
    }
}

Customer 讀者類
主要是去掉多餘的臨時變量total,frequentRenterPoints等。

public class Customer {

    private String name;
    private List<Rental> rentals = new ArrayList();

    public Customer() {
    }

    public Customer(String name) {
        this.name = name;
    }

    //添加租書信息
    public void addRental(Rental rental) {
        rentals.add(rental);
    }

    //生成訂單
    public String generateOrder() {
        String result = "Rental Record for " + getName() + "\n";
        for (Rental rental : rentals) {
            result += "\t" + rental.getBook().getTitle() + "\t" + String.valueOf(rental.getCharge()) + "\n";
        }
        result += "Amount owed is " + String.valueOf(getTotalCharge()) + "\n";
        result += "You earned " + String.valueOf(getFrequentRenterPoints()) + "frequent renter points";

        return result;
    }

    //獲取購買總數
    private double getTotalCharge() {
        double result = 0;
        for (Rental rental : rentals) {
            result += rental.getCharge();
        }
        return result;
    }

    //統計積分
    private double getFrequentRenterPoints() {
        double result = 0;
        for (Rental rental : rentals) {
            result += rental.getFrequentRenterPoints();
        }
        return result;
    }


    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

最後
測試結果跟上面的一樣,就是將代碼的結果調動一下。
現在大致的UML類圖如下:

第二次重構

經過第一次重構,還是沒有實現需求修改增加多個分類的效果。那麼接下來使用接口抽象來再次重構。

Price接口
接口抽象兩個規約方法,具體如下

public abstract class Price {

    abstract int getPriceCode();
    //1.提取統計總價的方法
    abstract double getCharge(int daysRented);

    //1.提取計算會員積分的方法
    public int getFrequentRenterPoints(int daysRented) {
        return 1;
    }

}

RegularPrice 普通的書價格類

public class RegularPrice extends Price {
    @Override
    int getPriceCode() {
        return Book.REGULAR;
    }

    @Override
    public double getCharge(int daysRented) {
        double result = 2;
        if (daysRented >2) {
            result += (daysRented - 2) * 1.5;
        }
        return result;
    }
}

ChildrensPrice 少兒讀物類價格

public class ChildrensPrice extends Price {
    @Override
    int getPriceCode() {
        return Book.CHILDRENS;
    }


    @Override
    public double getCharge(int daysRented) {
        double result = 1.5;
        if (daysRented >3) {
            result += (daysRented - 3) * 1.5;
        }
        return  result;
    }
}

NewReleasePrice 新書型類價格

public class NewReleasePrice extends Price {
    @Override
    int getPriceCode() {
        return Book.NEW_RELEASE;
    }

    @Override
    public double getCharge(int daysRented) {
        return daysRented * 3;
    }

    @Override
    public int getFrequentRenterPoints(int daysRented) {
        return (daysRented > 1)?2:1;
    }
}

Book類
將priceCode換成Price。

public class Book {

    public static final int CHILDRENS = 2;
    public static final int REGULAR = 0;
    public static final int NEW_RELEASE = 1;


    private String title;
    private Price _price;

    public Book() {
    }

    public Book(String title, int priceCode) {
        this.title = title;
        setPriceCode(priceCode);
    }

    //1.提取統計數量的方法
    public double getCharge(int daysRented) {
        return _price.getCharge(daysRented);
    }

    //1.提取計算會員積分的方法
    public int getFrequentRenterPoints(int daysRented) {
        if ((getPriceCode() == Book.NEW_RELEASE) && daysRented > 1) {
            return 2;
        }
        return 1;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public int getPriceCode() {
        return _price.getPriceCode();
    }

    public void setPriceCode(int arg) {
        switch (arg){
            case REGULAR:
                _price = new RegularPrice();
                break;
            case CHILDRENS:
                _price = new ChildrensPrice();
                break;
            case NEW_RELEASE:
                _price = new NewReleasePrice();
                break;
                default:
                    throw new IllegalArgumentException("Incorrect Price code");
        }
    }
}

最終類圖如下:

總結

大致的工作如下:

  • 抽離成方法。
  • 移動方法到所屬的類。
  • 用多態性替換條件。
  • 自我封裝。
  • 用策略替換類型代碼。

最後想說:
如果你發現自己需要爲程序添加一個特性,而代碼結構使你無法很方便地達成目的,那麼就先重構那個程序,使特性的添加比較容易進行,然後再添加特性。
寫代碼就應該像寫詩一樣,而不是沒BUG,我就不動它。

參考文章

重構】作者: Martin Fowler

最後

如果對 Java、大數據感興趣請長按二維碼關注一波,我會努力帶給你們價值。覺得對你哪怕有一丁點幫助的請幫忙點個贊或者轉發哦。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章