在面向对象编程过程中,明确该对象的职责。类应该是:做自己该做的事,应尽该尽的义务:
主要内容:
1、类函数:移动函数
2、类成员字段:迁移字段
3、类分解和合并:大类提炼小类,小类合并到大类里面。
4、类之间的调用关系疏离:委托关系和中间人。
目录
1.Move Method 移动函数:类的行为做到单一职责 不要越俎代庖
2.Move Field 搬移字段:类的属性就应去改去的地方
3.Extract Class提炼类:一个类应该是一个清楚地抽象,处理一些明确的责任,把大类分解成小类
4. Inline Class 将类内联化:小类直接移到大类:与提炼类正好相反
5. Hide Delegate 隐藏委托关系:客户通过一个委托类在调用另一个对象。在服务类上建立客户所需的所有函数,用以隐藏委托关系。
6.Remove middle Man 移除中间人:与“隐藏“委托关系相反,让客户直接调用受托类
7. Introduce Foreign Method 引入外加函数:
8.Introduce Local Extension 引入本地扩展:
1.Move Method 移动函数
类的行为做到单一职责 不要越俎代庖: 你的程序中,有个函数与其所驻类之外的另一个类进行更多的交流:调用后者,或被后者调用。在该函数最常用引用的类中建立一个有着类似行为的新函数。将旧函数变成一个单纯的委托函数,或是将旧函数完全移除。
public class Ranking{
int baseScore = 10;
private int GetStudentIntegral(Student student){
if(student.TimeOut()){
return student.Score* 0.9f;
}
else return baseScore + student.Result;
}
}
public class Ranking{
int baseScore = 10;
private int GetStudentIntegral(Student student){
return student.GetIntegral(baseScore);
}
}
public class Student{
public int GetIntegral( int baseScore){
if(TimeOut()){
return Score* 0.9f;
}
else return baseScore + Score;
}
}
“搬移函数”是重构理论的支柱。如果一个类有太多行为,或如果一个类与另一个类有太多合作而形成高度耦合,就需要搬移函数。通过这种手段,可以使系统中的类更简单,这些类最终也将更干净利落的实现系统交付的工作。
浏览类的所有函数,从中找出这样的函数:使用另一个对象的次数比使用自己所驻对象的次数还多。一旦移动了一些字段,就该做这样的检查。一旦发现有可能搬移的函数,就观察调用它的那一端、它调用的那一端,已经继承体系中它的任何一个重定义函数。然后,会根据“这个函数与哪个对象的交流比较多”,决定其移动路径。
这往往不是容易做出的决定。如果不能肯定是否应该移动一个函数,就继续观察其他函数。移动其他函数往往会让这项决定变得容易一些。有时候,即使你移动了其他函数,还是很难对眼下这个函数做出决定。其实这也没什么大不了的。如果真的很难做出决定,那么也许“移动这个函数与否”并不是那么重要。所以,就凭本能去做,反正以后总是可以修改的。
2.Move Field 搬移字段
类的属性就应去改去的地方:你的程序中,某个字段被其所驻类之外的另一个类更多的用到。在目标类建立一个新字段,修改源字段的所有用户,令它们改用新字段
在类之间移动状态和行为,是重构过程中必不可少的措施。随着系统发展,你会发现自己需要新的类,并需要将现有的工作责任拖到新的类中。在这个星期看似合理而正确的设计决策,到了下个星期可能不再正确。这没问题,如果你从来没遇到这种情况,那才有问题。
如果发现对于一个字段,在其所驻类之外的另一个类中有更多函数使用了它,就考虑搬移这个字段。上述所谓“使用”可能是通过设值/取值函数间接进行的。也可能移动该字段的用户(某个函数),这取决于是否需要保持接口不受变化。如果这些函数看上去很适合待在原地,就选择搬移字段。
使用Extract Class (提炼类)时,也可能需要搬移字段。此时可以先搬移字段,然后搬移函数。
3.Extract Class提炼类
一个类应该是一个清楚地抽象,处理一些明确的责任,把大类分解成小类:某个类做了应该由两个类做的事。建立一个新类,将相关的字段和函数从旧类搬移到新类。
据单一职责原则,一个类应该有明确的责任边界。但在实际工作中,类会不断的扩展。当给某个类添加一项新责任时,你会觉得不值得分离出一个单独的类。于是,随着责任不断增加,这个类包含了大量的数据和函数,逻辑复杂不易理解。
一个类应该是一个清楚地抽象,处理一些明确的责任。但是在实际工作中,类会不断成长扩展。你会在这儿加入一些功能,在哪加入一些数据。给某个类添加一项新责任时,你会觉得不值得为这项责任分离出一个单独的类。于是,随着责任不断增加,这个类会变得过分复杂。
很快,你的类就会变成一团乱麻。
此时你需要考虑将哪些部分分离到一个单独的类中,可以依据高内聚低耦合的原则。如果某些数据和方法总是一起出现,或者某些数据经常同时变化,这就表明它们应该放到一个类中。另一种信号是类的子类化方式:如果你发现子类化只影响类的部分特性,或者类的特性需要以不同方式来子类化,这就意味着你需要分解原来的类。
/原始类
public class Person {
private String name;
private String officeAreaCode;
private String officeNumber;
public String getName() {
return name;
}
public String getTelephoneNumber() {
return ("(" + officeAreaCode + ")" + officeNumber);
}
public String getOfficeAreaCode() {
return officeAreaCode;
}
public void setOfficeAreaCode(String arg) {
officeAreaCode = arg;
}
public String getOfficeNumber() {
return officeNumber;
}
public void setOfficeNumber(String arg) {
officeNumber = arg;
}
}
//新提炼的类(以对象替换数据值)
public class TelephoneNumber {
private String areaCode;
private String number;
public String getTelephnoeNumber() {
return ("(" + getAreaCode() + ")" + number);
}
String getAreaCode() {
return areaCode;
}
void setAreaCode(String arg) {
areaCode = arg;
}
String getNumber() {
return number;
}
void setNumber(String arg) {
number = arg;
}
}
4. Inline Class 将类内联化
小类直接移到大类:与提炼类正好相反,但同样也要使用移动函数、移动字段。
某个类没有做太多事情。将这个类的所有特性搬移到另一个类中,然后移除原类。
Inline Class (将类内联化)正好于Extract Class (提炼类)相反。如果一个类不再承担足够责任、不再有单独存在的理由(这通常是因为此前的重构动作移走了这个类的责任),就挑选这个“萎缩类”的最频繁的用户(也是个类),以Inline Class (将类内联化)手法将“萎缩类”塞进另一个类中。
5. Hide Delegate 隐藏委托关系
客户通过一个委托类在调用另一个对象。在服务类上建立客户所需的所有函数,用以隐藏委托关系。
//员工Employee 部门department
public class Employee{
private Department department;
public Department GetDepartment(){
return department;
}
public void SetDepartment(Department _department){
department = _department;
}
}
public class Department(){
private Employee manager;//经理
public Department(Employee _person){
manager = _person;
}
public Employee GetManager(){
return manager;
}
}
如果客服想知道某人的经理是谁,那么他就得先取得Department对象:
manager = employee.GetDepartment().GetManager();
这样就对客户暴露了Department的工作原理,如果要对客户隐藏Department,可以减少耦合:在Employee中建立一个简单的委托函数
public employee GetManager(){
return department.GetManager();
}
那么获取经理的方式就改为:
manager = employee.GetManager();
封装”即使不是对象的关键特征,也是关键特征之一。“封装”意味每个对象都有应该尽可能少了解系统的其他部分。如此一来,一旦发生变化,需要了解这一变化的对象就会比较少,这会使变化较容易进行。
如果某个客户先通过服务对象的字段得到另一个对象,然后调用后者的函数,那么客户就必须知晓这一层委托关系。万一委托关系发生变化,客户也得相应变化。你可以在服务对象上放置一个简单的委托函数,将委托关系隐藏起来,从而去除这种依赖。这么一来,即便将来发生委托关系上的变化,变化也将被限制在服务对象中,不会波及客户。
对于某些或全部客户,你可能会发现,有必要先使用Extract Class (提炼类)。一旦你对所有客户都隐藏了委托关系,就不再需要在服务对象的接口中公开被委托对象。
6.Remove middle Man 移除中间人
- 与“隐藏“委托关系相反。
- 过多的中间层会增加代码维护的为难
某个类做了过多的简单委托动作。让客户直接调用受托类。
在Hide Delegate (隐藏委托关系)的“动机”中,谈到了“封装委托对象”的好处。但是这层封装也是要付出代价的,它的代价是:每当客户要使用受托类的新特性时,你就必须在服务端添加一个简单委托函数。随着委托类的特性(功能)越来越多,这一过程让你痛苦不已。服务类完全变成了“中间人”,此时你就应该让客户直接调用受托类。
很难说什么程度的隐藏才是合适的。还好,有了Hide Delegate (隐藏委托关系)和Remove Middle Man (移除中间人),你大可不必操心这个问题。因为你可以在系统运行过程中不断进行调整。随着系统的变化,“合适的隐藏程度”这个尺度也相应改变。6个月前恰如其分的封装,现今可能就显得笨拙。重构的意义在于:你永远不必说对不起—只要把出问题的地方修补好就行了。
7. Introduce Foreign Method 引入外加函数
你需要为提供服务的类增加一个函数,但你无法修改这个类。在客户类中建立一个函数,并以第一参数形式传入一个服务类实例。
Date newStart = new Date(previous.GetYear(),
previous.GetMonth(),
previous.GetDate());
Data newStart = new Date(previous.GetYear(),previous.GetMonth(),previous.GetDate());
private Date NextDate(Date arg){
return new Date(arg.GetYear(),arg.GetMonth(),arg.GetDate());
}
这种事情发生了太多次了,你正在使用一个类,它真的很好,为你提供了需要的所有服务。而后,你又需要一项新服务,这个类却无法供应。于是你开始咒骂“为什么不能做这件事?”如果可以修改源码,你便可以自行添加一个新函数;如果不能,你就得在客户端编码,补足你要的那个函数。
如果客户类只使用这项功能一次,那么额外编码工作没什么大不了,甚至可能根本不需要原本提供服务的那个类。然而,如果你需要多次使用这个函数,就得不断重复这些代码。重复代码是软件万恶之源。这些重复代码应该被抽出来放进一个函数中。进行本项重构时,如果你以外加函数实现一项功能,那就是一个明确信号:这个函数原本应该在提供服务的类中实现。
如果你发现自己为一个服务类建立了大量外加函数,或者发现有许多类需要同样的外加函数,就不应该再使用本项重构,而应该使用 Introduce Local Extension (引入本地扩展)。
但是不要忘记:外加函数终归是权宜之计。如果有可能,你仍然应该将这些函数搬移到它们的理想家园。如果由于代码所有权的原因使你无法做这样的搬移,就把外加函数交给服务类的提供者,请他帮你在服务类中实现这个函数。
8.Introduce Local Extension 引入本地扩展
public class DateSub:Date{
//处理构造函数的时候注意下
//添加新特性
private Date NextDate(){
return new Data(GetYear(),GetMonth(),GetData());
}
}
作为 包装类
public class DateWrap{
private Date original;
public DateWrap(Date arg){
original = arg;
}
//为原始类所有函数提供委托函数,例如:
public int GetYear(){
return original.GetYear();
}
...
//添加新特性
private Date NextDate(){
return new Data(GetYear(),GetMonth(),GetData());
}
}
类的作者无法预知未来,他们常常没能为你预先准备一些有用的函数。如果你可能修改源码,最后的办法就是直接加入自己需要的函数。但你经常无法修改源码。如果只需要一两个函数,你可以使用 Introduce Foreign Method (引入外加函数)。但如果你需要的额外函数超过2个,外加函数就很难控制它们了。所以你需要将这些函数组织在一起,放到一个恰当的地方去。要达到这个目的,2种标准对象技术—子类化(subclassing)和包装(wrapping)是显而易见的办法。这种情况下,把子类化和包装类统称为本地扩展。
所谓本地扩展是一个独立的类,但也是被扩展类的字类型:它提供源类的一切特性,同时额外添加新特性。在任何使用源类的地方,你都可以使用本地扩展取而代之。
使用本地扩展使你得以坚持:函数和数据应该被统一封装“的原则。如果你一直把本该放在扩展类中的代码零散的放置于其他类中,最终只会让其他这些类变得过分复杂,并使得其他函数难以被复用。
在子类和包装类之间做选择时,首选子类。因为这样的工作量比较少。制作子类的最大障碍在于,它必须在对象创建期实施。如果可以接管对象创建过程,那当然没问题;但如果你想在对象创建之后再使用本地扩展,就有问题了。此时,子类化方案还必须产生一个子类对象,这种情况下,如果有其他对象引用了旧对象,我们就同时有2个对象保存了原数据。如果原数据是不可修改的,那也没问题。可以放心进行复制;但如果原数据允许修改,问题就来了,因为一个修改动作无法同时改变2份副本。这时候就必须改用包装类。使用包装类时,对本地扩展的修改会波及原对象,反之亦然。