小酌重构系列[3]——方法、字段的提升和降低

本文要介绍的是4种重构策略,它们分别是提升方法、降低方法、提升字段和降低字段。
由于这4种重构策略具有一定的相通性,所以我将它们放到一篇来讲解。

定义

以下是这4种策略的定义

提升方法:当子类的方法描述了相同的行为时,应将这样的方法提升到基类。
降低方法:在基类中的行为仅和个别子类相关时,应将这样的行为降低到子类。
提升字段:当子类中的字段描述着相同的信息时,应将这样的字段提升到基类。
降低字段:当基类中的字段仅仅用于个别子类时,应将这样的字段降低到子类。

以上的定义是较为为枯燥无趣的,各位读者大可不必care文字的内容,因为这是我自己的理解,你们应该会有自己的理解。
接下来,我要介绍本文的重点——语义,这有助于我们理解并良好地使用这些重构策略。

事物的“语义”

在前面的文章中,我常常提到一个词“语义”,它是一个较为抽象的词。以下引用内容是对语义的解释,这段内容来源于百度百科

数据的含义就是语义(semantic)。简单的说,数据就是符号。数据本身没哟佮意义,只有赋予含义的数据才能够被使用,这时候数据就转化为了信息,而数据的含义就是语义。

语义可以简单地看作是数据对应的现实世界中的事物所代表的概念和含义,以及这些含义直接的关系,是数据在某个领域上的解释和逻辑表示。

语义具有领域特征,不属于任何领域的语义是不存在的。

而我对它的理解是:在事物所处的环境下,事物所表现的概念和含义。
这里面有2点要强调一下:

1. 事物:是指现实世界(真实)存在的个体,比如一个人、一辆车。这也是我们所说的对象。
2. 环境:是指现实世界的环境,我们也可以将环境理解为上下文。

我们需要结合这2点去理解语义,事物不能脱离环境单独存在,事物在不同的环境下表现出的特征和行为会有所不同。

例如:一辆普通的大众捷达汽车,如果它处于“出租车公司”这样一个语境,那么它的表现特征是“出租车”,体现出来的行为是“为市民提供有偿的乘车服务”。
如果这辆车处于“某人的车”这样一个语境,那么它的表现特征是“私家车”,体现出来的行为是”车主可以自驾去做xxx事“。

当你在街上分别看到下面两部车时,你会理所当然地认为左边的是“出租车”,右边的是“私家车”。


你近乎条件反射地知道了这两部车所代表的语义!你为什么能够如此快速地定义它们呢?
因为我们对这两部车已经有了足够的认知,即使我们不去触碰它们,但是结合我们自身的知识和经验,它们所代表的含义已经深深地刻在我们的心底。

从这个例子我们可以很容易地看出,当事物处于不同的环境时,它们表现的特征和行为是有差异的。
这也是所谓的“语义异构”,它指的是同一事物在解释上所存在的差异,也就体现为同一事物在不同领域中的理解不同。

另外,由于小孩子认知上的不足,他们对这两部车的理解和大人也会有所不同。

小孩:“这两部车都能带我去游乐场玩”
大人:左边那辆车能”为市民提供有偿的乘车服务“,右边那辆车的“车主可以自驾去做xxx事”

小孩由于对事物的认知较为浅薄,所以他们的主观判断也是较浅显的。
大人由于对事物已经足够了解了,所以他们的主观判断时较深刻的。

每个人都是从小孩成长到大人的,人们对事物的探索和认知也会经历这个过程。在不同时期,不同场合,人们对同一个事物的认知和理解是不同的。

现在大致介绍完了语义,我们正式进入本文的示例环节。以下这4则示例代码非常简单,请结合语义去感受这4种重构策略。

提升方法

当子类的方法描述了相同的行为时,应将这样的方法提升到基类。

下图表示了这个重构策略(蓝色表示重构前,红色表示重构后)

方法提升到基类时,应该注意两点:

1. 基类中定义的行为实现细节,应该是所有子类共有的。
2. 子类应该具有重写基类行为的能力,重写时应该是对行为细节的附加,而不应当随意篡改基类的行为细节(你确实可以这么做,但我不建议这么做)

示例

重构前

这段代码定义了3个类:Vechicle(机动车),Car(汽车)和Motorcycle(摩托车)。在Car里定义了Turn()方法,表示汽车的行驶行为。

namespace PullUpMethod.Before
{
    public abstract class Vehicle
    {
        // other methods
    }

    public class Car : Vehicle
    {
        public void Turn(Direction direction)
        {
            // code here
        }
    }

    public class Motorcycle : Vehicle
    {
    }

    public enum Direction
    {
        Left,
        Right
    }
}

在这个场景中,Motorcycle也具有行驶行为,如果在Motorcycle中也定义一个Turn()方法,会造成语义上的重复,所以我们应将Car中的Turn()方法提升到基类Vehicle。

重构后

namespace PullUpMethod.After
{
    public abstract class Vehicle
    {
        public virtual void Turn(Direction direction)
        {
            // 基类行为的实现细节
        }
    }

    public class Car : Vehicle
    {

    }

    public class Motorcycle : Vehicle
    {
        public override void Turn(Direction direction)
        {
            // 使用基类行为的细节
            base.Turn(direction);
            // 附加一些子类本身的行为细节
        }
    }

    public enum Direction
    {
        Left,
        Right
    }
}

Vehicle类的Turn()方法使用了virtual关键字,当基类的实现方法不能满足子类的需求时,我们可以在子类中override。

降低方法

在基类中的行为仅和个别子类相关时,应将这样的行为降低到子类。

下图表示了这个重构策略(蓝色表示重构前,红色表示重构后)

示例

重构前

这段代码定义了3个类:Animal(动物)、Dog(狗)和Cat(猫),在Animal里定义了Bark()方法,表示动物的吠叫行为。

namespace PushDownMethod.Before
{
    public abstract class Animal
    {
        public void Bark()
        {
            // code to bark
        }
    }

    public class Dog : Animal
    {
    }

    public class Cat : Animal
    {
    }
}

在这个场景中,Dog能够吠叫,吠叫行为不属于Cat,Cat只能喵喵叫,所以应将Bark()方法降低到Dog类。

重构后

namespace PushDownMethod.After
{
    public abstract class Animal
    {
    }

    public class Dog : Animal
    {
        public void Bark()
        {
            // code to bark
        }
    }

    public class Cat : Animal
    {
    }
}

提升字段

当子类中的字段描述着相同的信息时,应将这样的字段提升到基类。

下图表示了这个重构策略(蓝色表示重构前,红色表示重构后)

在C#中,字段通常都是以private修饰的。当使用这种重构策略时,为了让子类能够访问,提升到基类的字段至少应该使用protected修饰符。

示例

重构前

这段代码定义了3个类:Account(账户)、CheckingAccount(活期账户)和SavingAccount(储蓄账户),CheckingAcount和SavingAccount继承自Account。

namespace PullUpField.Before
{
    public abstract class Account
    {
    }

    public class CheckingAccount : Account
    {
        private decimal _minimumCheckingBalance = 5m;
    }

    public class SavingAccount : Account
    {
        private decimal _minimumSavingBalance = 5m;
    }
}

在这个场景中,CheckingAccount和SavingAccount都定义了最小余额字段,虽然命名不同,但表示的含义是一样的,所以应在Account中定义最小余额字段,同时使用protected修饰该字段。

重构后

namespace PullUpField.After
{
    public abstract class Account
    {
        protected decimal _minimumBalance = 5m;
    }

    public class CheckingAccount : Account
    {

    }

    public class SavingAccount : Account
    {

    }
}

降低字段

当基类中的字段仅仅用于个别子类时,应将这样的字段降低到子类。

下图表示了这个重构策略(蓝色表示重构前,红色表示重构后)

示例

重构前

这段代码定义了3个类:Task(任务)、BugTask(缺陷任务)和FeatureTask(功能任务),基类Task定义了_resolution字段。

namespace PushDownField.Before
{
    public abstract class Task
    {
        protected string _resolution;
    }

    public class BugTask : Task
    {
    }

    public class FeatureTask : Task
    {

    }
}

在这个场景中,_resolution字段表示“bug的解决状态”,这个字段和BugTask类有关,和Feature类是无关的,所以应将_resolution字段定义在BugTask类,并以private修饰。

重构后

namespace PushDownField.After
{
    public abstract class Task
    {

    }

    public class BugTask : Task
    {
        private string _resolution;
    }

    public class FeatureTask : Task
    {

    }
}

总结

这4种方式是较为简单的重构策略,也是经常使用的重构策略。
如果要使用好这些策略,需要我们对类、方法和字段的语义有一个清晰地了解和认知。

即使再简单的重构策略,也需要左右权衡,否则可能造成“过度重构”或“重构不当”。
如果您只是刚开始经历重构,请不要过于担心这两点,你能发现这两个问题,说明你已经思考过了,你需要经历这个过程才能够有所成长。

时间: 2024-11-03 05:39:47

小酌重构系列[3]——方法、字段的提升和降低的相关文章

小酌重构系列[11]——提取基类、提取子类、合并子类

概述 继承是面向对象中的一个概念,在小酌重构系列[7]--使用委派代替继承这篇文章中,我"父子关系"描述了继承,这是一种比较片面的说法.后来我又在UML类图的6大关系,描述了继承是一种"is a kind of"关系,它更偏向于概念层次,这种解释更契合继承的本质.本篇要讲的3个重构策略提取基类.提取子类.合并子类都是和继承相关的,如果大家对继承的理解已经足够深刻了,这3个策略用起来应该会得心应手. 提取基类 定义:如果有超过一个类有相似的功能,应该提取出一个基类,并

小酌重构系列目录汇总

为了方便大家阅读这个系列的文章,我弄了个目录汇总. 方法.字段重构 移动方法 (2016-04-24) 提取方法.提取方法对象 (2016-04-26) 方法.字段的提升和降低 (2016-05-01) 分解方法 (2016-05-02) 为布尔方法命名 (2016-05-03) 引入对象参数 (2016-05-04) 类.接口重构 使用委派代替继承 (2016-05-07) 提取接口 (2016-05-08) 解除依赖 (2016-05-09) 分离职责 (2016-05-11) 提取基类.提

小酌重构系列[5]——为布尔方法命名

概述 当一个方法包含大量的布尔参数时,方法是很脆弱的,由此还可能会产生两个问题: 1. 方法不容易被理解 2. 给方法的使用者造成一定困扰,可能会产生一些预期之外的结果. 本文要介绍的重构策略"为布尔方法命名",可以有效地避开这两个问题. 为布尔方法命名 大量布尔参数带来的问题 下图中的SomeClass的SomeMethod包含3个布尔参数,如果没有注释,调用者根本不知道3个布尔参数所代表的含义. 即使为这个方法提供了详细的注释,调用者也很容易在调用时出错.调用者一不小心写错了其中一

小酌重构系列[4]——分解方法

概述 "分解方法"的思想和前面讲到的"提取方法"."提取方法对象"基本一致.它是将较大个体的方法不断的拆分,让每个"方法"做单一的事情,从而提高每个方法的可读性和可维护性.分解方法可以看做是"提取方法"的递归版本,它是对方法反复提炼的一种重构策略. 分解方法 下图表示了这个重构策略,第1次提炼和第2次提炼都采用了"提取方法"这个策略. 何时分解方法? "分解方法"最终

小酌重构系列[2]——提取方法、提取方法对象

前言 "艺术源于生活"--代码也源于生活,你在生活中的一些行为习惯,可能会恰如其分地体现在代码中.当实现较为复杂的功能时,由于它包含一系列的逻辑,我们倾向于编写一个"大方法"来实现.为了使项目便于维护,以及增强代码的可读性,我们有必要对"大方法"的逻辑进行整理,并提取出分散的"小方法".这就是本文要讲的两种重构策略:提取方法.提取方法对象. 如何快速地找到想读的书? 在生活中,我是一个比较随意的人,平时也买了不少书去看.我的书

小酌重构系列[12]——去除上帝类

关于上帝类 神说:"要有光",就有了光.--<圣经>.上帝要是会写程序,他写的类一定是"上帝类".程序员不是上帝,不要妄想成为上帝,但程序员可以写出"上帝类".上帝是唯一的,上帝的光芒照耀人间,上帝是很爱面子的,他知道程序员写了"上帝类",抢了他的风头,于是他降下神罚要惩戒程序员.--既然你写了"上帝类",那么就将你流放到艰难地修改和痛苦的维护的炼狱中,在地狱之火中永久地熬炼. 你看,上帝也是有

小酌重构系列[7]&mdash;&mdash;使用委派代替继承

概述 子类可以继承父类的字段.属性和方法,使用"继承"可以较大程度地复用代码.在使用继承时,务必要确定代码中定义的"父类"和"子类"确实存在客观的"父子关系",而不要去做"为了代码复用而使用继承"的事情,这是舍本逐末的做法,也是滥用继承的体现.滥用继承会破坏类之间客观存在的关系,也会模糊代码所体现的语义. 使用委派代替继承 继承的误区 当多个类具有相似的属性.方法时,使其中一个类变成基类,其他的类去继承该基

小酌重构系列[15]&mdash;&mdash;策略模式代替分支

前言 在一些较为复杂的业务中,客户端需要依据条件,执行相应的行为或算法.在实现这些业务时,我们可能会使用较多的分支语句(switch case或if else语句).使用分支语句,意味着"变化"和"重复",每个分支条件都代表一个变化,每个分支逻辑都是相似行为或算法的重复.当追加新的条件时,我们需要追加分支语句,并追加相应的行为或算法. 上一篇文章"使用多态代替条件判断"中,我们讲到它可以处理这些"变化"和"重复&qu

小酌重构系列[20]&mdash;&mdash;用条件判断代替异常

概述 异常处理的关键在于何时处理异常以及如何使用异常,有些开发者会觉得try catch的处理和使用难以把握,于是他们秉承着"您可错杀一千,不可放过一个"的想法,给所有的方法添加try catch. 这种方式会对应用程序造成什么影响吗? 从用户角度出发,用户确实难以察觉到什么,应用程序运行正常,使用的体验好像也没什么差别. 从程序角度出发,大量的try catch会降低代码的可读性,只有在异常触发时才会对程序的性能造成较大的影响. 这两种角度有对错吗? 二者都没有错,第一种角度甚至要远