回 帖 发 新 帖 刷新版面

主题:从Java的角度理解设计模式(连载)


从Java的角度理解设计模式1:什么是重构 


MF在《重构》一书中是这样定义重构的:重构是这样一个过程,在不改变代码外在行为的前提下,对代码作出修改,以改进程序的内部结构。重构是一种有纪律的、经过训练的、有条不紊的程序整理方法,可以将整理过程中不小心引入错误的机率降到最低。本质上说,重构就是在代码写好之后改进它的设计。 
通常,软件开发总是先设计后编码,然而难免地,程序员编码风格的随意性、系统庞大化等诸多因素,导致代码质量将变得愈发难以控制。重构使得即使在面对糟糕设计,甚至是代码一片混乱的情况下,也能逐步改良代码的设计。大多数重构的步骤非常简单,比如为一个方法添加一个参数(Refactoring:Add Parameter)、(在类间)搬动一个方法(Refactoring :Move Method)、在类阶层中上下迁移代码:如值域(Refactoring :Push Up/Down Field)。然而正是这些微小的步骤,保障了代码朝着优良的结构持续演变,或者说不会日益“腐烂”。 
此外,重构的工作方法也使得所谓的精心设计不再如此举足轻重了,因为设计可以在持续重构中得到强化。 
如下展示了一个非常简单的重构,它采用了(Refactoring:Add Parameter): 
private void mainTestBody() { 
ApplicationContext context = 
new ClassPathXmlApplicationContext("ch6/prototype/setter-injection.xml"); 
BookList booklist = (BookList)context.getBean("bookList"); 
Book[] books = booklist.findBooksWrittenBy("天下霸唱"); 
assertEquals("鬼吹灯",books[0].getName()); 





private void mainTestBody(String configMetadata) { 
ApplicationContext context = 
new ClassPathXmlApplicationContext(configMetadata); 
BookList booklist = (BookList)context.getBean("bookList"); 
Book[] books = booklist.findBooksWrittenBy("天下霸唱"); 
assertEquals("鬼吹灯",books[0].getName()); 

本书对于重构剖析的侧重点有两个:一个是如何将重构应用于Java模式和框架设计的编程中;一个则是如何利用自动化重构工具对代码实施重构(当然并不是所有的重构技巧都得到工具支持)。 
本书对重构提供的阅读方法是:本章将会列出所有本书中用到的重构技巧名录(按字母顺序排列),而在后续各个章节中,对这些重构技巧的应用时机和方法会有更贴近实际的讲解;关于自动化重构工具的使用仅在本章列出,在后续章节中如果希望得到自动化的重构指导(对于每个重构技巧,如果自动化重构工具支持的话,都会有相应的指导),可参考本章。

回复列表 (共5个回复)

沙发

从Java的角度理解设计模式2:自动化重构工具的使用
重构的理论很具有吸引力,然而纯手工的重构需要检查的东西相当多,是一项耗时、易错的工作。也因为这个原因,很多程序员觉得重构的成本太大而不愿意进行重构。自动化重构工具的出现,大大改善了这个局面。现代化IDE融入重构功能后,使得编程和重构之间的差异也日益缩小(程序员几乎不再需要分辩“正在编程”还是“正在重构”),许多自动化的重构甚至基本都无须测试,重构成本也因此大大降低。 
Java语言语法简洁清晰,是非常便于重构的一门语言,本书介绍的自动化重构工具便是一款成熟的Java IDE:Eclipse(使用的版本是3.3),读者可在http://www.eclipse.org/downloads/ 下载到Eclipse的最新版本。

板凳


从Java的角度理解设计模式3:自动提炼方法
下面将简单展示两个重构技巧“提炼方法(Refactoring:Extract Method)”和“搬移方法(Refactoring:Move Method)”如何通过Eclipse自动实施。给出原始代码,如下所示。 
HelloBean.java 
package ch2.extractandmovemethod.prototype; 

public class HelloBean { 
public void sayHello() { 
System.out.println("朋友,时刻重构吧!"); 


RefactoringIDETest.java 
package ch2.extractandmovemethod.prototype; 

public class RefactoringIDETest { 
public static void main(String[] args) throws Exception { 
String className = "ch2.refactoringusingide.HelloBean"; 
Object beanInstance = null; 
try { 
Class clazz = Class.forName(className); 
beanInstance = clazz.newInstance(); 
} catch (Exception ex) { 
throw ex; 

((HelloBean)beanInstance).sayHello(); 


注意上述代码的框选部分,显然这部分代码可以被提炼出来形成一个公用方法。 
说明:如果使用重构工具,只需简单地圈选出要提炼的段落,然后点选菜单选项“Extract Method”就行了。重构工具会自动检查被圈选的代码段是否可以提炼。代码无法提炼的原因有很多,可能是:它可能只包含部分标识符声明;可能对某个变量赋值而该变量又被其它代码用到。所有这些情况,程序员都完全不必担心,因为重构工具会自动识别并处理一切。随后,工具会自动计算出新方法所需的参数,并要求程序员为新方法取一个名字。此时,程序员可自行决定新方法参数的命名、排列顺序等。所有准备工作都完成后,重构工具会把圈选的代码从源方法中提炼出来,并在源方法中加入对新方法的调用。随后它会再源方法所属的类中建立新的方法,并以程序员指定的名称来命名该方法。整个重构过程耗时极短,且不会出错,相比手工进行这个重构,方便、安全了许多,体现了自动化重构工具的威力。 
现在用Eclipse来实现上述步骤,首先选中上述代码的框选部分,点击鼠标右键,找到重构菜单“Refactor”,点选“Extract Method” (注意,重构菜单会自动识别出当前圈选的代码段有哪些重构技巧可用),如图1所示。 
图1 自动化重构过程第一步 
在随后出现的对话框中,为提炼出的新方法命名(命名为“instantiateClass”),并决定其它的方法签名(注意,为了后续可以不用修改地将此方法搬移到另一个工具类中,请选择“public”的方法访问权限),如图2所示。 
图2 自动化重构过程第二步 
点选“Preview”可预览重构所牵动的资源和结果,点选“OK”将直接进行重构,点选“Cancel”将放弃重构,当然即使在重构完成后,Eclipse也提供了强大的重构回复功能,可将所有牵动的代码回复到重构前的状态,甚至还有重构的历史回复功能。现在,点击“OK”,得到应用了(Refactoring:Extract Method)后的代码,如下所示。 
RefactoringIDETest.java 
package ch2.extractandmovemethod; 

import ch2.extractandmovemethod.prototype.HelloBean; 

public class RefactoringIDETest { 
public static void main(String[] args) throws Exception { 
String className = "ch2.refactoringusingide.HelloBean"; 
Object beanInstance = instantiateClass(className); 
((HelloBean)beanInstance).sayHello(); 


public static Object instantiateClass(String className) throws Exception { 
Object beanInstance = null; 
try { 
Class clazz = Class.forName(className); 
beanInstance = clazz.newInstance(); 
} catch (Exception ex) { 
throw ex; 

return beanInstance; 





融智技术学苑(http://www.rzchina.net)版权所有,本公司致力于提供更好更实用的Java培训课程,帮助学员迅速成为编程的行家里手,更多Java面试题\Java视频\Java教程请访问我们的网站.转载请保留这段文字。

3 楼

从Java的角度理解设计模式4:自动搬移方法
接着,需要通过(Refactoring:Move Method)将这个方法搬移至一个工具类中,为此,简单的拟一个抽象工具类BeanUtils,如下所示。 

BeanUtils.java 

package ch2.utils; 

public abstract class BeanUtils { 


回到上文的RefactoringIDETest类,在IDE中双击选中instantiateClass()方法名,点击鼠标右键,在重构菜单中,点选“Move”(IDE会自动识别选定的资源,比如本例选中的是方法,那么采用的重构技巧就是(Refactoring:Move Method)),如图3所示。 

图3 自动化重构过程第三步 
在随后的对话框中,找到刚才新建的BeanUtils类,点击“OK”,如图4所示。 

图4 自动化重构过程第四步 
在实施了所有自动化重构后,给出所有更动过的代码,如代码~2.2所示。 

代码 BeanUtils.java 

package ch2.utils; 

public abstract class BeanUtils { 

public static Object instantiateClass(String className) throws Exception { 
Object beanInstance = null; 
try { 
Class clazz = Class.forName(className); 
beanInstance = clazz.newInstance(); 
} catch (Exception ex) { 
throw ex; 

return beanInstance; 




代码 RefactoringIDETest.java 

package ch2.extractandmovemethod; 

import ch2.extractandmovemethod.prototype.HelloBean; 
import ch2.utils.BeanUtils; 

public class RefactoringIDETest { 
public static void main(String[] args) throws Exception { 
String className = "ch2.refactoringusingide.HelloBean"; 
Object beanInstance = BeanUtils.instantiateClass(className); 
((HelloBean)beanInstance).sayHello(); 


上面演示了自动化重构工具(Eclipse3.3)的基本使用方法,下文将介绍更多的重构技巧(自动/非自动) 



融智技术学苑(http://www.rzchina.net )版权所有,本公司致力于提供更好更实用的Java培训课程,帮助学员迅速成为编程的行家里手,更多Java面试题\Java视频\Java教程请访问我们的网站.转载请保留这段文字。

4 楼

从Java的角度理解设计模式5:自动改变方法签名
(Refactoring:Change Method Signature)其实是Eclipse组合了《重构》一书中的几个重构技巧而成,它们分别是Add/Remove Parameter(增加/移除 方法参数)、Rename Method(重命名方法)。 
1.动机 
方法签名包含方法名、参数列(类型和次序)、方法返回类型、方法访问权限等,在编程的时候,改变方法签名有各种动机,比如当发现方法的名称未能明确揭示方法用途时,则需要改变方法名称(Refactoring:Rename Method);当发现某个方法需要从调用端得到更多信息时,则需要为方法添加参数(Refactoring:Add Parameter,见2.1节示例);当发现方法本体不再需要某个参数时,就需要移除参数(Refactoring:Remove Parameter),冗余的参数往往会给客户带来负担。 
2.作法 
以上这些重构技巧,在Eclipse中都可以通过重构菜单“Change Method Signature”来一并完成。给出原始的类,如下所示。 

BeanFactory.java 

package ch2.changemethodsignature.prototype; 

import ch2.utils.BeanUtils; 

public class BeanFactory { 
public Book getBook() throws Exception { 
return (Book)BeanUtils.instantiateClass( 
"ch2.changemethodsignature.prototype.Book"); 


显然getBook()作为BeanFactory的主力工厂方法它不够通用,因此需要适当改变它的方法签名和实现。现在用Eclipse来实现上述步骤,首先双击选中getBook()方法,点击鼠标右键,找到重构菜单“Refactor”,点选“Change Method Signature”,在随后出现的对话框中,为getBook()方法重命名(命名为“getBean”);调整它的返回值为“Object”、访问权限为“public”;增加两个String型的参数“packageName”和“className”,如图5所示。 

图5 自动改变方法签名 
去掉方法内的强制转型后,得到初步重构后的代码,如下所示。 

BeanFactory.java 

public class BeanFactory { 
public Object getBean(String packageName, String className) throws Exception { 
return BeanUtils.instantiateClass(packageName + className); 


发现方法参数冗余,使用一个Bean的标识符就足够了。迅速作出再次重构,通过Eclipse的“Change Method Signature”菜单去掉一个参数(使用图5中的Remove按钮),并改变参数名称,得到最后的代码,如代码所示。 

代码 BeanFactory.java 

package ch2.changemethodsignature; 

import ch2.utils.BeanUtils; 

public class BeanFactory { 
public Object getBean(String beanIdentifier) throws Exception { 
return BeanUtils.instantiateClass(beanIdentifier); 






融智技术学苑(http://www.rzchina.net )版权所有,本公司致力于提供更好更实用的Java培训课程,帮助学员迅速成为编程的行家里手,更多Java面试题\Java视频\Java教程请访问我们的网站.转载请保留这段文字。

5 楼

从Java的角度理解设计模式6:封装集合
(Refactoring:Encapsulate Collection)是指当发现某个方法返回类型是集合(Collection)的时候,使该方法仅返回一个该集合的只读视图(read-only view),并在该class内提供添加/移除(add/remove)该集合元素的方法。如图6所示。 

图6 封装集合示意图 
1.动机 
一个类如果持有一个集合,那么它往往需要提供出对该集合的取值器或者设值器(getter/setter)。然而有时,取值器直接返回集合(引用)是不合适的,因为用户可以通过该引用直接修改集合内容。此外,也不应该为整个集合提供一个设值器,一个更好的方法是给出用以为集合添加/移除(add/remove)元素的方法。做到以上两点,这就加强了类对集合的封装并降低了用户和类(集合持有者)的耦合。 
2.作法 
给出原始的类,假设它是对一个购物车的抽象,称其为Cart,而Cart内部又持有一个商品集合,如下所示。 

Cart.java 

package ch2.encapsulatecollection.prototype; 

import java.util.ArrayList; 
import java.util.List; 

public class Cart { 

private List products = new ArrayList(); 

public List getProducts() { 
return products; 


public void setProducts(List products) { 
this.products = products; 


给出商品抽象,称其为Product,注意其中实现了equals()方法,它用以判断两个Product对象是否相等(判断依据主要是Product的name),如代码所示。 

代码 Product.java 

package ch2.encapsulatecollection.prototype; 

public class Product { 

private String name; 
private int price; 

public Product(String name, int price) { 
this.name = name; 
this.price = price; 


public String getName() { 
return name; 


public int getPrice() { 
return price; 


public boolean equals(Object obj) { 
if (this == obj) return true; 
if (obj != null && (obj.getClass() == getClass())) { 
if (((Product)obj).getName().equals(getName())) { 
return true; 


return false; 


(Refactoring:Encapsulate Collection)在Eclipse中并未提供自动支持,所以需要手工进行。大致步骤如下: 
(1)使Cart内部持有的集合只读化,如下所示。 

Cart.java 

public class Cart { 
... 
public List getProducts() { 
return Collections.unmodifiableList(products); 

... 

(2)向Cart增加/移除商品的方法,增加/移除商品的方法返回类型设为Cart是因为便于编程,这样就可在单行语句中连续刷新商品集合,如下所示。 

Cart.java 

public class Cart { 
... 
public Cart addProduct(Product product) { 
for (int i = 0; i < this.products.size(); i++) { 
Product currentProduct = (Product) this.products.get(i); 
if (currentProduct.equals(product)) { 
setProductAt(product, i); 
return this; 


products.add(product); 
return this; 


public Cart removeProduct(Product product) { 
products.remove(product); 
return this; 


private void setProductAt(Product product, int i) { 
products.set(i, product); 

... 

(3)重构Cart的setProducts()方法。 
这里读者可能会产生两个疑问:既然有了商品的增加/移除方法,为何不简单移除setProducts()方法?如果保留setProducts()方法,为何要重构它,它有什么问题?以下稍微解答一下,去掉setProducts()方法是可以的,然而当重构前的代码中存在大量对setProducts()方法的调用时,在每一处作相应改变是繁琐的,因此一个折中的方案是重构setProducts()方法;那么原来的setProducts()方法存在什么问题呢,请看如下测试代码。 

代码 CartTest.java 

package ch2.encapsulatecollection; 

import java.util.ArrayList; 
import java.util.List; 

import ch2.encapsulatecollection.prototype.Product; 
import junit.framework.TestCase; 

public class CartTest extends TestCase { 

public void testSetProducts() { 
Cart cart = new Cart(); 
List products = new ArrayList(); 
cart.setProducts(products);① 
int size1 = cart.getProducts().size(); 
Product p = new Product("aa", 1); 
products.add(p); ② 
int size2 = cart.getProducts().size(); 

assertEquals(size1, size2);③ 



如上所示,在①处向Cart初始化了一个空集合后,在②处对原有集合的变更理应不会波及到Cart,然而③处的测试却失败了。这就说明了原有的setProducts()方法有问题,它破坏了集合的封装,怎样修改呢?迅速对Cart的setProducts()方法作出改变,使其调用内部的addProduct()方法,并在之前清空了原有的集合,如下所示。 
public class Cart { 
... 
public void setProducts(List products) { 
this.products = new ArrayList(); 
for (Iterator iterator = products.iterator(); iterator.hasNext();) { 
addProduct((Product)iterator.next()); 


... 

观察上述代码后发现,其实setProducts()只是逐一向一个空集合添加元素,它执行的是初始化功能,因此应该使用(Refactoring:Rename Method)来明示方法的用途,并且可以提供一个更简单、高效的实现,如下所示: 
public class Cart { 
... 
public void initializeProducts(List products) { 
this.products = new ArrayList(); 
this.products.addAll(products); 

... 

(4)修改牵连代码,比如原先通过getProducts()方法来改变Cart内部集合的代码需要改用addProduct()方法。 
完成所有重构后,给出Cart的完整实现,如代码所示。 

代码 Cart.java 

package ch2.encapsulatecollection; 

import java.util.ArrayList; 
import java.util.Collections; 
import java.util.List; 

import ch2.encapsulatecollection.prototype.Product; 

public class Cart { 

private List products = new ArrayList(); 

public void initializeProducts(List products) { 
this.products = new ArrayList(); 
this.products.addAll(products); 


public List getProducts() { 
return Collections.unmodifiableList(products); 


public Cart addProduct(Product product) { 
for (int i = 0; i < this.products.size(); i++) { 
Product currentProduct = (Product) this.products.get(i); 
if (currentProduct.equals(product)) { 
setProductAt(product, i); 
return this; 


products.add(product); 
return this; 


public Cart removeProduct(Product product) { 
products.remove(product); 
return this; 


private void setProductAt(Product product, int i) { 
products.set(i, product); 






融智技术学苑(http://www.rzchina.net )版权所有,本公司致力于提供更好更实用的Java培训课程,帮助学员迅速成为编程的行家里手,更多Java面试题\Java视频\Java教程请访问我们的网站.转载请保留这段文字。

我来回复

您尚未登录,请登录后再回复。点此登录或注册