Почему ключевое слово 'final' будет полезным?

54

Кажется, что Java обладает способностью объявлять классы, которые не могут быть извлечены целую вечность, и теперь C ++ имеет это тоже. Однако, в свете принципа открытия / закрытия в SOLID, почему это было бы полезно? Для меня finalключевое слово звучит так же, как friendэто - это законно, но если вы используете его, скорее всего, дизайн не так. Пожалуйста, приведите несколько примеров, где невзаимозаменяемый класс будет частью большой архитектуры или шаблона проектирования.

Vorac
источник
43
Как вы думаете, почему класс неправильно спроектирован, если он помечен final? Многие люди (включая меня) считают, что это хороший дизайн - создавать каждый неабстрактный класс final.
Пятнистый
20
Подарите композицию наследованию, и вы можете иметь любой неабстрактный класс final.
Энди
24
Принцип открытия / закрытия в некотором смысле является анахронизмом 20-го века, когда мантра должна была создать иерархию классов, унаследованных от классов, которые, в свою очередь, унаследовали от других классов. Это было хорошо для обучения объектно-ориентированному программированию, но оказалось, что оно создает запутанные, не поддерживаемые беспорядки применительно к реальным задачам. Создать класс, который будет расширяемым, сложно.
Дэвид Хаммен,
34
@DavidArno Не будь смешным. Наследование является непременным условием объектно-ориентированного программирования, и оно далеко не такое сложное или запутанное, как любят проповедовать некоторые чрезмерно догматичные люди. Это инструмент, как и любой другой, и хороший программист знает, как использовать правильный инструмент для работы.
Мейсон Уилер
48
Как выздоравливающий разработчик, я вспоминаю, что final был отличным инструментом для предотвращения проникновения вредоносного поведения в критические разделы. Я также, кажется, вспоминаю, что наследование является мощным инструментом во многих отношениях. Это почти как если бы GASP разные инструменты имеют свои плюсы и минусы , и мы как инженеры должны сбалансировать эти факторы , как мы производим наше программное обеспечение!
CorsiKa

Ответы:

136

final выражает намерение . Он сообщает пользователю о классе, методе или переменной: «Этот элемент не должен изменяться, и если вы хотите изменить его, вы не поняли существующий дизайн».

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

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

Килиан Фот
источник
14
Как и я. Но любой, кто когда-либо писал широко используемый базовый класс (например, фреймворк или медиатеку), знает лучше, чем ожидать, что прикладные программисты будут вести себя вменяемым образом. Они будут подрывать, неправильно использовать и искажать ваше изобретение способами, о которых вы даже не думали, что это возможно, если вы не заблокируете его железной рукояткой.
Килиан Фот
8
@KilianFoth Хорошо, но, если честно, как это ваша проблема, что делают программисты приложений?
coredump
20
@coredump Люди, использующие мою библиотеку, плохо создают плохие системы. Плохие системы порождают плохую репутацию. Пользователи не смогут отличить отличный код Килиана от чудовищно нестабильного приложения Фреда Рэндома. Результат: я проигрываю в программировании кредитов и клиентов. Делать ваш код сложным для неправильного использования - это вопрос долларов и центов.
Килиан Фот
21
Утверждение «Этот элемент не должен изменяться, и если вы хотите изменить его, вы не поняли существующий дизайн». невероятно высокомерен и не тот настрой, который мне хотелось бы видеть в любой библиотеке, с которой я работал. Если бы только у меня было десять центов на каждый раз, когда какая-то нелепо чрезмерно инкапсулированная библиотека не давала мне механизма для изменения какого-то фрагмента внутреннего состояния, которое нужно изменить, потому что автор не смог предвидеть важный вариант использования ...
Мейсон Уилер
31
@JimmyB Практическое правило. Если вы думаете, что знаете, для чего будет использоваться ваше творение, вы уже ошибаетесь. Белл воспринимал телефон как систему Muzak. Kleenex был изобретен с целью более удобного удаления макияжа. Томас Уотсон, президент IBM, однажды сказал: «Я думаю, что существует мировой рынок, возможно, для пяти компьютеров». Представитель Джим Сенсенбреннер, который представил закон «ПАТРИОТ» в 2001 году, официально заявляет, что он был специально предназначен для того, чтобы АНБ не могло делать то, что делает «с полномочиями ПАТРИОТА». И так далее ...
Мейсон Уилер
59

Это позволяет избежать хрупкой проблемы базового класса . Каждый класс поставляется с набором неявных или явных гарантий и инвариантов. Принцип подстановки Лискова требует, чтобы все подтипы этого класса также предоставляли все эти гарантии. Тем не менее, это действительно легко нарушить это, если мы не используем final. Например, давайте проверим пароль:

public class PasswordChecker {
  public boolean passwordIsOk(String password) {
    return password == "s3cret";
  }
}

Если мы допустим переопределение этого класса, одна реализация может заблокировать всех, другая может предоставить всем доступ:

public class OpenDoor extends PasswordChecker {
  public boolean passwordIsOk(String password) {
    return true;
  }
}

Обычно это не так, поскольку подклассы теперь имеют поведение, очень несовместимое с оригиналом. Если мы действительно намерены расширить класс другим поведением, лучше использовать Chain of Responsibility:

PasswordChecker passwordChecker =
  new DefaultPasswordChecker(null);
// or:
PasswordChecker passwordChecker =
  new OpenDoor(null);
// or:
PasswordChecker passwordChecker =
 new DefaultPasswordChecker(
   new OpenDoor(null)
 );

public interface PasswordChecker {
  boolean passwordIsOk(String password);
}

public final class DefaultPasswordChecker implements PasswordChecker {
  private PasswordChecker next;

  public DefaultPasswordChecker(PasswordChecker next) {
    this.next = next;
  }

  @Override
  public boolean passwordIsOk(String password) {
    if ("s3cret".equals(password)) return true;
    if (next != null) return next.passwordIsOk(password);
    return false;
  }
}

public final class OpenDoor implements PasswordChecker {
  private PasswordChecker next;

  public OpenDoor(PasswordChecker next) {
    this.next = next;
  }

  @Override
  public boolean passwordIsOk(String password) {
    return true;
  }
}

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

public class Page {
  ...;

  @Override
  public String toString() {
    PrintWriter out = ...;
    out.print("<!DOCTYPE html>");
    out.print("<html>");

    out.print("<head>");
    out.print("</head>");

    out.print("<body>");
    writeHeader(out);
    writeMainContent(out);
    writeMainFooter(out);
    out.print("</body>");

    out.print("</html>");
    ...
  }

  void writeMainContent(PrintWriter out) {
    out.print("<div class='article'>");
    out.print(htmlEscapedContent);
    out.print("</div>");
  }

  ...
}

Теперь я создаю подкласс, который добавляет немного больше стиля:

class SpiffyPage extends Page {
  ...;


  @Override
  void writeMainContent(PrintWriter out) {
    out.print("<div class='row'>");

    out.print("<div class='col-md-8'>");
    super.writeMainContent(out);
    out.print("</div>");

    out.print("<div class='col-md-4'>");
    out.print("<h4>About the Author</h4>");
    out.print(htmlEscapedAuthorInfo);
    out.print("</div>");

    out.print("</div>");
  }
}

Теперь на мгновение игнорируем, что это не очень хороший способ создания HTML-страниц, что произойдет, если я захочу еще раз изменить макет? Я должен был бы создать SpiffyPageподкласс, который каким-то образом оборачивает этот контент. Здесь мы видим случайное применение шаблона метода шаблона. Методы шаблона - это четко определенные точки расширения в базовом классе, которые предназначены для переопределения.

А что произойдет, если базовый класс изменится? Если содержимое HTML изменяется слишком сильно, это может нарушить компоновку, предоставляемую подклассами. Поэтому не очень безопасно менять базовый класс впоследствии. Это не очевидно, если все ваши классы находятся в одном проекте, но очень заметно, если базовый класс является частью какого-либо опубликованного программного обеспечения, на котором строят другие люди.

Если бы эта стратегия расширения была предназначена, мы могли бы позволить пользователю поменять способ создания каждой части. Либо, для каждого блока может быть стратегия, которая может быть предоставлена ​​извне. Или мы могли бы вкладывать декораторы. Это будет эквивалентно приведенному выше коду, но будет гораздо более явным и более гибким:

Page page = ...;
page.decorateLayout(current -> new SpiffyPageDecorator(current));
print(page.toString());

public interface PageLayout {
  void writePage(PrintWriter out, PageLayout top);
  void writeMainContent(PrintWriter out, PageLayout top);
  ...
}

public final class Page {
  private PageLayout layout = new DefaultPageLayout();

  public void decorateLayout(Function<PageLayout, PageLayout> wrapper) {
    layout = wrapper.apply(layout);
  }

  ...
  @Override public String toString() {
    PrintWriter out = ...;
    layout.writePage(out, layout);
    ...
  }
}

public final class DefaultPageLayout implements PageLayout {
  @Override public void writeLayout(PrintWriter out, PageLayout top) {
    out.print("<!DOCTYPE html>");
    out.print("<html>");

    out.print("<head>");
    out.print("</head>");

    out.print("<body>");
    top.writeHeader(out, top);
    top.writeMainContent(out, top);
    top.writeMainFooter(out, top);
    out.print("</body>");

    out.print("</html>");
  }

  @Override public void writeMainContent(PrintWriter out, PageLayout top) {
    ... /* as above*/
  }
}

public final class SpiffyPageDecorator implements PageLayout {
  private PageLayout inner;

  public SpiffyPageDecorator(PageLayout inner) {
    this.inner = inner;
  }

  @Override
  void writePage(PrintWriter out, PageLayout top) {
    inner.writePage(out, top);
  }

  @Override
  void writeMainContent(PrintWriter out, PageLayout top) {
    ...
    inner.writeMainContent(out, top);
    ...
  }
}

(Дополнительный topпараметр необходим, чтобы убедиться, что вызовы writeMainContentпроходят через вершину цепочки декораторов. Это эмулирует функцию подклассов, называемую открытой рекурсией .)

Если у нас есть несколько декораторов, мы можем теперь смешивать их более свободно.

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

final class Thingies implements Iterable<Thing> {
  private ArrayList<Thing> thingList = new ArrayList<>();

  @Override public Iterator<Thing> iterator() {
    return thingList.iterator();
  }

  public void add(Thing thing) {
    thingList.add(thing);
  }

  ... // custom methods
}

Вместо этого они создали подкласс:

class Thingies extends ArrayList<Thing> {
  ... // custom methods
}

Это внезапно означает, что весь интерфейс ArrayListстал частью нашего интерфейса. Пользователи могут remove()вещи или get()вещи по определенным показателям. Это было задумано таким образом? ХОРОШО. Но часто мы не продумываем все последствия.

Поэтому желательно

  • никогда не extendурок без тщательной мысли.
  • всегда помечайте свои классы как finalисключая, если вы намереваетесь переопределить любой метод.
  • создавать интерфейсы, где вы хотите поменять реализацию, например, для модульного тестирования.

Есть много примеров, когда это «правило» нужно нарушать, но оно обычно ведет вас к хорошему, гибкому дизайну и позволяет избежать ошибок из-за непреднамеренных изменений в базовых классах (или непреднамеренного использования подкласса в качестве экземпляра базового класса. ).

Некоторые языки имеют более строгие механизмы обеспечения соблюдения:

  • Все методы являются окончательными по умолчанию и должны быть явно помечены как virtual
  • Они обеспечивают частное наследование, которое не наследует интерфейс, а только реализацию.
  • Они требуют, чтобы методы базового класса были помечены как виртуальные, и также требуется пометить все переопределения. Это позволяет избежать проблем, когда подкласс определяет новый метод, но метод с той же сигнатурой был позже добавлен в базовый класс, но не предназначен для использования в качестве виртуального.
Амон
источник
3
Вы заслуживаете как минимум +100 за упоминание «проблемы хрупкого базового класса». :)
Дэвид Арно
6
Меня не убеждают высказанные здесь замечания. Да, хрупкий базовый класс - это проблема, но final не решает все проблемы с изменением реализации. Ваш первый пример плох, потому что вы предполагаете, что знаете все возможные варианты использования PasswordChecker («заблокировать всех или разрешить всем доступ ... не в порядке» - говорит кто?). Ваш последний список «поэтому рекомендуется ...» действительно плох - вы в основном выступаете за то, чтобы ничего не расширять и пометить все как окончательное, что полностью исключает полезность ООП, наследования и повторного использования кода.
adelphus
4
Ваш первый пример не является примером проблемы хрупкого базового класса. В проблеме хрупкого базового класса изменения в базовом классе разрушают подкласс. Но в этом примере ваш подкласс не следует контракту подкласса. Это две разные проблемы. (Кроме того, вполне разумно, что при определенных обстоятельствах вы можете отключить проверку пароля (скажем, для разработки))
Winston Ewert
5
«Это позволяет избежать проблем с хрупким базовым классом» - так же, как убийство себя позволяет избежать голода.
user253751
9
@immibis, это больше похоже на отказ от еды, чтобы избежать попадания еды в пищу. Конечно, никогда не ешь будет проблемой. Но есть только в тех местах, которым вы доверяете, имеет большой смысл.
Уинстон Эверт
33

Я удивлен тем, что еще никто не упомянул « Эффективную Java», 2-е издание Джошуа Блоха (которое должно быть обязательно прочитано, по крайней мере, для каждого разработчика Java). Пункт 17 в книге обсуждает это в деталях, и называется: « Дизайн и документ для наследования или иначе запретить ».

Я не буду повторять все полезные советы в книге, но эти конкретные пункты кажутся актуальными:

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

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

Даниэль Приден
источник
21

Одна из причин, которая finalможет быть полезна, заключается в том, что она гарантирует, что вы не можете создать подкласс класса таким образом, который нарушил бы контракт родительского класса. Такое создание подклассов было бы нарушением SOLID (больше всего "L"), и создание класса finalпредотвращает это.

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

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

Final также иногда помогает предотвратить простые ошибки, такие как повторное использование одной и той же переменной для двух вещей в методе и т. Д. В Scala рекомендуется использовать только то, valчто примерно соответствует конечным переменным в Java, и фактически любое использование varили неконечная переменная рассматривается с подозрением.

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

Михал Космульский
источник
6
Наконец, компиляторы могут, по крайней мере, теоретически => Я лично рассмотрел этап девиртуализации Кланга и подтверждаю, что он используется на практике.
Матье М.
Но не может ли компилятор заранее сказать, что никто не переопределяет класс или метод независимо от того, помечен ли он как финальный или нет?
JesseTG
3
@JesseTG Если у него есть доступ ко всему коду одновременно, возможно. А как насчет компиляции отдельных файлов?
Восстановите Монику
3
@JesseTG devirtualization или (мономорфное / полиморфное) встроенное кэширование является распространенной техникой в ​​JIT-компиляторах, поскольку система знает, какие классы загружены в настоящее время, и может деоптимизировать код, если предположение об отсутствии переопределяющих методов окажется ложным. Однако преждевременный компилятор не может. Когда я компилирую один класс Java и код, который использует этот класс, я могу позже скомпилировать подкласс и передать экземпляр потребляющему коду. Самый простой способ - добавить еще одну банку в начало пути к классам. Компилятор не может знать обо всем этом, так как это происходит во время выполнения.
Амон
5
@MTilsted Вы можете предположить, что строки являются неизменяемыми, так как мутирующие строки с помощью отражения могут быть запрещены с помощью a SecurityManager. Большинство программ не используют его, но они также не запускают ненадежный код. +++ Вы должны предположить, что строки неизменны, так как в противном случае вы получаете нулевую безопасность и кучу произвольных ошибок в качестве бонуса. Любой программист, считающий, что строки Java могут измениться в производительном коде, безусловно, безумен.
Maaartinus
7

Вторая причина - производительность . Первая причина заключается в том, что некоторые классы имеют важные поведения или состояния, которые не должны изменяться, чтобы позволить системе работать. Например, если у меня есть класс «PasswordCheck» и для его создания я нанял команду экспертов по безопасности, и этот класс связывается с сотнями банкоматов с хорошо изученными и определенными процедурами. Разрешить новому наемному парню, только что закончившему университет, создать класс «TrustMePasswordCheck», который расширяет вышеуказанный класс, может быть очень вредным для моей системы; эти методы не должны быть переопределены, вот и все.

JoulinRouge
источник
1
bancomat = ATM: en.wikipedia.org/wiki/Cash_machine
tar
JVM достаточно умен, чтобы рассматривать классы как финальные, если у них нет подклассов, даже если они не объявлены как финальные.
user253751
Понижено, потому что претензия на «производительность» неоднократно опровергалась.
Пол Смит
7

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

Если кто-то хочет подкласса моего класса, я хочу полностью отрицать любую ответственность за то, что происходит. Я достигаю этого, делая класс "финальным". Если вы хотите создать подкласс, помните, что я не учитывал подклассы во время написания класса. Таким образом, вы должны взять исходный код класса, удалить «финал», и с этого момента все, что происходит, является вашей полной ответственностью .

Вы думаете, что это «не объектно-ориентированный»? Мне заплатили, чтобы сделать класс, который делает то, что он должен делать. Никто не заплатил мне за создание класса, который мог бы быть подклассом. Если вам платят за повторное использование моего класса, вы можете это сделать. Начните с удаления "окончательного" ключевого слова.

(Кроме этого, «final» часто допускает существенную оптимизацию. Например, в Swift «final» для открытого класса или для метода открытого класса означает, что компилятор может полностью знать, какой код будет выполнять вызов метода, и может заменить динамическую диспетчеризацию статической диспетчеризацией (крошечная выгода) и часто заменять статическую диспетчеризацию на встраивание (возможно, огромная выгода)).

adelphus: Что так трудно понять о том, «если вы хотите создать подкласс, взять исходный код, удалить« последний », и это ваша ответственность»? «окончательный» означает «справедливое предупреждение».

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

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

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

gnasher729
источник
4
-1 Этот ответ описывает все, что не так с разработчиками программного обеспечения. Если кто-то хочет повторно использовать ваш класс с помощью подклассов, пусть они. Почему вы несете ответственность за то, как они его используют (или злоупотребляют)? По сути, вы используете final, так как вы не используете мой класс. * «Никто не заплатил мне за создание класса, который можно было бы разделить на подклассы» . Ты серьезно? Именно поэтому инженеры программного обеспечения нанимаются - для создания надежного, многократно используемого кода.
adelphus
4
-1 Просто сделай всё приватным, чтобы никто не подумал даже о подклассах ..
M4ks
3
@adelphus Хотя формулировка этого ответа является тупой, граничит с резкой, это не «неправильная» точка зрения. Фактически это та же точка зрения, что и большинство ответов на этот вопрос, пока только с менее клиническим тоном.
NemesisX00
+1 за упоминание, что вы можете удалить «окончательный». Высокомерно требовать предварительного знания всех возможных вариантов использования вашего кода. Тем не менее, скромно дать понять, что вы не можете поддерживать некоторые возможные варианты использования, и что для этих целей потребуется поддержка разветвления.
Gmatht
4

Давайте представим, что SDK для платформы поставляется в следующем классе:

class HTTPRequest {
   void get(String url, String method = "GET");
   void post(String url) {
       get(url, "POST");
   }
}

Приложение подклассов этого класса:

class MyHTTPRequest extends HTTPRequest {
    void get(String url, String method = "GET") {
        requestCounter++;
        super.get(url, method);
    }
}

Все хорошо, но кто-то, работающий над SDK, решает, что передавать метод getглупо, и делает интерфейс лучше для обеспечения обратной совместимости.

class HTTPRequest {
   @Deprecated
   void get(String url, String method) {
        request(url, method);
   }

   void get(String url) {
       request(url, "GET");
   }
   void post(String url) {
       request(url, "POST");
   }

   void request(String url, String method);
}

Все вроде нормально, пока приложение сверху не перекомпилируется с новым SDK. Внезапно переопределенный метод get больше не вызывается, и запрос не считается.

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

Final запрещает кому-либо создавать подклассы для вашего класса. Таким образом, какие методы внутри класса можно изменить, не беспокоясь о том, что где-то кто-то зависит от того, какие именно вызовы методов сделаны.

Уинстон Эверт
источник
1

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

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

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

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

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

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

Устраняя возможность расширения, за исключением случаев, когда я специально намереваюсь расширить класс, я избавляю себя (и, надеюсь, других) от многих головных болей.

Скотт Тейлор
источник
2
Если ваш метод таргетинга изменится, почему вы когда-либо сделали его публичным? И если вы значительно измените поведение класса, вам нужна другая версия, а не чрезмерная защитаfinal
M4ks
Я понятия не имею, почему это изменится. Хоббиты - хитрость, и изменение требовалось. Дело в том, что если я создаю его как окончательный вариант, я предотвращаю наследование, которое защищает других людей от того, что мои изменения могут заразить их код.
Скотт Тейлор
0

Использование finalни в коем случае не является нарушением принципов SOLID. К сожалению, очень часто интерпретировать открытый / закрытый принцип («программные объекты должны быть открыты для расширения, но закрыты для модификации») как означающее «вместо того, чтобы изменять класс, создавать подклассы и добавлять новые функции». Это не то, что изначально подразумевалось под этим, и, как правило, считается не лучшим подходом к достижению своих целей.

Наилучший способ соблюдения OCP - проектировать точки расширения в классе, специально предоставляя абстрактные поведения, которые параметризуются путем введения зависимости от объекта (например, с помощью шаблона проектирования Стратегии). Эти поведения должны быть разработаны для использования интерфейса, чтобы новые реализации не зависели от наследования.

Другой подход заключается в реализации вашего класса с его открытым API в качестве абстрактного класса (или интерфейса). Затем вы можете создать совершенно новую реализацию, которая может подключаться к тем же клиентам. Если ваш новый интерфейс требует поведения, аналогичного оригинальному, вы можете:

  • использовать шаблон проектирования Decorator для повторного использования существующего поведения оригинала или
  • рефакторинг частей поведения, которые вы хотите сохранить в объект помощника и использовать тот же помощник в вашей новой реализации (рефакторинг не является модификацией).
Жюль
источник
0

Для меня это вопрос дизайна.

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

Почему? Просто. Допустим, разработчик хочет унаследовать базовый класс WorkingDaysUSA в классе WorkingDaysUSAmyCompany и изменить его так, чтобы он отражал, что его предприятие будет закрыто для забастовки / обслуживания / по любой причине 2 марта.

Расчеты для заказов и доставки клиентов будут отражать задержку и работать соответственно, когда во время выполнения они вызывают WorkingDaysUSAmyCompany.getWorkingDays (), но что происходит, когда я вычисляю время отпуска? Должен ли я добавить 2 марта в качестве праздника для всех? Нет. Но поскольку программист использовал наследование, а я не защищал класс, это может привести к путанице.

Или, скажем, они наследуют и модифицируют класс, чтобы отразить, что эта компания не работает по субботам, а в стране они работают в перерыве по субботам. Затем землетрясение, кризис электричества или некоторые обстоятельства заставляют президента объявить 3 нерабочих дня, как это недавно произошло в Венесуэле. Если метод унаследованного класса уже вычитался каждую субботу, мои модификации в исходном классе могли привести к тому, что вычитали один и тот же день дважды. Я должен был бы пойти к каждому подклассу на каждом клиенте и проверить, что все изменения совместимы.

Решение? Сделайте класс окончательным и предоставьте метод addFreeDay (companyID mycompany, Date freeDay). Таким образом, вы уверены, что при вызове класса WorkingDaysCountry это ваш основной класс, а не подкласс

BNS
источник