Кажется, что Java обладает способностью объявлять классы, которые не могут быть извлечены целую вечность, и теперь C ++ имеет это тоже. Однако, в свете принципа открытия / закрытия в SOLID, почему это было бы полезно? Для меня final
ключевое слово звучит так же, как friend
это - это законно, но если вы используете его, скорее всего, дизайн не так. Пожалуйста, приведите несколько примеров, где невзаимозаменяемый класс будет частью большой архитектуры или шаблона проектирования.
54
final
? Многие люди (включая меня) считают, что это хороший дизайн - создавать каждый неабстрактный классfinal
.final
.Ответы:
final
выражает намерение . Он сообщает пользователю о классе, методе или переменной: «Этот элемент не должен изменяться, и если вы хотите изменить его, вы не поняли существующий дизайн».Это важно, потому что архитектура программы была бы действительно очень сложной, если бы вы ожидали, что каждый класс и каждый метод, который вы когда-либо напишете, могут быть изменены для выполнения чего-то совершенно другого подклассом. Гораздо лучше заранее решить, какие элементы должны быть изменяемыми, а какие нет, и принудительно применить неизменность
final
.Вы также можете сделать это с помощью комментариев и архитектурных документов, но всегда лучше позволить компилятору принудительно применить то, что он может, чем надеяться, что будущие пользователи будут читать и подчиняться документации.
источник
Это позволяет избежать хрупкой проблемы базового класса . Каждый класс поставляется с набором неявных или явных гарантий и инвариантов. Принцип подстановки Лискова требует, чтобы все подтипы этого класса также предоставляли все эти гарантии. Тем не менее, это действительно легко нарушить это, если мы не используем
final
. Например, давайте проверим пароль:Если мы допустим переопределение этого класса, одна реализация может заблокировать всех, другая может предоставить всем доступ:
Обычно это не так, поскольку подклассы теперь имеют поведение, очень несовместимое с оригиналом. Если мы действительно намерены расширить класс другим поведением, лучше использовать Chain of Responsibility:
Проблема становится более очевидной, когда более сложный класс вызывает свои собственные методы, и эти методы могут быть переопределены. Я иногда сталкиваюсь с этим, когда красиво печатаю структуру данных или пишу HTML. Каждый метод отвечает за некоторый виджет.
Теперь я создаю подкласс, который добавляет немного больше стиля:
Теперь на мгновение игнорируем, что это не очень хороший способ создания HTML-страниц, что произойдет, если я захочу еще раз изменить макет? Я должен был бы создать
SpiffyPage
подкласс, который каким-то образом оборачивает этот контент. Здесь мы видим случайное применение шаблона метода шаблона. Методы шаблона - это четко определенные точки расширения в базовом классе, которые предназначены для переопределения.А что произойдет, если базовый класс изменится? Если содержимое HTML изменяется слишком сильно, это может нарушить компоновку, предоставляемую подклассами. Поэтому не очень безопасно менять базовый класс впоследствии. Это не очевидно, если все ваши классы находятся в одном проекте, но очень заметно, если базовый класс является частью какого-либо опубликованного программного обеспечения, на котором строят другие люди.
Если бы эта стратегия расширения была предназначена, мы могли бы позволить пользователю поменять способ создания каждой части. Либо, для каждого блока может быть стратегия, которая может быть предоставлена извне. Или мы могли бы вкладывать декораторы. Это будет эквивалентно приведенному выше коду, но будет гораздо более явным и более гибким:
(Дополнительный
top
параметр необходим, чтобы убедиться, что вызовыwriteMainContent
проходят через вершину цепочки декораторов. Это эмулирует функцию подклассов, называемую открытой рекурсией .)Если у нас есть несколько декораторов, мы можем теперь смешивать их более свободно.
Гораздо чаще, чем желание слегка адаптировать существующие функциональные возможности, возникает желание повторно использовать некоторую часть существующего класса. Я видел случай, когда кто-то хотел класс, где вы могли бы добавлять элементы и перебирать их все. Правильным решением было бы:
Вместо этого они создали подкласс:
Это внезапно означает, что весь интерфейс
ArrayList
стал частью нашего интерфейса. Пользователи могутremove()
вещи илиget()
вещи по определенным показателям. Это было задумано таким образом? ХОРОШО. Но часто мы не продумываем все последствия.Поэтому желательно
extend
урок без тщательной мысли.final
исключая, если вы намереваетесь переопределить любой метод.Есть много примеров, когда это «правило» нужно нарушать, но оно обычно ведет вас к хорошему, гибкому дизайну и позволяет избежать ошибок из-за непреднамеренных изменений в базовых классах (или непреднамеренного использования подкласса в качестве экземпляра базового класса. ).
Некоторые языки имеют более строгие механизмы обеспечения соблюдения:
virtual
источник
Я удивлен тем, что еще никто не упомянул « Эффективную Java», 2-е издание Джошуа Блоха (которое должно быть обязательно прочитано, по крайней мере, для каждого разработчика Java). Пункт 17 в книге обсуждает это в деталях, и называется: « Дизайн и документ для наследования или иначе запретить ».
Я не буду повторять все полезные советы в книге, но эти конкретные пункты кажутся актуальными:
источник
Одна из причин, которая
final
может быть полезна, заключается в том, что она гарантирует, что вы не можете создать подкласс класса таким образом, который нарушил бы контракт родительского класса. Такое создание подклассов было бы нарушением SOLID (больше всего "L"), и создание классаfinal
предотвращает это.Один типичный пример делает невозможным создание подкласса неизменяемого класса таким образом, чтобы сделать подкласс изменчивым. В некоторых случаях такое изменение поведения может привести к очень неожиданным эффектам, например, когда вы используете что-то в качестве ключей на карте, думая, что ключ неизменен, в то время как в действительности вы используете подкласс, который является изменяемым.
В Java можно было бы ввести много интересных проблем безопасности, если бы вы смогли
String
создать подкласс и сделать его изменчивым (или сделать так, чтобы он возвращался домой, когда кто-то вызывает его методы, таким образом, возможно, извлекая конфиденциальные данные из системы) при передаче этих объектов вокруг некоторого внутреннего кода, связанного с загрузкой классов и безопасностью.Final также иногда помогает предотвратить простые ошибки, такие как повторное использование одной и той же переменной для двух вещей в методе и т. Д. В Scala рекомендуется использовать только то,
val
что примерно соответствует конечным переменным в Java, и фактически любое использованиеvar
или неконечная переменная рассматривается с подозрением.Наконец, компиляторы могут, по крайней мере, теоретически, выполнить некоторые дополнительные оптимизации, когда они знают, что класс или метод являются окончательными, поскольку, когда вы вызываете метод в конечном классе, вы точно знаете, какой метод будет вызван, и не нужно идти через таблицу виртуальных методов для проверки наследования.
источник
SecurityManager
. Большинство программ не используют его, но они также не запускают ненадежный код. +++ Вы должны предположить, что строки неизменны, так как в противном случае вы получаете нулевую безопасность и кучу произвольных ошибок в качестве бонуса. Любой программист, считающий, что строки Java могут измениться в производительном коде, безусловно, безумен.Вторая причина - производительность . Первая причина заключается в том, что некоторые классы имеют важные поведения или состояния, которые не должны изменяться, чтобы позволить системе работать. Например, если у меня есть класс «PasswordCheck» и для его создания я нанял команду экспертов по безопасности, и этот класс связывается с сотнями банкоматов с хорошо изученными и определенными процедурами. Разрешить новому наемному парню, только что закончившему университет, создать класс «TrustMePasswordCheck», который расширяет вышеуказанный класс, может быть очень вредным для моей системы; эти методы не должны быть переопределены, вот и все.
источник
Когда мне нужен класс, я напишу класс. Если мне не нужны подклассы, мне наплевать на подклассы. Я удостоверяюсь, что мой класс ведет себя так, как задумано, а места, где я использую класс, предполагают, что класс ведет себя так, как задумано.
Если кто-то хочет подкласса моего класса, я хочу полностью отрицать любую ответственность за то, что происходит. Я достигаю этого, делая класс "финальным". Если вы хотите создать подкласс, помните, что я не учитывал подклассы во время написания класса. Таким образом, вы должны взять исходный код класса, удалить «финал», и с этого момента все, что происходит, является вашей полной ответственностью .
Вы думаете, что это «не объектно-ориентированный»? Мне заплатили, чтобы сделать класс, который делает то, что он должен делать. Никто не заплатил мне за создание класса, который мог бы быть подклассом. Если вам платят за повторное использование моего класса, вы можете это сделать. Начните с удаления "окончательного" ключевого слова.
(Кроме этого, «final» часто допускает существенную оптимизацию. Например, в Swift «final» для открытого класса или для метода открытого класса означает, что компилятор может полностью знать, какой код будет выполнять вызов метода, и может заменить динамическую диспетчеризацию статической диспетчеризацией (крошечная выгода) и часто заменять статическую диспетчеризацию на встраивание (возможно, огромная выгода)).
adelphus: Что так трудно понять о том, «если вы хотите создать подкласс, взять исходный код, удалить« последний », и это ваша ответственность»? «окончательный» означает «справедливое предупреждение».
И мне не платят за создание кода многократного использования. Мне платят за написание кода, который делает то, что должен. Если мне платят за создание двух одинаковых фрагментов кода, я извлекаю общие части, потому что это дешевле, и мне не платят, чтобы тратить мое время. Создание кода многократного использования, которое не используется повторно - пустая трата моего времени.
M4ks: Вы всегда делаете все конфиденциальным, к которому нельзя получить доступ извне. Опять же, если вы хотите создать подкласс, вы берете исходный код, меняете вещи на «защищенные», если вам нужно, и берете на себя ответственность за то, что вы делаете. Если вы считаете, что вам нужен доступ к вещам, которые я пометил как личные, вам лучше знать, что вы делаете.
Оба: Подклассы - это крошечная, крошечная часть повторного использования кода. Создание строительных блоков, которые можно адаптировать без создания подклассов, гораздо более мощно и приносит огромную пользу от «финала», потому что пользователи блоков могут полагаться на то, что они получают.
источник
Давайте представим, что SDK для платформы поставляется в следующем классе:
Приложение подклассов этого класса:
Все хорошо, но кто-то, работающий над SDK, решает, что передавать метод
get
глупо, и делает интерфейс лучше для обеспечения обратной совместимости.Все вроде нормально, пока приложение сверху не перекомпилируется с новым SDK. Внезапно переопределенный метод get больше не вызывается, и запрос не считается.
Это называется хрупкой проблемой базового класса, потому что казалось бы безобидное изменение приводит к разрыву подкласса. Любое изменение метода, вызываемого внутри класса, может привести к разрыву подкласса. Это означает, что почти любое изменение может привести к разрыву подкласса.
Final запрещает кому-либо создавать подклассы для вашего класса. Таким образом, какие методы внутри класса можно изменить, не беспокоясь о том, что где-то кто-то зависит от того, какие именно вызовы методов сделаны.
источник
Окончательный результат означает, что ваш класс можно безопасно менять в будущем, не затрагивая какие-либо классы, основанные на наследовании (потому что их нет), или каких-либо проблем, связанных с безопасностью потока класса (я думаю, что есть случаи, когда ключевое слово final в поле предотвращает какая-то нить на основе высокого сглаза).
Заключительный означает, что вы можете свободно изменять работу своего класса без каких-либо непреднамеренных изменений в поведении, которые проникают в код других людей, который опирается на ваш в качестве основы.
В качестве примера я пишу класс с именем HobbitKiller, и это здорово, потому что все хоббиты - обманщики и, вероятно, должны умереть. Поцарапайте это, они все определенно должны умереть.
Вы используете его в качестве базового класса и добавляете потрясающий новый метод для использования огнемета, но используете мой класс в качестве базового, потому что у меня есть отличный метод для нацеливания на хоббитов (помимо того, что они хитрые, они быстрые), который вы используете, чтобы помочь направить огнемет.
Три месяца спустя я меняю реализацию метода таргетинга. Теперь, в какой-то момент, когда вы обновите свою библиотеку, без вашего ведома, фактическая реализация среды выполнения вашего класса кардинально изменилась из-за изменения метода суперкласса, от которого вы зависите (и обычно не контролируете).
Поэтому для того, чтобы быть добросовестным разработчиком и обеспечить плавную смерть хоббита в будущем, используя мой класс, я должен быть очень, очень осторожен с любыми изменениями, которые я вносю в любые классы, которые могут быть расширены.
Устраняя возможность расширения, за исключением случаев, когда я специально намереваюсь расширить класс, я избавляю себя (и, надеюсь, других) от многих головных болей.
источник
final
Использование
final
ни в коем случае не является нарушением принципов SOLID. К сожалению, очень часто интерпретировать открытый / закрытый принцип («программные объекты должны быть открыты для расширения, но закрыты для модификации») как означающее «вместо того, чтобы изменять класс, создавать подклассы и добавлять новые функции». Это не то, что изначально подразумевалось под этим, и, как правило, считается не лучшим подходом к достижению своих целей.Наилучший способ соблюдения OCP - проектировать точки расширения в классе, специально предоставляя абстрактные поведения, которые параметризуются путем введения зависимости от объекта (например, с помощью шаблона проектирования Стратегии). Эти поведения должны быть разработаны для использования интерфейса, чтобы новые реализации не зависели от наследования.
Другой подход заключается в реализации вашего класса с его открытым API в качестве абстрактного класса (или интерфейса). Затем вы можете создать совершенно новую реализацию, которая может подключаться к тем же клиентам. Если ваш новый интерфейс требует поведения, аналогичного оригинальному, вы можете:
источник
Для меня это вопрос дизайна.
Предположим, у меня есть программа, которая рассчитывает зарплату для сотрудников. Если у меня есть класс, который возвращает количество рабочих дней между двумя датами в зависимости от страны (один класс для каждой страны), я поставлю этот окончательный вариант и предоставлю метод для каждого предприятия, чтобы предоставить свободный день только для своих календарей.
Почему? Просто. Допустим, разработчик хочет унаследовать базовый класс WorkingDaysUSA в классе WorkingDaysUSAmyCompany и изменить его так, чтобы он отражал, что его предприятие будет закрыто для забастовки / обслуживания / по любой причине 2 марта.
Расчеты для заказов и доставки клиентов будут отражать задержку и работать соответственно, когда во время выполнения они вызывают WorkingDaysUSAmyCompany.getWorkingDays (), но что происходит, когда я вычисляю время отпуска? Должен ли я добавить 2 марта в качестве праздника для всех? Нет. Но поскольку программист использовал наследование, а я не защищал класс, это может привести к путанице.
Или, скажем, они наследуют и модифицируют класс, чтобы отразить, что эта компания не работает по субботам, а в стране они работают в перерыве по субботам. Затем землетрясение, кризис электричества или некоторые обстоятельства заставляют президента объявить 3 нерабочих дня, как это недавно произошло в Венесуэле. Если метод унаследованного класса уже вычитался каждую субботу, мои модификации в исходном классе могли привести к тому, что вычитали один и тот же день дважды. Я должен был бы пойти к каждому подклассу на каждом клиенте и проверить, что все изменения совместимы.
Решение? Сделайте класс окончательным и предоставьте метод addFreeDay (companyID mycompany, Date freeDay). Таким образом, вы уверены, что при вызове класса WorkingDaysCountry это ваш основной класс, а не подкласс
источник