Обойти последний метод в подклассе в Java?

В данный момент я изучаю наследование, и меня немного смущают ограничения, которые накладывают объявление метода final в суперклассе на подклассы. Скажем, у меня есть суперкласс BankAccount с методом вывода, который требует вывода пароля пользователя и суммы и устанавливает баланс счета на (баланс - сумма). Я хотел бы объявить этот метод окончательным, чтобы другие подклассы не могли его переопределить и позволить клиенту снимать деньги без изменения баланса счета.

public final void withdraw(double amount, String pass) {
    if (checkPassword(pass) && getBalance() >= amount;) {
        setBalance(getBalance() - amount);
    } else {
        System.out.println("Rejected.");
    }
}

Я хочу, чтобы что-то подобное не было разрешено:

public void withdraw(double amount, String pass) {

}

Однако некоторые банковские счета допускают овердрафты, которые также необходимо учитывать при снятии средств. Теперь, если у меня есть подкласс BankAccountOverdraft, унаследованный метод вывода является окончательным, поэтому я не смогу изменить какие-либо его части. Но все же Я ДОЛЖЕН учитывать лимит овердрафта в подклассе? Как я могу это сделать?

    public void withdraw(double amount, String pass) {
    if (checkPassword(pass) && getOverDraftLimit() + getBalance() >= amount) {
        setBalance(getBalance() - amount);
    } else {
        System.out.println("Rejected.");
    }
}

person jack    schedule 14.11.2015    source источник
comment
Вы сами ответили? Реализуйте метод getOverDraftLimit в суперклассе со значением по умолчанию и переопределите его в подклассах, которые определяют ограничение.   -  person dunni    schedule 14.11.2015
comment
Почему бы просто не добавить атрибут allowedOverdraft к BankAccount и не игнорировать BankAccountOverdraft? Другим решением было бы определить метод getAllowedOverdraft() в BankAccount (как не окончательный), который возвращает 0. Вы можете перезаписать этот метод в BankAccountOverdraft, чтобы вернуть то, что вы хотите, и использовать его в withdraw(...) методе BankAccount.   -  person Turing85    schedule 14.11.2015
comment
Между прочим, в качестве обучающего упражнения это нормально, но на самом деле такого рода бизнес-ограничения (с реальными деньгами на кону), вероятно, никогда не будут реализованы через наследование. Обычно у вас есть четкие бизнес-правила.   -  person biziclop    schedule 14.11.2015
comment
Только не объявляйте свой метод окончательным, если он не является окончательным. Просто переопределите его только там, где это необходимо (в BankAccountOverdraft).   -  person Jean-François Savard    schedule 14.11.2015
comment
@ Jean-FrançoisSavard Но тогда подкласс может случайно пропустить проверку пароля или что-то в этом роде.   -  person biziclop    schedule 14.11.2015
comment
Но разве реализация getOverDraftLimit в BankAccount make с BankAccountOverdraft и использованием наследования в целом бессмысленна? Я не хочу, чтобы у всех подклассов BankAccount был овердрафт ...   -  person jack    schedule 14.11.2015
comment
@jack Для начала вам нужно найти общий язык. Вы не можете использовать некоторые атрибуты / методы в классе без того, чтобы класс знал об этих атрибутах / методах (например, если только некоторые подклассы определяют эти атрибуты / методы).   -  person Turing85    schedule 14.11.2015
comment
@jack Это не делает наследование бессмысленным, а просто делает его непригодным для этой задачи в той форме, в которой вы написали свой пример.   -  person biziclop    schedule 14.11.2015
comment
Вы можете определить, например, подкласс, который делает getOverDraftLimit final со значением 0, а затем ограничивает клиентов только его подклассами. Это не бессмысленно, но наследование в то же время не лучший инструмент для обеспечения соблюдения правил. См. Отражение.   -  person zapl    schedule 14.11.2015
comment
Хорошо, это немного проясняет, спасибо за вашу помощь, ребята!   -  person jack    schedule 14.11.2015


Ответы (3)


Возникает вопрос: отличается ли порядок вывода средств для счета с овердрафтом? Нет, на самом деле это не так, процесс тот же: вы проверяете пароль, вы проверяете, достаточно ли средств, вы списываете деньги со счета.

Отличие - это шаг check there are enough funds. Итак, ваша абстракция должна это отражать, как и в случае с checkPassword().

public final void withdraw(double amount, String pass) {
 if (checkPassword(pass) && checkFundsAvailable(amount)) {
    setBalance(getBalance() - amount);
 } else {
    System.out.println("Rejected.");
 }
}

protected boolean checkFundsAvailable(double amount) {
  return amount <= getBalance();
}

И когда у вас может быть овердрафт, вы отменяете его с помощью:

protected boolean checkFundsAvailable(double amount) {
  return amount <= getBalance() + overdraftLimit;
}

Таким образом, ваш суперкласс не должен знать о лимитах овердрафта или о чем-то еще. Вы можете реализовать заблокированную учетную запись в качестве ее подкласса, который отклоняет все запросы на снятие средств, или вы можете поместить любую другую логику в checkFundsAvailable().

P.s .: Несмотря на все вышесказанное, есть очень веская причина не подходить к этой проблеме с наследованием (если бы это была настоящая проблема, а не просто упражнение), но она более тонкая. Имея класс BankAccount и подкласс BankAccountOverdraft, вы также заявляете, что учетные записи, которые не допускают овердрафты, никогда не могут быть превращены в учетные записи с возможностью овердрафта, и наоборот. Но реальные банковские счета не ведут себя так, вы можете начать без допустимого овердрафта и согласовать лимит овердрафта позже. Наследование не может выразить это, вам нужно использовать некоторую форму композиции.

person biziclop    schedule 14.11.2015
comment
Хорошее объяснение некоторых подводных камней наследования. - person Jason; 14.11.2015

Другие предлагали изменить ваш дизайн, но важно понимать, почему ваш оригинальный дизайн не работает.

Ваш BankAccount класс ведет себя определенным образом, когда вы пытаетесь получить овердрафт. Такое поведение является частью его неявного API. Класс, который разрешает овердрафты, нарушает этот API, ослабляя постусловие, согласно которому баланс не может быть отрицательным. В некотором смысле это не BankAccount. Следовательно, он не должен быть подклассом BankAccount. Создание класса или методов final - это способ добиться этого в Java.

Как отмечает biziclop, можно использовать наследование, чтобы выразить связь между учетной записью, которая может иметь овердрафт, и учетной записью, которая не может. Но превращение одного из них в родительский нарушает принцип замещения Лискова. Вместо этого заставьте два класса реализовать или расширить общий интерфейс или суперкласс, который не определяет поведение овердрафта. Возможно, суперкласс вообще не должен включать метод withdraw. Избегайте написания одного класса, который работает в обоих направлениях. Если клиенту вашего API необходимо протестировать поведение и возможности объекта, вы обошли строгую типизацию, которая является одной из самых сильных сторон Java.

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

person Kevin Krumwiede    schedule 14.11.2015
comment
Я полностью согласен с этим, но следует отметить, что в этом случае постусловие не обязательно должно быть универсальным. Если ваша модель говорит, что постусловие может быть различным для разных учетных записей, то это можно выразить через наследование. Я бы дал отдельный +1 за комментарий к обучению, если бы мог, 90% примеров наследования, с которыми я сталкивался в моем образовании, были недействительными. - person biziclop; 14.11.2015

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

Вы можете разделить withdraw на два отдельных метода. super метод может быть помечен final, и у вас может быть второй protected метод, который не помечен final, который выполняет фактическое снятие средств. Это заставляет проверку произойти, но позволяет фактическому поведению отказа быть динамичным.

public final void withdraw(double amount, String pass) {

    if (checkPassword(pass)) {
        withdraw(amount);
    } else {
        System.out.println("Rejected.");
    }
}

protected void withdraw(double amount) {

  if (getBalance() >= amount) {
    setBalance(getBalance() - amount);
  } else {
    System.out.println("Rejected.");
  }
}

В производственном классе ...

@Override
protected void withdraw(double amount) {

  if (getOverDraftLimit() + getBalance() >= amount) {
    setBalance(getBalance() - amount);
  } else {
    System.out.println("Rejected.");
  }
}

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

person Jason    schedule 14.11.2015