zoukankan      html  css  js  c++  java
  • 小酌重构系列[11]——提取基类、提取子类、合并子类

    概述

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

    提取基类

    定义:如果有超过一个类有相似的功能,应该提取出一个基类,并将共通的功能放在基类中。

    图说

    初定关系

    下图左定义了Department和Employee类,虽然这两个类有一些共同的特质——“获取年度成本”、“获取名称”。

    image

    在你第一眼看到这张图时,会觉得Department和Employee会是什么关系?通常情况下,大家可能会觉得Department和Employee是一个聚合关系。

    image

    从这两个类的特质分析,它们具有一些共同点——“获取年度成本”、“获取名称”。
    如果希望重用这些共通点,我们该怎么做呢?当然是使用继承啦。
    拿Employee继承Department吗?毛线啊,Department和Employee在语义上不存在“is a kind of”关系啊。我们总不能说“Employee is a kind of department”吧。

    Well,这条路子走不通,那我们换个概念来理解Department和Employee吧——Party。
    Party是个什么玩意儿?在概念上,Party表示一个群体,群体可以是一个部门,也可以是一个人。

    造句时刻

    前面讲了,继承关系可以用"is a kind of"来描述,现在咱们就用"is a kind of"造几个句子试试。
    "Department is a kind of party."
    "Employee is a kind of party."
    "Party"概念这样理解起来是否通顺一些了呢?我觉得是通顺的。

    如果你觉得它不通顺,咱们换个姿势再来造2个句子,用被动式造句。
    "Party can be a kind of department."
    "Party can be a kind of employee."

    "Party"是否也能作为Department和Employee的抽象概念呢?我仍然觉得是(如果您觉得还不是,我也没辙了)。

    确定关系

    经过这么一番折腾后,我们终于得以见证以下关系的产生。
    注意,我们仍然保留了Department和Employee的聚合关系。

    image

    看了这几幅图,是否会引起你的思考呢?
    一些看似没有特定关系的事物,可以通过抽象更高层次的概念,让它们产生其它层面的一些关系。
    Department和Employee看似没有继承关系,然而通过抽象出"Party"这个概念,让它Department和Employee产生了另外一层关系——同为Party的子类。

    示例

    重构前

    这段代码定义了一个Dog类,并定义了两个方法:EatFood()和Grrom(),分别表示“进食”和“训练”行为。

    public class Dog
    {
        public void EatFood()
        {
            // eat some food
        }
    
    
        public void Groom()
        {
            // perform grooming
        }
    }
    

    EatFood()和Groom()是大多数动物的公有行为,当追加新的动物class时,也可能会有这两个行为,我们应考虑将这两个行为提取到基类。

    重构后

    public class Animal
    {
        public void EatFood()
        {
            // eat some food
        }
    
    
        public void Groom()
        {
            // perform grooming
        } 
    }
    
    public class Dog : Animal
    {
    
    }
    

    小提示:在提取基类时,如果当下只有一个类,你需要根据场景去分析“将来”可能发生的事情,这个例子较好地体现了这一点。
    Of course,你可以不必这么早地去提取基类,等到代码出现2~3次重复以后再去提取也不迟。

    提取子类

    定义:如果基类的方法或属性不是所有子类需要的,应将这些方法和属性提取到子类中。

    图说

    有人可能觉得基类中的方法或属性越多越好,这样子类只需要很少的代码就能完成很多的功能。
    我们在基类中定义方法或属性时,一定要事先确定基类的职责。

    初回

    下图的Animal类定义了4个方法:Eat()、Run()、Fly()和Swim(),分别表示进食、奔跑、飞翔和浮游行为。

    image

    确实所有的动物都要进食,但世界上基本上不存在水陆空三栖动物,你以为这是传说中的龙吗?

    第2回

    现在你添加了一个类Mammal,它继承了Animal类。

    image

    Eat()和Run()是哺乳动物的共通行为,Fly()和Swim()并不是所有哺乳动物都具备的能力。

    第3回

    再丰富一下咱们的动物世界,把鸟类和鱼类也加进来( 哺乳动物终于不那么寂寞了,可以吃海鲜和烧鸡了)。

    image

    咱们不搞特殊化,从一般化上去认知Mammal、Bird和Fish。
    一般来说,Mammal具备Eat()和Run()行为,Bird具备Eat()和Fly()行为,Fish具备Eat()和Swim()行为。

    这幅图如果用代码来展现,代码量会非常少。
    虽然使用继承可以减少代码量,可以一定程度上达到代码复用的目的,但这不意味着准确性,也不意味着较低的复杂性。

    以上内容已经说明了“不准确性”,即子类的行为不那么准确,那是基类强加给子类的。
    接着,我们来看看复杂性,上面这幅图为例,我们从两个角度来判断复杂性:

    • 基类 + 子类的方法总数:一共16个方法
    • 每个子类的方法总数:每个子类都有4个方法

    第4回

    结合下面这幅图,我们比较一下这两种方式的复杂性:

    • 基类 + 子类的方法总数:一共7个方法
    • 每个子类的方法总数:每个子类都有2个方法

    image

    从数量上看,这两种方式的复杂程度不言而喻。

    终回

    磨磨唧唧了这么久,用一幅图来表示“提取子类”这个重构策略吧(绿色表示重构前,红色表示重构后)。

    image

    示例

    重构前

    Registration类描述了学生选课的场景,学生选课有两种情况——已注册的课程和未注册的课程。

    public class Registration
    {
        public NonRegistrationAction Action { get; set; }
        public decimal RegistrationTotal { get; set; }
        public string Notes { get; set; }
        public string Description { get; set; }
        public DateTime RegistrationDate { get; set; }
    }
    
    public class NonRegistrationAction
    {
        
    }
    

    Registration类的属性NonRegistrationAction和Notes只有在未注册的场景才会用到,所以可以考虑将这两个属性提取到子类。

    重构后

    重构时追加了NonRegistration类,这样职责划分就很清晰了:Registration类对应已注册的选课场景,NonRegistration类对应未注册的选课场景。

    public class Registration
    {
        public decimal RegistrationTotal { get; set; }
        public string Description { get; set; }
        public DateTime RegistrationDate { get; set; }
    }
    
    public class NonRegistration : Registration
    {
        public NonRegistrationAction Action { get; set; }
        public string Notes { get; set; }
    
    }
    
    public class NonRegistrationAction
    {
        
    }

    小提示:在基类中定义方法和属性时,请确定它们是否具备“一般性”。

    合并子类

    定义:当子类所有的方法属性完全适用于基类时,应该将这些方法和属性提取到基类,并移除子类。

    这个重构策略比较简单,咱就不上图示了,直接用示例来向大家说明吧 ,有点困了2F7A6592

    示例

    重构前

    这段代码定义了两个类:Website和StudentWebsite,分别表示“网站”和“学生网站”( 难道还有“成人网站”?2F7BAF78[3]) 。StudentWebsite继承自Website,StudentWebSite扩展了一个IsActive属性,表示“是否激活”。

    public class Website
    {
        public string Title { get; set; }
        public string Description { get; set; }
        public IEnumerable<Webpage> Pages { get; set; } 
    }
    
    public class StudentWebsite : Website
    {
        public bool IsActive { get; set; }
    }
    
    public class Webpage
    {
        
    }
    

    重构后

    实际上IsActive属性完全适用于所有的Website实例,所以没有必要单独地声明一个StudentWebsite类,可以将IsActive属性定义到Website类中,并移除StudentWebSite类(难道我的“成人网站”说没有就没有了吗?)

    public class Website
    {
        public string Title { get; set; }
        public string Description { get; set; }
        public IEnumerable<Webpage> Pages { get; set; }
        public bool IsActive { get; set; }
    }
    
    public class Webpage
    {
    
    }
    

    小提示:不要盲目地创建子类,也不要盲目地在子类中定义方法和属性,搞不好基类也要用呢。

  • 相关阅读:
    shell脚本的常用参数
    Qt中使用Protobuf简单案例(Windows + msvc)
    使用PicGo和Typora写Markdown
    CentOS7安装protobuf(C++)和简单使用
    protobuf编译、安装和简单使用C++ (Windows+VS平台)
    protocol buffers 文档(一)-语法指导
    Base64编码和其在图片的传输的应用
    Qt程序打包发布
    Qt中的Label和PushButton背景图自动缩放设置
    TCP的粘包和拆包问题及解决
  • 原文地址:https://www.cnblogs.com/keepfool/p/5488031.html
Copyright © 2011-2022 走看看