Рубрики
Без рубрики

ТВЕРДОЕ ТЕЛО: Принцип замещения Лискова

Это продолжение серии SOLID principles. Принцип замещения Лискова является наиболее распространенным… С тегами java, ооп, программирование, архитектура.

Это продолжение ТВЕРДЫЕ принципы серии.

Принцип замещения Лискова – самый технический принцип из всех. Однако именно он в наибольшей степени помогает разрабатывать несвязанные приложения, что является основой проектирования повторно используемых компонентов.

Барбара Лисков определила этот принцип следующим образом:

Пусть Φ(x) – свойство, доказуемое относительно объектов x типа T. Тогда Φ(y) должно быть истинным для объектов y типа S, где S – подтип T”

Определение, данное Лисковым, основано на концепции проектирования по контракту (DBC), определенной Бертраном Мейером. Контракт, который определяется предварительными условиями, инвариантами и постусловиями:

  • Подпрограмма может ожидать, что при вводе любым клиентским модулем, который ее вызывает, будет гарантировано определенное условие: предварительное условие подпрограмм. Это является обязательством для клиента и выгодой для поставщика, поскольку освобождает его от необходимости рассматривать дела, выходящие за рамки предварительного условия.
  • Процедура может гарантировать определенное свойство при выходе: обычное постусловие – обязательство для поставщика и выгода для клиента.
  • Поддерживать определенное свойство, принимаемое при входе и гарантируемое при выходе: инвариант класса.

Концепция контракта и реализации является основой для наследования и полиморфизма в объектно-ориентированном программировании.

В 1996 году Роберт К. Мартин переосмыслил концепцию, данную Лисковым, следующим образом:

Функции, использующие указатели ссылок на базовые классы, должны иметь возможность использовать объекты производных классов, не зная об этом.

Переопределение, данное Бобом Мартином, помогло упростить концепцию, реализованную Лисковым много лет назад, и ее принятие разработчиками.

Нарушение принципа подстановки Лискова

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

Вы реализуете следующий абстрактный класс в качестве основы вашей системы.

public abstract class BankAccount {

    /**
     * In charge of depositing a specific amount into the account.
     * @param amount            Dollar ammount.
     */
    public abstract void deposit(double amount);

    /**
     * In charge of withdrawing a specific amount from the account.
     * @param amount            Dollar amount.
     * @return                  Boolean result.
     */
    public abstract boolean withdraw(double amount);
}

Этот абстрактный класс определяет обязательство для любого производного класса переопределять любой абстрактный метод, определенный в классе BankAccount. Это означает, что базовые и премиум-аккаунты должны переопределять способ пополнения и вывода средств.

public class BasicAccount extends BankAccount {

    private double balance;

    @Override
    public void deposit(double amount) {
        this.balance += amount;
    }

    @Override
    public boolean withdraw(double amount) {
        if(this.balance < amount)
            return false;
        else{
            this.balance -= amount;
            return true;
        }       
    }
}
public class PremiumAccount extends BankAccount {

    private double balance;
    private int preferencePoints;

    @Override
    public void deposit(double amount) {
        this.balance += amount;
        accumulatePreferencePoints();
    }

    @Override
    public boolean withdraw(double amount) {
         if(this.balance < amount)
            return false;
        else{
            this.balance -= amount;
            accumulatePreferencePoints();
            return true;
        }
    }

    private void accumulatePreferencePoints(){
        this.preferencePoints++;
    }

}

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

Все базовые и премиум-аккаунты имеют скидку в размере 25,00 долларов США в год на административные расходы. Для реализации этой политики вы определили следующий класс:

public class WithdrawalService {

    public static final double ADMINISTRATIVE_EXPENSES_CHARGE = 25.00;

    public void cargarDebitarCuentas(){

        BankAccount basiAcct = new BasicAccount();
        basiAcct.deposit(100.00);

        BankAccount premiumAcct = new PremiumAccount();
        premiumAcct.deposit(200.00);

        List accounts = new ArrayList();

        accounts.add(basiAcct);
        accounts.add(premiumAcct);

        debitAdministrativeExpenses(accounts);

    }

    private void debitAdministrativeExpenses(List accounts){
        accounts.stream()
                .forEach(account -> account.withdraw(WithdrawalService.ADMINISTRATIVE_EXPENSES_CHARGE));
    }
}

На втором этапе вашего проекта ваш начальник просит вас внедрить долгосрочные счета в вашу систему управления банковскими счетами. Различия между долгосрочными счетами и базовыми/премиум-счетами заключаются в следующем:

  • Долгосрочные счета освобождены от административных расходов.
  • Долгосрочные счета не позволяют снимать средства. Если клиент хочет снять какую-либо сумму со своего счета, это должно быть сделано с помощью другого процесса.

Как разработчик, отвечающий за систему учетных записей, вы решили расширить класс BankAccount для долгосрочных счетов.

public class LongTermAccount extends BankAccount {

    private double balance;

    @Override
    public void deposit(double amount) {
        this.balance += amount;
    }

    @Override
    public boolean withdraw(double amount) {
        throw new UnsupportedOperationException("Not supported yet."); 
    }
}

В этой части нарушение принципа подстановки Лискова очевидно. Вы не можете расширить класс BankAccount на Долгосрочном счете без переопределения метода вывода средств. Однако долгосрочные счета не позволяют выводить средства в соответствии с требованиями вашего проекта.

У вас есть следующие два варианта решения этой проблемы:

  • Вы можете переопределить метод вывода как пустой метод или вызвать исключение UnsupportedOperationException. Однако объекты банковского счета не будут полностью взаимозаменяемы с объектами Долгосрочного счета, потому что, если мы попытаемся выполнить метод вывода средств, мы получим исключение. В качестве решения этой проблемы мы можем обусловить метод дебетовых административных расходов, чтобы мы могли пропустить объекты LongTermAccount, но это нарушило бы принцип Open/Closed. Например:
private void debitAdministrativeExpenses(List accounts){

        for(BankAccount account : accounts){
            if(account instanceof LongTermAccount)
                continue;
            else
                account.withdraw(ADMINISTRATIVE_EXPENSES_CHARGE);
        }
    }
  • Вы можете сделать свой код совместимым с принципом подстановки Лискова.

Реализация Принципа Замещения Лискова

Основная проблема со структурой банковского счета заключается в том, что долгосрочный счет не является обычным банковским счетом, по крайней мере, не является типом, определенным в абстрактном классе BankAccount. Существует простой тест в области абдуктивных рассуждений, который можно использовать для проверки того, является ли класс подтипом типа “X”. Тест на утку гласит: “Если она выглядит как утка, плавает как утка и крякает как утка, то, вероятно, это утка”. Долгосрочный счет выглядит как обычный банковский счет, но ведет себя не так, как обычный. Чтобы решить эту проблему, мы должны изменить текущую структуру классов.

Чтобы сделать наш код совместимым с LSP, мы внесем следующие изменения:

  • Действие по внесению депозита будет разрешено для всех типов банковских счетов.
  • Действие по выводу средств будет разрешено только на банковских счетах basic и premium.
  • Мы определим абстрактный банковский счет для всех типов счетов. Этот абстрактный класс будет определять только один метод – метод deposit.
  • Мы расширим Банковский счет абстрактным классом Выводимого счета, который будет определять метод дебетования.
  • Базовые и премиум-аккаунты расширят абстрактный класс счета с возможностью вывода средств, в то время как долгосрочный аккаунт расширит абстрактный класс BankAccount.

Абстрактный класс BankAccount будет определять метод внесения депозита.

public abstract class BankAccount {

    /**
     * In charge of depositing a specific amount into the account.
     * @param amount            Dollar ammount.
     */
    public abstract void deposit(double amount);
}

Абстрактный класс счета с возможностью вывода средств будет определять способ вывода средств.

public abstract class WithdrawableAccount extends BankAccount {

    /**
     * In charge of withdrawing a specific amount from the account.
     * @param amount            Dollar amount.
     * @return                  Boolean result.
     */
    public abstract boolean withdraw(double amount);
}

Классы счетов basic и premium расширят класс счетов с возможностью вывода средств, который расширяет класс BankAccount. Это вложенное наследование позволяет базовым/премиум-аккаунтам иметь оба способа пополнения и вывода средств.

public class BasicAccount extends WithdrawableAccount {

    private double balance;

    @Override
    public void deposit(double amount) {
        this.balance += amount;
    }

    @Override
    public boolean withdraw(double monto) {
        if(this.balance < monto)
            return false;
        else{
            this.balance -= monto;
            return true;
        }       
    }   
}
public class PremiumAccount extends WithdrawableAccount {

    private double balance;
    private int preferencePoints;

    @Override
    public void deposit(double monto) {
        this.balance += monto;
        accumulatePreferencePoints();
    }

    @Override
    public boolean withdraw(double monto) {
         if(this.balance < monto)
            return false;
        else{
            this.balance -= monto;
            accumulatePreferencePoints();
            return true;
        }
    }

    private void accumulatePreferencePoints(){
        this.preferencePoints++;
    }
}

Класс обслуживания вывода средств реализован с использованием только типов или подтипов Withdrawable Account.

public class WithdrawableService {

    public static final double ADMINISTRATIVE_EXPENSES_CHARGE = 25.00;

    public void cargarDebitarCuentas(){

        WithdrawableAccount basicAcct = new BasicAccount();
        basicAcct.deposit(100.00);

        WithdrawableAccount premiumAcct = new PremiumAccount();
        premiumAcct.deposit(200.00);

        List accounts = new ArrayList();

        accounts.add(basicAcct);
        accounts.add(premiumAcct);

        debitarGastosAdmon(accounts);

    }

    private void debitarGastosAdmon(List accounts){
        accounts.stream()
                .forEach(account -> account.withdraw(WithdrawableService.ADMINISTRATIVE_EXPENSES_CHARGE));
    }
}

Изменения, внесенные в структуру классов, гарантируют, что наш код соответствует стандарту LSB. Теперь нам не требуется реализовывать метод вывода средств в классе Долгосрочных счетов. Кроме того, мы обмениваем объекты Выводимого счета на любой подтип этого абстрактного класса. Структура классов также соответствует OCP, потому что, если бы мы добавили другой тип банковского счета, который не разрешает вывод средств, нам не нужно было бы изменять текущий код только для расширения типа BankAccount.

Важность принципа замещения Лискова

LSP позволяет нам выявлять неправильные области обобщения, выполненные на этапе проектирования, и исправлять их. Принцип подстановки Лискова является основополагающим при разработке концепции внедрения зависимостей, которая широко используется в Java Enterprise Edition и Spring Framework.

Вы можете воспользоваться следующими советами, если хотите легко обнаружить нарушения принципа подстановки Лискова:

Существует нарушение LSP, если вы вводите условие, используя тип вашего объекта, как показано выше в примере примера. Существует нарушение LSP, если вы расширяете абстрактный класс и устанавливаете один из абстрактных методов в качестве пустого метода или создаете не определенное исключение.

Почему бы не использовать интерфейсы вместо абстрактных классов

Интерфейсы и абстрактные классы очень похожи, но это не одно и то же. Абстрактные классы определяют функциональность, которую должны реализовывать подклассы. С другой стороны, интерфейсы определяют функциональность, которая должна быть реализована любым классом, реализующим данный интерфейс.

Например, абстрактные классы определяют, что пони скачет галопом, потому что это тип лошади. Напротив, интерфейс определяет, что “вещь” может “двигаться”, потому что при реализации интерфейса существует соглашение о том, что вещь должна двигаться.

Итак, это поднимает два вопроса:

  • Можем ли мы реализовать пример банковского счета с использованием интерфейсов? да, конечно. Здесь нет неправильного ответа. Вы могли бы реализовать этот пример, используя либо абстрактные классы, либо интерфейсы.

  • Буду ли я по-прежнему нарушать LSP, используя интерфейсы? Что ж, здесь нет правильного ответа. LSP был определен в контексте подтипирования. Однако, я думаю, это больше не актуально. LSP – это не абстрактные классы или интерфейсы, это все о соблюдении контракта.

Если вы хотите узнать больше о LSP, вы можете заглянуть в Блог дяди Боба .

В следующем посте мы поговорим о принципе разделения интерфейса.

Вы можете следовать за мной по Твиттер или Linkedin .

Оригинал: “https://dev.to/victorpinzon1988eng/solid-liskov-substitution-principle-3jel”