Анализ рефакторинга 21: отказ в завещании

Java
Анализ рефакторинга 21: отказ в завещании

Отношения между подклассами и суперклассами начинаются с простого, но со временем могут становиться все более и более сложными. Подкласс обычно должен тесно зависеть от своего суперкласса, но иногда это излишне.

Это двойственность наследования, давайте посмотрим на Code Smell, который наследует возможный код.

01 Воспроизведение сцены

Описание требования

这是关于活动(Activity)和票(Ticket)的业务需求:

活动的主题(ActityType): session | workshop | read | TDD
活动(Activity)包含属性:日期、主题、基础价格
票有两种:普通票(Ticket)、VIP票(VIPTicket)

普通票(Ticket)的业务描述:
(1)是否有Session活动:如果主题是session且活动日期是工作日则返回true,否则返回false。
(2)获得票价:如果是如果周一到周四票价=原价,如果周五返回则票价=原价x2
(3)退款:活动开始前可以进行退款。

VIP票(VIPTicket)的业务需求:
(1)是否有Session活动:如果主题是session则返回true,否则返回false。
(2)获得票价:票价 = 如果是如果周一到周四票价=原价+100,如果周五返回则票价=原价x2+100
(3)是否有附加活动:如果活动主题为TDD或者制定了附加活动则返回true,否则返回false。

Основываясь на приведенном выше бизнес-требовании, вот фрагмент кода с запахом «отклоненного наследства» следующим образом:

Activity.java

@Getter
public 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 enum ActivityType {WORKSHOP, TDD, SESSION}
}

Ticket.java

package com.page.refactoring;

import java.time.DayOfWeek;

public class Ticket {

    private 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();
    }
}

VIPTicket.java

public 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;
    }
}

«Отклоненное завещание»Code SmellКодовый адрес:git lab.com/cypress/горячий воздух…

02 Проблема в коде выше

В приведенном выше коде Ticket и VIPTicket используют наследование. Первое наследование является ценным механизмом для размещения общих данных и поведения в родительских классах, при этом каждый подкласс переопределяет некоторые функции по мере необходимости. В большинстве случаев желаемый эффект может быть достигнут без каких-либо проблем. Однако приведенный выше код имеет следующие проблемы при использовании наследования.

VIPTicket наследует Ticket.Хотя VIPTicket повторно использует атрибуты и некоторые методы Ticket, это вызывает следующие проблемы в коде:

Метод getPrice() не только переопределяет метод суперкласса, но также вызывает метод суперкласса getPrice(). Хотя с методом getPrice() для повторного использования текущего результата проблем нет, при изменении внутренней логики getPrice() в классе Ticket это повлияет на подкласс VIPTicket.

VIPTicket предоставляет метод hasExtensionalActivities(), но родительский класс не имеет этого метода.

Билет предоставляет функцию возврата ()) Это делает код не раскрывать бизнес-намерение, как предполагалось.

Это ясное нарушение ЛСП (принцип замены Рихтера). Твердые принципы, которые мы часто используем, lsp (принцип замены Рихтера): подкласс должен иметь возможность заменить их родитель. Именно здесь появляется родительский класс, который вы можете использовать вместо подкласса, и без каких-либо ошибок или исключений.

В дополнение к приведенному выше коду, проблемы, которые часто возникают при наследовании:

  • Подкласс наследует суперкласс, но метод в подклассе вызывает исключение, но метод в суперклассе не генерирует исключение.
  • Подкласс наследует суперкласс, но подкласс изменяет внутреннее поведение метода.
  • Вызывающий может получить доступ к классу только через подкласс, но не через родительский класс.
  • Бессмысленное наследование, подкласс не является экземпляром родительского класса.

03 Действия и льготы для «отклоненных наследств»

Целью первого рефакторинга приведенного выше кода является: 1, код может раскрыть бизнес-намерение, 2, улучшить тестируемость (один и тот же метод не нужно беспокоиться о другом контексте).

1. Реорганизовать отношения наследования.

Как показано на рисунке ниже, создайте родительский класс BasicTicket, который предоставляет общедоступные свойства и методы.Ticket и VIPTicket становятся родственными подклассами и предоставляют нужные им методы.

重新整理继承关系

Рефакторинг кода: https://gitlab.com/tengbai/refactoring/tree/21-refused-bequest-rebuild-mapping

2. Композиция лучше, чем наследование

Во многих обсуждениях упоминается использование композиции интерфейса вместо наследования. На приведенной ниже диаграмме показано использование композиции интерфейса для решения проблемы «отвергнутого наследия», с которой мы столкнулись выше.

接口组合优于继承

Код реконструкции: https://gitlab.com/tengbai/refactoring/tree/21-refused-bequest-refactoring-with-interface

3. Используйте прокси вместо наследования

Делегируйте разные причины изменений разным классам. Делегат — это общая связь между классами, использование интерфейса делегата более понятно и менее связано.

Использование делегирования вместо наследования в приведенном выше примере является самой простой модификацией. Как показано ниже:

使用委托代替继承

Рефакторинг кода: https://gitlab.com/tengbai/refactoring/tree/21-refused-bequest-refactoring-with-delegation

04 Нужен ли рефакторинг «отклоненного завещания»?

Не совсем. Рефакторинг «отклоненного устаревшего» кода или нет, зависит от выгоды.

1. Иногда пост "отвергнутое наследство" создает не какие-то новые типы, иногда не описанные в деле, а чисто технические реализации. Например, в приведенном выше примере вместо наследования используется композиция интерфейса. Простые выражения намерений легче понять, чем очень абстрактные выражения кода.

2. Если рефакторинг «решенной наследственной» проблемы приведет к большому количеству повторяющихся классов, то представьте себе новые методы рефакторинга.

3. При чтении исходного кода иногда обнаруживается, что в исходном коде присутствует "отклоненный легаси" код Smell. Причина, по которой автор его сохраняет, вероятно, в том, что его рефакторинг внесет много изменений, а ввод и выход не не высокий. В «Рефакторинге» автор также часто использует наследование, и в большинстве случаев можно добиться желаемого эффекта, если его модифицировать позже, это отношение наследования будет рефакторингом. Всегда проводите рефакторинг и сохраняйте простой дизайн кода.

05 Проблемы, которые могут быть вызваны наследством

1. Подкласс может наследовать только один родительский класс. Может быть несколько причин такого поведения, но наследование может обрабатывать изменения только в одном направлении. 2. Наследование вводит очень тесную связь между классами. Любая модификация родительского класса может повлиять на поведение дочернего класса. Следовательно, при работе с кодом с положительными отношениями наследования необходимо полностью понимать отношения между родительскими классами и подклассами.

Отклоненные завещания — это запах кода, в котором может произойти наследование. Запах, который часто возникает при наследовании, включает:

  • отклоненное завещание
  • чрезмерная теснота
  • Ленивый класс

Эта статья будет посвященаотклоненное завещаниеПо этому поводу неуместная теснота и ленивые занятия будут объяснены в следующей статье.

Статья организована не по порядку Запаха в "Рефакторинге", а напрямую "Отказ-Завещание". Некоторый другой код и контент Smell будут разобраны позже.

Ссылаться на

01Первое издание «Рефакторинга»

02 «Рефакторинг», второе издание

03 "Руководство по рефакторингу"