聊聊面向对象设计中的Is-A

面向对象编程范式得到了广大开发者的青睐,在做面向对象软件设计的同仁也或多或少曾经心存困惑过。比如,怎么样才是正确的封装?如何恰当的继承?何时应该抽象? 对于设计,我们很难说对与错,通常只有好与不好的区分,而所谓的最佳实践也只是 -- 在当前约束下,人们所能找到的最佳解决方案。

最近我在给ThoughtWorks内部某海外交付团队的核心成员(Tech Lead & Second Tier)做OO Bootcamp的培训,在分享讨论和编码实践的过程中加强了对面向对象设计的理解,本文我来聊一聊面向对象中关于继承设计的IS-A的这个工具。


IS-A是把好尺子

在做面向对象设计的时候,我们心中始终会装着三大武器:封装继承多态,设计出的软件也得有它们的身影。然而,很多时候并不是没有它们,而是它们的影子太多了(滥用或误用)。就拿继承来说,我们会经常使用IS-A来审视两个类的继承关系。比如以下场景:

  1. A Parrot IS A Bird(鹦鹉是一只鸟)
  2. A Man IS A Person(男人是一个人)
  3. A Square IS A Rectangle(正方形是一个矩形)

以上关系,单纯从自然属性来思考都好像是正确的,所以我们在设计继承关系的时候通常会很容易类似写出以下代码:

class Man extends Person{ }

class Person {
    private int age;
    private double height;
    
    public void walk(){ }
}

class Square extends Rectangle{ }

class Rectangle{ }

因为IS-A这把尺子的辅助,我们很容易地采用了继承,继承之后,子类什么也不用做就拥有了父类的特征和行为能力。看起来很好,它如期达到我们复用的期望。


IS-A的失效区

Square IS A Rectangle来说,我们都知道正方形是一个矩形,这话没毛病。然而,当我们按照真实业务要求完善Rectangle之后可能是这样子的:

public class Rectangle {
    protected double width;
    protected double height;
    public void setWidth(double width) { this.width = width; }
    public void setHeight(double height) { this.height = height; }
    public double calculateArea() { return width * height; }
}

public class Square extends Rectangle {
    @Override
    public void setHeight(double height) {
        this.height = height;
        this.width = height;
    }
    @Override
    public void setWidth(double width) {
        this.height = width;
        this.width = width;
    }
}

此时我们有一个客户类这样使用Rectangle:

public class SizeChanger {
    private double newWidth;
    private double newHeight;

    public SizeChanger(double newWidth, double newHeight) {
        this.newWidth = newWidth;
        this.newHeight = newHeight;
    }

    public double resize(Rectangle rectangle) {
        rectangle.setWidth(newWidth);
        rectangle.setHeight(newHeight);
        return rectangle.calculateArea();
    }
}

resize方法接受一个Rectangle对象参数,而Square作为子类,也可以被传入到这个方法中,比如我们测试客户类:

class SizeChangerTest {
    @Test
    void should_calculate_correct_area_after_resize() {
        SizeChanger sizeChanger = new SizeChanger(5, 10);
        Rectangle rectangle = new Rectangle();
        rectangle.setWidth(4);
        rectangle.setHeight(5);

        assertThat(sizeChanger.resize(rectangle)).isEqualTo(50);
    }
}

我们期望resize的返回值是50,没毛病。但我们如果把Rectangle子类对象传给resize方法就会挂掉:

class SizeChangerTest {
    @Test
    void should_calculate_correct_area_after_resize() {
        SizeChanger sizeChanger = new SizeChanger(5, 10);
        Rectangle rectangle = new Square();
        rectangle.setWidth(4);
        rectangle.setHeight(5);

        assertThat(sizeChanger.resize(rectangle)).isEqualTo(50); // 100 not 50
    }
}

作为客户程序就会产生疑惑了:"我调用resize方法的表现时而不一样,这让我很焦虑,没法信任你的程序,既然A Square IS A Rectangle,给resize传入SquareRectangle的结果应该是跟期望一致的。" 所以从resize的角度来看,A Square IS NOT A Rectangle。而导致这一现象的原因是SquareRectangle的行为方式发生了改变,它们的setWidthsetHeight行为不一样。

行为是面向对象设计的关键所在,我们通过封装将对象属性隐藏,以API的方式来服务于客户程序,这些公开的API就是一系列行为,这些行为正是客户程序想使用的(客户程序依赖这些行为),它们也构成了我们软件的功能。

所有,不难理解LSP(里氏替换原则)强调IS-A的关系是针对行为方式来讲的,这也是面向对象软件设计中与真实世界的对象关系的微妙差别,当子类与父类针对某个具体的行为发生改变时,这个继承就违背了LSP


拯救IS-A的铁弹

IS-A是基于行为方式的,也就是说,当你的子类改变了父类的某个具体行为时,IS-A就需要重新审视了。

如何重新审视?你需要进一步进行抽象,进一步提取抽象概念,此时需要念出面向抽象编程的六字真经了,抽出多态这把匕首,并移步让里氏替换原则为你效力


Posted by 袁慎建@ThoughtWorks

版权声明:自由转载•非商用•非衍生•保持署名 | Creative Commons BY-NC-ND 4.0

原文链接:https://sjyuan.cc/talking-about-is-a-in-ood/

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容