• 小酌重构系列[7]——使用委派代替继承


    概述

    子类可以继承父类的字段、属性和方法,使用“继承”可以较大程度地复用代码。
    在使用继承时,务必要确定代码中定义的“父类”和“子类”确实存在客观的“父子关系”,而不要去做“为了代码复用而使用继承”的事情,这是舍本逐末的做法,也是滥用继承的体现。
    滥用继承会破坏类之间客观存在的关系,也会模糊代码所体现的语义。

    使用委派代替继承

    继承的误区

    当多个类具有相似的属性、方法时,使其中一个类变成基类,其他的类去继承该基类实现代码复用。
    当将应用场景中的某一个类提升为基类时,需要慎重考虑这个类是否确实和其他类存在“父子关系”,如果不存在“父子关系”,则不构成继承关系。

    如何确定类之间的继承关系?
    应该从现实的业务和语义去理解(这是一句无关痛痒的废话)。

    举个不那么恰当的例子,虽然大头儿子和隔壁王叔叔长得实在是太像了,但是在法律上,大头儿子继承小头爸爸是合法的,而不是去继承隔壁的王叔叔。
    (PS:我也不知道大头儿子是不是小头爸爸亲生的)

    继承的滥用

    下图是Java 中的Vector和Stack类,为了让IsEmpty()方法得以复用,Stack继承了Vector。
    但Vector和Stack是不同的数据结构,二者没有明显的“父子关系”,这是典型的滥用继承的做法。

    image

    委派代替继承

    现在问题来了,当两个类确实不存在继承关系,并且一个类依赖于另外一个类的方法时,应该如何构建这2个类的关系?
    这就是本文要说明的重构策略——使用委派代替类继承

    继承是一种强关联的关系,而委派是一种弱关联的关系。

    示例

    重构前

    这段代码定义了Sanitation类和Child类和类,用于描述“小孩洗手”这件事情。
    在这件事情中,Sanitation类表示卫生设施,比如水龙头和洗手液,可供人们洗手,当然也可供小孩洗手;Child表示小孩,他是利用这些卫生设施去洗手。
    Sanitation类提供了WashHands()方法,并让Child继承Sanitation。

    public class Sanitation
    {
        public string WashHands()
        {
            return "Cleaned!";
        }
    }
    
    public class Child : Sanitation
    {
    }
    

    从语义上分析:

    1. 卫生设施可供小孩洗手,这是一个被动行为。小孩利用卫生设施洗手,这是一个主动行为。
    2. 小孩不是从卫生设施里面蹦出来的,而是诞生自父母的受精卵,二者之间本身是一个间接关系。

    二者之间仅存在“利用”关系,不存在“继承”关系。为了体现“利用”语义,我们应该使用委派。

    重构后

    将这段代码进行如下调整:

    1. 解除Child和Sanitation的继承关系。
    2. 在Child类中定义Sanitation属性。
    3. 在Child类中定义WashHands()方法,并调用Sanitation属性的WashHand()方法。
    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();
        }
    }

    大家可能会对重构后的代码产生疑问,代码量比之前多了,Child还需要依赖于Sanitation类,这能带来什么好处?
    仅从代码角度去看,这确实如你所想,它确实没有带来什么好处。

    但我觉得代码应该能够体现客观事实和语义——“小孩利用卫生设施去洗手”,而不是“小孩因为继承了卫生设施,才具备洗手的行为”。

    我个人一直比较提倡一个观点——代码层面的所见即所得,当我们看到一段代码时,就知道这段代码能做什么事情,不需要过多的修辞和注释,不多不少,恰如其分。用一个词概括,就是“言出法随”。

    就好比这篇文章:写了10年的代码,我最怕写这段代码中的一些“干货”(没错,我确实不认为这是什么“干货)。
    “判断字符串是否为Email”,本来就是一件很简单的事情,为何不能直截了当地去描述呢?

    public static bool Email(this String str)
    {
        bool result = false;
        if (!string.IsNullOrEmpty(str) && System.Text.RegularExpressions.Regex.IsMatch(str, @"^([0-9a-zA-Z]+[-._+&])*[0-9a-zA-Z]+@([-0-9a-zA-Z]+[.])+[a-zA-Z]{2,6}$"))
        {
            result = true;
        }
        else
        {
            result = false;
        }
        return result;
    }

    “判断字符串是否为Email”这件事情用一句话就能说清楚它的语义和逻辑——“字符串不为空,且匹配Email正则表达式”

    public static bool IsEmail(this String str)
    {
        string emailPattern = @"^([0-9a-zA-Z]+[-._+&])*[0-9a-zA-Z]+@([-0-9a-zA-Z]+[.])+[a-zA-Z]{2,6}$";
        return !string.IsNullOrEmpty(str) && Regex.IsMatch(str, emailPattern);
    }

    这段代码没有用到语法糖,也不是什么装x行为,我也不是为了体现2行代码就一定比10行代码好。

  • 相关阅读:
    函数的随机梯度下降(固定函数与自定义函数随机梯度下降)
    常见六种随机变量分布可视化
    基于beautifulSoup进行电影网站排名的获取与格式化输出
    基于numpy实现矩阵计算器
    中文论文-LaTex模板
    论文阅读:Deformable ConvNets v2
    darknet loss可视化软件
    《为什么精英都是时间控》读书总结
    Window下Latex加速编译方法以及西农毕设论文模板推荐
    时间加数字问题
  • 原文地址:https://www.cnblogs.com/keepfool/p/5467753.html
Copyright © 2020-2023  润新知