2010-06-24 13:36:56
摘要:重构概述
重构的目的:为什么重构(why)?
对于项目来言,重构可以保持代码质量持续处于一个可控状态,不至于腐化到无可救药的地步。对于个人而言,重构非常锻炼一个人的代码能力,并且是一件非常有成就感的事情。它是我们学习的经典设计思想、原则、模式、编程规范等理论知识的练兵场。
重构的对象:重构什么(what)?
按照重构的规模,我们可以将重构大致分为大规模高层次的重构和小规模低层次的重构。大规模高层次重构包括对代码分层、模块化、解耦、梳理类之间的交互关系、抽象复用组件等等。这部分工作利用的更多的是比较抽象、比较顶层的设计思想、原则、模式。小规模低层次的重构包括规范命名、注释、修正函数参数过多、消除超大类、提取重复代码等等编程细节问题,主要是针对类、函数级别的重构。小规模低层次的重构更多的是利用编码规范这一理论知识。
重构的时机:什么时候重构(when)?
我们一定要建立持续重构意识,把重构作为开发必不可少的部分,融入到日常开发中,而不是等到代码出现很大问题的时候,再大刀阔斧地重构。
重构的方法:如何重构(how)?
大规模高层次的重构难度比较大,需要组织、有计划地进行,分阶段地小步快跑,时刻让代码处于一个可运行的状态。而小规模低层次的重构,因为影响范围小,改动耗时短,所以,只要你愿意并且有时间,随时随地都可以去做。
单元测试
什么是单元测试?
单元测试是代码层面的测试,由研发自己来编写,用于测试“自己”编写的代码的逻辑的正确性。单元测试顾名思义是测试一个“单元”,有别于集成测试,这个“单元”一般是类或函数,而不是模块或者系统。
为什么要写单元测试?
写单元测试的过程本身就是代码 Code Review 和重构的过程,能有效地发现代码中的 bug 和代码设计上的问题。除此之外,单元测试还是对集成测试的有力补充,还能帮助我们快速熟悉代码,是 TDD 可落地执行的改进方案。
如何编写单元测试?
写单元测试就是针对代码设计各种测试用例,以覆盖各种输入、异常、边界情况,并将其翻译成代码。我们可以利用一些测试框架来简化单元测试的编写。除此之外,对于单元测试,我们需要建立以下正确的认知:
编写单元测试尽管繁琐,但并不是太耗时;
我们可以稍微放低对单元测试代码质量的要求;
覆盖率作为衡量单元测试质量的唯一标准是不合理的;
单元测试不要依赖被测代码的具体实现逻辑;
单元测试框……
阅读全文
2010-06-22 14:45:21
摘要:说在前面
就一个类而言,应该仅有一个引起变化的原因。我们在编程的时候,很自然地就会给一个类加各种各样的功能,比如我们编写一个窗体应用程序,一般都会生成一个Form1这样的类,于是我们就把各种各样的代码,像某种商业运算的算法、数据库访问的sql语句都写到这样的类当中,这就意味着,无论任何需求变化,都需要更改这个窗体类,这其实是很糟糕的,维护麻烦,复用不可能,也缺乏灵活性。
如果一个类承担的责任过多,就等于把这些职责耦合在一起,一个职责的变化可能会削弱或者抑制这个类完成其他职责的能力。这种耦合会导致脆弱的设计。当变化发生时,设计会遭受到意想不到的破坏。
软件设计真正要做的许多内容,就是发现职责并把职责相互分离。其实要去判断是否应该分离出类来,也不难,那就是如果你能够想到多于一个的动机去改变一个类,那么这个类就具有多于一个的职责。
经典的设计原则,其中包括,SOLID、KISS、YAGNI、DRY、LOD 等。SOLID 原则,实际上,SOLID 原则并非单纯的 1 个原则,而是由 5 个设计原则组成的,它们分别是:单一职责原则、开闭原则、里式替换原则、接口隔离原则和依赖反转原则,依次对应 SOLID 中的 S、O、L、I、D 这 5 个英文字母。
单一职责原则(SRP)
如何理解单一职责原则(SRP)?
一个类只负责完成一个职责或者功能。不要设计大而全的类,要设计粒度小、功能单一的类。单一职责原则是为了实现代码高内聚、低耦合,提高代码的复用性、可读性、可维护性。
如何判断类的职责是否足够单一?
不同的应用场景、不同阶段的需求背景、不同的业务层面,对同一个类的职责是否单一,可能会有不同的判定结果。实际上,一些侧面的判断指标更具有指导意义和可执行性,比如,出现下面这些情况就有可能说明这类的设计不满足单一职责原则:类中的代码行数、函数或者属性过多;类依赖的其他类过多,或者依赖类的其他类过多;私有方法过多;比较难给类起一个合适的名字;类中大量的方法都是集中操作类中的某几个属性。
类的职责是否设计得越单一越好?
单一职责原则通过避免设计大而全的类,避免将不相关的功能耦合在一起,来提高类的内聚性。同时,类职责单一,类依赖的和被依赖的其他类也会变少,减少了代码的耦合性,以此来实现代码的高内聚、低耦合。但是,如果拆分得过细,实际上会适得其反,反倒会降低内聚性,也会影响代码的可维护……
阅读全文
2010-06-20 23:20:39
摘要:面向对象概述
什么是面向对象编程?
面向对象编程是一种编程范式或编程风格。它以类或对象作为组织代码的基本单元,并将封装、抽象、继承、多态四个特性,作为代码设计和实现的基石 。
什么是面向对象编程语言?
面向对象编程语言是支持类或对象的语法机制,并有现成的语法机制,能方便地实现面向对象编程四大特性(封装、抽象、继承、多态)的编程语言。
如何判定一个编程语言是否是面向对象编程语言?
如果按照严格的的定义,需要有现成的语法支持类、对象、四大特性才能叫作面向对象编程语言。如果放宽要求的话,只要某种编程语言支持类、对象语法机制,那基本上就可以说这种编程语言是面向对象编程语言了,不一定非得要求具有所有的四大特性。
面向对象编程和面向对象编程语言之间有何关系?
面向对象编程一般使用面向对象编程语言来进行,但是,不用面向对象编程语言,我们照样可以进行面向对象编程。反过来讲,即便我们使用面向对象编程语言,写出来的代码也不一定是面向对象编程风格的,也有可能是面向过程编程风格的。
什么是面向对象分析和面向对象设计?
简单点讲,面向对象分析就是要搞清楚做什么,面向对象设计就是要搞清楚怎么做。两个阶段最终的产出是类的设计,包括程序被拆解为哪些类,每个类有哪些属性方法、类与类之间如何交互等等。
面向对象四大特性
关于封装特性
封装也叫作信息隐藏或者数据访问保护。类通过暴露有限的访问接口,授权外部仅能通过类提供的方式来访问内部信息或者数据。它需要编程语言提供权限访问控制语法来支持,例如 C# 中的 private、protected、public 关键字。封装特性存在的意义,一方面是保护数据不被随意修改,提高代码的可维护性;另一方面是仅暴露有限的必要接口,提高类的易用性。
关于抽象特性
封装主要讲如何隐藏信息、保护数据,那抽象就是讲如何隐藏方法的具体实现,让使用者只需要关心方法提供了哪些功能,不需要知道这些功能是如何实现的。抽象可以通过接口类或者抽象类来实现,但也并不需要特殊的语法机制来支持。抽象存在的意义,一方面是提高代码的可扩展性、维护性,修改实现不需要改变定义,减少代码的改动范围;另一方面,它也是处理复杂系统的有效手段,能有效地过滤掉不必要关注的信息。
关于继承特性
继承是用来表示类之间的 is-a 关系,分为两种模式:单继承和多继承。单继承表示一个子类只继承一个父类,多继承表示……
阅读全文
2010-06-19 13:09:05
摘要:如何评价代码质量的高低?
代码质量的评价有很强的主观性,描述代码质量的词汇也有很多,比如可读性、可维护性、灵活、优雅、简洁。这些词汇是从不同的维度去评价代码质量的。它们之间有互相作用,并不是独立的,比如,代码的可读性好、可扩展性好就意味着代码的可维护性好。代码质量高低是一个综合各种因素得到的结论。我们并不能通过单一维度去评价一段代码的好坏。
最常用的评价标准有哪几个?
最常用到几个评判代码质量的标准有:可维护性、可读性、可扩展性、灵活性、简洁性、可复用性、可测试性。其中,可维护性、可读性、可扩展性又是提到最多的、最重要的三个评价标准。
如何才能写出高质量的代码?
要写出高质量代码,我们就需要掌握一些更加细化、更加能落地的编程方法论,这就包含面向对象设计思想、设计原则、设计模式、编码规范、重构技巧等。
阅读全文
2009-10-31 13:53:48
摘要:一天一个重构方法(1):Extract Method
一天一个重构方法(2):Inline Method
一天一个重构方法(3):Switch to Strategy
一天一个重构方法(4):Break Dependencies
一天一个重构方法(5):Introduce Explaining Variable
一天一个重构方法(6):Extract Method Object
一天一个重构方法(7):Break Responsibilities
一天一个重构方法(8):Encapsulate Conditional
一天一个重构方法(9):Substitute Algorithm
一天一个重构方法(10):Rename Method
一天一个重构方法(11):Add Parameter
一天一个重构方法(12):Remove Parameter
一天一个重构方法(13):Remove Arrowhead Antipattern
一天一个重构方法(14):Parameterize Method
一天一个重构方法(15):Replace Parameter with Explicit Methods
一天一个重构方法(16):Remove Double Negative
一天一个重构方法(17):Return ASAP
一天一个重构方法(18):Introduce Parameter Object
一天一个重构方法(19):Replace Error Code with Exception
一天一个重构方法(20):Replace Exception with Test
一天一个重构方法(21):Replace Constructor with Factory Method
一天一个重构方法(22):Encapsulate Downcast
一天一个重构方法(23):Introduce Design By Contract
一天一个重构方法(24):Extract class
一天一个重构方法(25):Pull Up Field
一天一个重构方法(26):Pull Up Mothod
一天一个重构方法(27):Push Down Field
一天一个重构方法(28):Push Down Method
一天一个重构方法(29):Remove God Classes
……
阅读全文
2009-10-17 17:06:14
摘要:Replace conditional with Polymorphism:以多态取代条件式
多态(Polymorphism)是面向对象编程的基本概念之一。在这里,是指在进行类型检查和执行某些类型操作时,最好将算法封装在类中,并且使用多态来对代码中的调用进行抽象。
public abstract class Customer
{
}
public class Employee : Customer
{
}
public class NonEmployee : Customer
{
}
public class OrderProcessor
{
public decimal ProcessOrder(Customer customer, IEnumerableProduct products)
{
// do some processing of order
decimal orderTotal = products.Sum(p = p.Price);
Type customerType = customer.GetType();
if (customerType == typeof(Employee))
{
orderTotal -= orderTotal * 0.15m;
}
else if (customerType == typeof(NonEmployee))
{
orderTotal -= orderTotal * 0.05m;
}
return orderTotal;
}
}
如你所见,我们没有利用已有的继承层次进行计算,而是使用了违反SRP 原则的执行方式。要进行重构,我们只需将百分率的计算置于实际的customer类型之中。我知道这只是一项补救措施,但我还是会这么做,就像在代码中那样。
public abstract class Customer
{
public abstract decimal DiscountPercentage { get; }
}
public class Employee : Customer
{
public override decimal DiscountPercentage
{
get { return 0.15m; }
}
}
publi……
阅读全文
2009-10-11 16:18:28
摘要:Replace Inheritance with Delegation:以委托取代继承
某个子类只使用父类接口中的一部分,或者根本不需要继承而来的数据。在子类中新建一个字段用以保存父类;调整子类方法,令它改成委托父类;然后去掉两者之间的继承关系。
public class Sanitation
{
public string WashHands()
{
return Cleaned!;
}
}
public class Child : Sanitation
{
}
在该例中,Child 并不是Sanitation,因此这样的继承层次是毫无意义的。我们可以这样重构:在Child 的构造函数里实现一个Sanitation实例,并将方法的调用委托给这个实例。如果你使用依赖注入,可以通过构造函数传递Sanitation实例,尽管在我看来还要向IoC容器注册模型是一种坏味道,但领会精神就可以了。继承只能用于严格的继承场景,并不是用来快速编写代码的工具。
public class Sanitation
{
public string WashHands()
{
return Cleaned!;
}
}
public class Child
{
private Sanitation Sanitation { get; set; }
public Child()
{
Sanitation = new Sanitation();
}
public string WashHands()
{
return Sanitation.WashHands();
}
}
阅读全文
2009-10-03 14:30:49
摘要:Collapse Hierarchy:折叠继承体系
所谓重构继承体系,往往是将方法和字段在体系中上下移动。完成这些动作后,你很可能发现某个子类并未带来该有的价值,因此需要把两个类合并。
public class Website
{
public string Title { get; set; }
public string Description { get; set; }
public IEnumerableWebpage Pages { get; set; }
}
public class StudentWebsite : Website
{
public bool IsActive { get; set; }
}
这里的子类并没有过多的功能,只是表示站点是否激活。这时我们会意识到判断站点是否激活的功能应该是通用的。因此可以将子类的功能放回到Website 中,并删除StudentWebsite 类型。
public class Website
{
public string Title { get; set; }
public string Description { get; set; }
public IEnumerableWebpage Pages { get; set; }
public bool IsActive { get; set; }
}
阅读全文
2009-09-30 18:54:22
摘要:Encapsulate Collection:封装群集
有个方法返回一个群集。让这个方法返回该群集的一个只读映件,并在这个类中提供添加/删除(add/remove)群集元素的方法。因此,以可迭代但不直接在集合上进行操作的方式来暴露集合,是个不错的主意。我们来看代码:
public class Order
{
private int _orderTotal;
private ListOrderLine _orderLines;
public IEnumerableOrderLine OrderLines
{
get { return _orderLines; }
}
public void AddOrderLine(OrderLine orderLine)
{
_orderTotal += orderLine.Total;
_orderLines.Add(orderLine);
}
public void RemoveOrderLine(OrderLine orderLine)
{
orderLine = _orderLines.Find(o = o == orderLine);
if (orderLine == null) return;
_orderTotal -= orderLine.Total;
_orderLines.Remove(orderLine);
}
}
如你所见,我们对集合进行了封装,没有将Add/Remove 方法暴露给类的使用者。在.NET Framework中,有些类如ReadOnlyCollection,会由于封装集合而产生不同的行为,但它们各自都有防止误解的说明。这是一个非常简单但却极具价值的重构,可以确保用户不会误用你暴露的集合,避免代码中的一些bug。
阅读全文
2009-09-25 11:58:55
摘要:Remove Middle Man:删除中间人
有时你的代码里可能会存在一些“Phantom”或“Ghost”类,Fowler 称之为“中间人(Middle Man)”。这些中间人类仅仅简单地将调用委托给其他组件,除此之外没有任何功能。就应该删除这个类,让客户直接调用受托类。
public class Consumer
{
public AccountManager AccountManager { get; set; }
public Consumer(AccountManager accountManager)
{
AccountManager = accountManager;
}
public void Get(int id)
{
Account account = AccountManager.GetAccount(id);
}
}
public class AccountManager
{
public AccountDataProvider DataProvider { get; set; }
public AccountManager(AccountDataProvider dataProvider)
{
DataProvider = dataProvider;
}
public Account GetAccount(int id)
{
return DataProvider.GetAccount(id);
}
}
public class AccountDataProvider
{
public Account GetAccount(int id)
{
// get account
}
}
最终结果已经足够简单了。我们只需要移除中间人对象,将原始调用指向实际的接收者。
public class Consumer
{
public AccountDataProvider AccountDataProvider { get; set; }
public Consumer(AccountDataProvider dataProvider)
{
AccountDataProvider = dataProvider;
}
public void Get(int id)
{
Acc……
阅读全文