重构-改善既有代码的设计:(类重构)—对象之间移动特性的八种方法(四)

       在面向对象编程过程中,明确该对象的职责。类应该是:做自己该做的事,应尽该尽的义务:

主要内容:

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份副本。这时候就必须改用包装类。使用包装类时,对本地扩展的修改会波及原对象,反之亦然。

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

hguisu

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值