【软件重构】【代码坏味道】详解滥用类的继承体系

举报
huahua.Dr 发表于 2022/09/28 10:30:32 2022/09/28
【摘要】 一、定义:子类应该继承超类的函数和数据,但有时子类却不想使用或者不需要继承超类的部分函数及数据,只想使用部分功能和字段。出现了不同的子类,容易存在混乱,滥用的情况。这就是继承的两面性。二、问题影响:1)由于超类基本都是抽象的,包含了没有具体实现的接口,而子类又不想实现这些接口,这就违背了继承原则2)子类只使用超类的部分功能和字段,那么他们之间的继承体系设计就存在不合理。3)如果存在这种坏味道...

一、定义:

子类应该继承超类的函数和数据,但有时子类却不想使用或者不需要继承超类的部分函数及数据,只想使用部分功能和字段。出现了不同的子类,容易存在混乱,滥用的情况。这就是继承的两面性。

二、问题影响:

1)由于超类基本都是抽象的,包含了没有具体实现的接口,而子类又不想实现这些接口,这就违背了继承原则

2)子类只使用超类的部分功能和字段,那么他们之间的继承体系设计就存在不合理。

3)如果存在这种坏味道,那么就意味着,子类或者父类会相互受到影响,很显然违反了LSP(里氏替换原则):子类必须能够替换掉他们的父类。即父类出现的地方就可以使用子类来代替,而且不会出现任何错误或者异常,常见的影响有:

  • 一个子类继承了父类但是子类中的某个方法抛出了异常,而父类中该方法并没有抛出异常。
  • 一个子类继承了父类,但是子类修改了某个方法的内部行为。
  • 调用者只能通过子类而不能通过父类来访问类。
  • 无意义的继承,子类并不是父类的一个实例。

(3)如何解决继承体系的问题

  • 创建兄弟类。为子类创建兄弟类,使用函数下移或者字段下移,将所有用不到的函数和字段推给兄弟类。超类只留公共的函数
  • 函数/字段下移:就是将超类中的函数/字段复制声明到每一个需要测函数的子类中,不需要的子类则不用复制声明,然后删除超类中的该函数/字段
  • 修改子类。如果子类确实不需要实现超类的接口,并且超类函数也不能移动,那么子类想复用超类的部分功能,只能使用委托类取代子类或者超类,将子类与超类划清界限。
  • 以委托(组合)取代子类/超类。如果两个超类与多个子类存在非常紧密的关系,修改超类都很可能破坏所有子类。可以使用委托解决这个问题,对应不同的变化原因,可以委托给不同的类,委托是对象之间常规的关系,与继承关系相比,使用委托关系时接口更清晰,耦合更少,委托即是组合,因此存在一个原则:对象组合由于继承;但是还是先建议使用继承,如果继承遇到这种问题后,才考虑使用委托重构。

看一个具体的例子:

import java.time.DayOfWeek;

import java.time.LocalDate;

 

public class refuseSmall {

/*

* 需求:火车票与优惠活动的业务需求

* 1.优惠活动:优惠时间,主题、基础价格

* 2.普通票:优惠时间是否过期、获取票价、退款

* 3.VIP票:优惠时间是否过期,获取票价,是否存在额外的活动

* */

}

 

// 优惠活动类

 class Activity {

    private final ActivityType type;//优惠主题

 

    private final LocalDate date;//优惠时间

 

    private final int price;//基础价格

 

    public Activity(ActivityType type, LocalDate date, int price) {

        this.type = type;

        this.date = date;

        this.price = price;

    }

 

    public LocalDate getDate() {

        return date;

    }

 

    public int getPrice() {

        return price;

    }

 

    public ActivityType getType() {

        return type;

    }

 

    enum ActivityType {WORKSHOP, TDD, SESSION}

}

 

// 普通票类

class Ticket {

 

    public final Activity activity;

 

    public Ticket(Activity activity) {

        this.activity = activity;

    }

    // 是否过期

    public boolean isSession() {

        return Activity.ActivityType.SESSION.equals(activity.getType()) && isWorkday();

    }

 

    //是否是工作时间

    private boolean isWorkday() {

        return !activity.getDate().getDayOfWeek().equals(DayOfWeek.SATURDAY)

                && !activity.getDate().getDayOfWeek().equals(DayOfWeek.SUNDAY);

    }

 

    //获取基础价格

    public int getPrice() {

        return DayOfWeek.FRIDAY.equals(activity.getDate().getDayOfWeek())

                ? activity.getPrice() * 2

                : activity.getPrice();

    }

 

    //退款

    public int refund() {

        return getPrice();

    }

}

 

// vip票

class VIPTicket extends Ticket {

 

    private final boolean supportExtensionalActivities;

 

    public VIPTicket(Activity activity, boolean supportExtensionalActivities) {

        super(activity);

        this.supportExtensionalActivities = supportExtensionalActivities;

    }

 

    public boolean isSession() {

        return Activity.ActivityType.SESSION.equals(activity.getType());

    }

 

    public int getPrice() {

        return super.getPrice() + 100;

    }

 

    // 查询是够存在额外的优惠活动

    public boolean hasExtensionalActivities() {

        return Activity.ActivityType.TDD.equals(activity.getType()) || supportExtensionalActivities;

    }

}

存在的问题:

  • getPrice()方法不但覆写父类的方法并且并且还还调用了父类的getPrice()方法。虽然当前的结果复用的getPrice()方法没有什么问题,但是当当Ticket类上getPrice()的内部逻辑变化时会影响到VIPTicket子类。
  • VIPTicket提供了hasExtensionalActivities()方法,但是父类并没有该方法。
  • Ticket提供了refund()退款功能,而VIPTicket业务中并不需要该功能,但是由于VIPTicket继承了Ticket,所以也拥有了refund()方法。这使得代码并没有按照本意来揭示业务意图。

解决方案:

1)整理继承关系,抽取基类,创建兄弟子类

创建一个父类BasicTicket,它提供了公共的属性和方法,Ticket和VIPTicket成为兄弟子类,他们提供各自需要的方法。

2)使用组合取代继承,实现松耦合

3)使用代理取代继承

将不同的变化原因委托给不同的类。委托是类之间的常规关系,使用委托接口更加清晰,耦合度更低。

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。