810 likes | 839 Views
第九讲 重构到模式. 什么是重构( Refactoring ). 所谓重构是这样一个过程:在不改变代码外在行为的前提下,对代码做出修改,以改进程序的内部结构。. Extract Method. 名称: Extract Method 动机:过长的函数或者一段需要注释才能让人理解用途的代码. Extract Method. 做法: 创造一个新函数,根据这个函数的意图来给它命名(以它 做什么 来命名,而不是以它 怎样做 命名) 将提炼出的代码从源函数拷贝到新建的目标函数中 仔细检查提炼出的代码,看看其中是否引用了 作用域限于源函数 的变量(包括局部变量和源函数参数)
E N D
什么是重构(Refactoring) • 所谓重构是这样一个过程:在不改变代码外在行为的前提下,对代码做出修改,以改进程序的内部结构。
Extract Method 名称:Extract Method 动机:过长的函数或者一段需要注释才能让人理解用途的代码
Extract Method 做法: • 创造一个新函数,根据这个函数的意图来给它命名(以它做什么来命名,而不是以它怎样做命名) • 将提炼出的代码从源函数拷贝到新建的目标函数中 • 仔细检查提炼出的代码,看看其中是否引用了作用域限于源函数的变量(包括局部变量和源函数参数) • 检查是否有仅用于被提炼码的临时变量,如果有,在目标函数中将它们声明为临时变量 • 检查被提炼码,看看是否有任何局部变量的值被它改变,如果一个临时变量值被修改了,看看是否可以将被提炼码处理为一个查询,并将结果赋值给相关变量,如果很难这样做,或如果被修改的变量不止一个,你就不能仅仅将这段代码原封不动地提炼出来,你可能需要先使用Split Temporary Variable,然后再尝试提炼,也可以使用Replace Temp with Query将临时变量消灭掉 • 将被提炼码中需要读取的局部变量,当作参数传给目标函数 • 处理完所有局部变量之后,进行编译 • 在源函数中,将被提炼码替换为对目标函数的调用 • 编译,测试
Extract Method 范例: void printOwing(double amount) { printBanner(); //print details System.out.println ("name:" + _name); System.out.println ("amount" + amount); } ↓ void printOwing(double amount) { printBanner(); printDetails(amount); } void printDetails (double amount) { System.out.println ("name:" + _name); System.out.println ("amount" + amount); }
重构-示例 • 这是一个影碟出租店用的程序,计算每一位顾客的消费金额并打印报表(Statement)。操作者告诉程序:顾客租了哪些影片、租期多长,程序便根据租赁时间和影片类型算出费用。影片分为三类:普通片、儿童片和新片。除了计算费用,还要为顾客计算点数;点数会随着租片种类是否为新片而有所不同。
Movie Movie只是一个简单的data 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 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 Customer class用来表示顾客,就像其他classes一样,它也拥有数据和相应的访问函数(accessor): 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(); //determine amounts for each line switch (each.getMovie().getPriceCode()) { case Movie.REGULAR: thisAmount += 2; if (each.getDaysRented() > 2) 16 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; } // add frequent renter points frequentRenterPoints ++; // add bonus for a two day new release rental if ((each.getMovie().getPriceCode() == Movie.NEW_RELEASE) && each.getDaysRented() > 1) frequentRenterPoints ++; //show figures for this rental result += "\t" + each.getMovie().getTitle()+ "\t" + String.valueOf(thisAmount) + "\n"; totalAmount += thisAmount; } //add footer lines result += "Amount owed is " + String.valueOf(totalAmount) + "\n"; result += "You earned " + String.valueOf(frequentRenterPoints) + " frequent renter points"; return result; }
起始程序的讨论 • 问题:考虑一下增加HTML格式打印报表的需求;改变影片分类规则 • 如果你发现自己需要为程序添加一个特性,而代码结构使你无法很方便地那么做,那就先重构那个程序,使特性的添加比较容易进行,然后再添加特性
重构的第一步 • 重构的第一个步骤永远相同:为即将修改的代码建立一组可靠的测试环境。这些测试必须有自我检测(self-checking)能力。
分解并重组statement() • 第一个步骤是找出代码的逻辑泥团(logical clump)并运用Extract Method
修改后的statement方法 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(); thisAmount = amountFor(each); // add frequent renter points frequentRenterPoints ++; // add bonus for a two day new release rental if ((each.getMovie().getPriceCode() == Movie.NEW_RELEASE) && each.getDaysRented() > 1) frequentRenterPoints ++; //show figures for this rental 20 result += "\t" + each.getMovie().getTitle()+ "\t" + String.valueOf(thisAmount) + "\n"; totalAmount += thisAmount; } //add footer lines result += "Amount owed is " + String.valueOf(totalAmount) + "\n"; result += "You earned " + String.valueOf(frequentRenterPoints) + " frequent renter points"; return result; } } private double amountFor(Rental each) { double thisAmount = 0; 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; 21 break; } return thisAmount; }
改名后的代码 private double amountFor(Rental aRental) { double result = 0; switch (aRental.getMovie().getPriceCode()) { case Movie.REGULAR: result += 2; if (aRental.getDaysRented() > 2) result += (aRental.getDaysRented() - 2) * 1.5; break; case Movie.NEW_RELEASE: result += aRental.getDaysRented() * 3; break; case Movie.CHILDRENS: result += 1.5; if (aRental.getDaysRented() > 3) result += (aRental.getDaysRented() - 3) * 1.5; break; } return result; }
转移 金额计算 代码 通过观察amountFor(),可以发现这个函数使用了来自Rental class信息,却没有使用来自customer class的信息 private double amountFor(Rental aRental) { double result = 0; switch (aRental.getMovie().getPriceCode()) { case Movie.REGULAR: result += 2; if (aRental.getDaysRented() > 2) result += (aRental.getDaysRented() - 2) * 1.5; 23 break; case Movie.NEW_RELEASE: result += aRental.getDaysRented() * 3; break; case Movie.CHILDRENS: result += 1.5; if (aRental.getDaysRented() > 3) result += (aRental.getDaysRented() - 3) * 1.5; break; } return result; }
Move Method Public double getCharge() { double result = 0; switch (getMovie().getPriceCode()) { case Movie.REGULAR: result += 2; if (getDaysRented() > 2) result += (getDaysRented() - 2) * 1.5; break; case Movie.NEW_RELEASE: result += getDaysRented() * 3; break; case Movie.CHILDRENS: result += 1.5; if (getDaysRented() > 3) result += (getDaysRented() - 3) * 1.5; break; } return result; }
Move Method Customer.amountFor()函数内的处理,使它委托(delegate)新函数: private double amountFor(Rental aRental) { return aRental.getCharge(); }
Move Method下一步 找出程序对于旧函数的所有引用点,并修改它们让它们改用新函数 class Customer 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(); thisAmount = each.getCharge(); // add frequent renter points 25 frequentRenterPoints ++; // add bonus for a two day new release rental if ((each.getMovie().getPriceCode() == Movie.NEW_RELEASE) && each.getDaysRented() > 1) frequentRenterPoints ++; //show figures for this rental result += "\t" + each.getMovie().getTitle()+ "\t" + String.valueOf(thisAmount) + "\n"; totalAmount += thisAmount; } //add footer lines result += "Amount owed is " + String.valueOf(totalAmount) + "\n"; result += "You earned " + String.valueOf(frequentRenterPoints) + " frequent renter points"; return result; }
转移金额计算函数后,所有classes的状态(state)转移金额计算函数后,所有classes的状态(state)
Replace Temp with Query 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(); // add frequent renter points frequentRenterPoints ++; // add bonus for a two day new release rental if ((each.getMovie().getPriceCode() == Movie.NEW_RELEASE) && each.getDaysRented() > 1) frequentRenterPoints ++; //show figures for this rental result += "\t" + each.getMovie().getTitle()+ "\t" + String.valueOf (each.getCharge()) + "\n"; totalAmount += each.getCharge(); } //add footer lines result += "Amount owed is " + String.valueOf(totalAmount) + "\n"; result += "You earned " + String.valueOf(frequentRenterPoints) + " frequent renter points"; return result; } }
提炼顾客积分计算代码 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(); // add frequent renter points frequentRenterPoints ++; // add bonus for a two day new release rental if ((each.getMovie().getPriceCode() == Movie.NEW_RELEASE) && each.getDaysRented() > 1) frequentRenterPoints ++; //show figures for this rental result += "\t" + each.getMovie().getTitle()+ "\t" + String.valueOf(each.getCharge()) + "\n"; totalAmount += each.getCharge(); } //add footer lines result += "Amount owed is " + String.valueOf(totalAmount) + "\n"; result += "You earned " + String.valueOf(frequentRenterPoints) + " frequent renter points"; return result; } }
提炼后代码 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 += each.getFrequentRenterPoints(); //show figures for this rental result += "\t" + each.getMovie().getTitle()+ "\t" + String.valueOf(each.getCharge()) + "\n"; totalAmount += each.getCharge(); } //add footer lines result += "Amount owed is " + String.valueOf(totalAmount) + "\n"; result += "You earned " + String.valueOf(frequentRenterPoints) + " frequent renter points"; return result; } class Rental... int getFrequentRenterPoints() { if ((getMovie().getPriceCode() == Movie.NEW_RELEASE) && getDaysRented() > 1) return 2; else return 1; }
结论 • 重构技术系以微小的步伐修改程序。如果你犯下错误,很容易便可发现它。 • 写出人类容易理解的代码,才是优秀的程序员
重构 • 重构:对软件内部结构的一种调整,目的是在不改变软件的可观察行为前提下,提高其可理解性,降低其修改成本 • 一般而言重构都是对软件的小改动,但重构可以包含另一个重构。例如Extract Class通常包含Move Method和Move Field
为何重构 • 重构可以改进软件设计。重构就像是在整理代码,让所有东西回到应该的位置上。 • 重构使软件更易被理解 • 重构可以帮助寻找bugs • 重构帮助提高编程速度。良好的设计是快速软件开发的根本,恶劣的设计会是开发速度下降,引起耗时的调试,增加新功能,维护等。
何时重构 • 三次法则(The Rule of Three) 第一次做某件事时只管去做;第二次做类似的事会产生反感,但无论如何还是做了;第三次再做类似的事,你就应该重构!
Design SmellsThe Odors of Rotting Software The software is rotting when it starts to exhibit any of the following odors • Rigidity • Fragility • Immobility • Viscosity • Needless complexity • Needless repetition • Opacity
Rigidity Rigidity is the tendency for software to be difficult to change, even in simple ways. A design is rigid if a single change causes a cascade of subsequent changes in dependent modules. The more modules that must be changed, the more rigid the design. Most developers have faced this situation in one way or another. They are asked to make what appears to be a simple change. They look the change over and make a reasonable estimate of the work required. But later, as they work through the change, they find that there are unanticipated repercussions to the change. The developers find themselves chasing the change through huge portions of the code, modifying far more modules than they had first estimated, and discovering thread after thread of other changes that they must remember to make. In the end, the changes take far longer than the initial estimate. When asked why their estimate was so poor, they repeat the traditional software developers lament: "It was a lot more complicated than I thought!"
Fragility Fragility is the tendency of a program to break in many places when a single change is made. Often, the new problems are in areas that have no conceptual relationship with the area that was changed. Fixing those problems leads to even more problems, and the development team begins to resemble a dog chasing its tail. As the fragility of a module increases, the likelihood that a change will introduce unexpected problems approaches certainty. This seems absurd, but such modules are not at all uncommon. These are the modules that are continually in need of repair, the ones that are never off the bug list. These modules are the ones that the developers know need to be redesigned, but nobody wants to face the spectre of redesigning them. These modules are the ones that get worse the more you fix them.
Immobility A design is immobile when it contains parts that could be useful in other systems, but the effort and risk involved with separating those parts from the original system are too great. This is an unfortunate but very common occurrence.
Viscosity Viscosity comes in two forms: viscosity of the software and viscosity of the environment. When faced with a change, developers usually find more than one way to make that change. Some of the ways preserve the design; others do not (i.e., they are hacks). When the design-preserving methods are more difficult to use than the hacks, the viscosity of the design is high. It is easy to do the wrong thing but difficult to do the right thing. We want to design our software such that the changes that preserve the design are easy to make. Viscosity of environment comes about when the development environment is slow and inefficient. For example, if compile times are very long, developers will be tempted to make changes that don't force large recompiles, even though those changes don't preserve the design. If the source code control system requires hours to check in just a few files, developers will be tempted to make changes that require as few check-ins as possible, regardless of whether the design is preserved. In both cases, a viscous project is one in which the design of the software is difficult to preserve. We want to create systems and project environments that make it easy to preserve and improve the design.
Needless Complexity A design smells of needless complexity when it contains elements that aren't currently useful. This frequently happens when developers anticipate changes to the requirements and put facilities in the software to deal with those potential changes. At first, this may seem like a good thing to do. After all, preparing for future changes should keep our code flexible and prevent nightmarish changes later. Unfortunately, the effect is often just the opposite. By preparing for many contingencies, the design becomes littered with constructs that are never used. Some of those preparations may pay off, but many more do not. Meanwhile, the design carries the weight of these unused design elements. This makes the software complex and difficult to understand.
Needless Repetition Cut and paste may be useful text-editing operations, but they can be disastrous code-editing operations. All too often, software systems are built on dozens or hundreds of repeated code elements. It happens like this: Ralph needs to write some code that fravles the arvadent.[2] He looks around in other parts of the code where he suspects other arvadent fravling has occurred and finds a suitable stretch of code. He cuts and pastes that code into his module and makes the suitable modifications. [2] For those of you who do not have English as your first language, the term fravle the arvadent is composed of nonsense words and is meant to imply some nondescript programming activity. Unbeknownst to Ralph, the code he scraped up with his mouse was put there by Todd, who scraped it out of a module written by Lilly. Lilly was the first to fravle an arvadent, but she realized that fravling an arvadent was very similar to fravling a garnatosh. She found some code somewhere that fravled a garnatosh, cut and paste it into her module, and modified it as necessary. When the same code appears over and over again, in slightly different forms, the developers are missing an abstraction. Finding all the repetition and eliminating it with an appropriate abstraction may not be high on their priority list, but it would go a long way toward making the system easier to understand and maintain. When there is redundant code in the system, the job of changing the system can become arduous. Bugs found in such a repeating unit have to be fixed in every repetition. However, since each repetition is slightly different from every other, the fix is not always the same.
Opacity • Opacity is the tendency of a module to be difficult to understand. Code can be written in a clear and • expressive manner, or it can be written in an opaque and convoluted manner. Code that evolves over • time tends to become more and more opaque with age. A constant effort to keep the code clear and • expressive is required in order to keep opacity to a minimum. • When developers first write a module, the code may seem clear to them. After all, they have • immersed themselves in it and understand it at an intimate level. Later, after the intimacy has worn • off, they may return to that module and wonder how they could have written anything so awful. To • prevent this, developers need to put themselves in their readers' shoes and make a concerted effort • to refactor their code so that their readers can understand it. They also need to have their code • reviewed by others.
Why Software Rots In nonagile environments, designs degrade because requirements change in ways that the initial design did not anticipate. Often, these changes need to be made quickly and may be made by developers who are not familiar with the original design philosophy. So, though the change to the design works, it somehow violates the original design. Bit by bit, as the changes continue, these violations accumulate until malignancy sets in. However, we cannot blame the drifting of the requirements for the degradation of the design. We, as software developers, know full well that requirements change. Indeed, most of us realize that the requirements are the most volatile elements in the project. If our designs are failing owing to the constant rain of changing requirements, it is our designs and practices that are at fault. We must somehow find a way to make our designs resilient to such changes and use practices that protect them from rotting. An agile team thrives on change. The team invests little up front and so is not vested in an aging initial design. Rather, the team keeps the design of the system as clean and simple as possible and backs it up with lots of unit tests and acceptance tests. This keeps the design flexible and easy to change. The team takes advantage of that flexibility in order to continuously improve the design; thus, each iteration ends with a system whose design is as appropriate as it can be for the requirements in that iteration.
There are three modules, or subprograms, in the application. The Copy module calls the other two. The Copy program fetches characters from the Read Keyboard module and routes them to the Write Printer module. • public class Copier • { • public static void Copy() • { • int c; • while((c=Keyboard.Read()) != -1) • Printer.Write(c); • } • }
First modification of Copy program • public class Copier • { • //remember to reset this flag • public static bool ptFlag = false; • public static void Copy() • { • int c; • while((c=(ptFlag ? PaperTape.Read() • : Keyboard.Read())) != -1) • Printer.Write(c); • } • }
Second modification of Copy program • public class Copier • { • //remember to reset these flags • public static bool ptFlag = false; • public static bool punchFlag = false; • public static void Copy() • { • int c; • while((c=(ptFlag ? PaperTape.Read() • : Keyboard.Read())) != -1) • punchFlag ? PaperTape.Punch(c) : Printer.Write(c); • } • }
Agile version 2 of Copy • public interface Reader • { • int Read(); • } • public class KeyboardReader : Reader • { • public int Read() {return Keyboard.Read();} • } • public class Copier • { • public static Reader reader = new KeyboardReader(); • public static void Copy() • { • int c; • while((c=(reader.Read())) != -1) • Printer.Write(c); • } • }
单一职责原则 (SRP) • 就一个类而言,应该仅有一个引起它变化的原因。
Defining a Responsibility • public interface Modem • { • public void Dial(string pno); • public void Hangup(); • public void Send(char c); • public char Recv(); • }