Открыто-закрытый принцип и модификатор Java "финал"
Принцип открытого-закрытого положения гласит, что "программные объекты (классы, модули, функции и т. Д.) Должны быть открыты для расширения, но закрыты для модификации".
Тем не менее, Джошуа Блох в своей знаменитой книге "Эффективная Java" дает следующий совет: "Создавайте и документируйте для наследования, или же запрещайте его", и рекомендует программистам использовать модификатор "final" для запрета подклассов.
Я думаю, что эти два принципа явно противоречат друг другу (я не прав?). Каким принципом вы руководствуетесь при написании кода и почему? Вы оставляете свои классы открытыми, не разрешаете наследование некоторых из них (какие?) Или используете модификатор final по возможности?
7 ответов
Честно говоря, я думаю, что принцип "открытый / закрытый" - это скорее анахронизм, чем нет. Это относится к 80-м и 90-м годам, когда OO-фреймворки были построены по принципу, что все должно наследоваться от чего-то другого, и что все должно быть подклассифицированным.
Это было наиболее типично для UI-фреймворков той эпохи, таких как MFC и Java Swing. В Swing у вас есть нелепое наследование, когда кнопка (iirc) расширяет флажок (или наоборот), давая одному из них поведение, которое не используется (я думаю, что это вызов setDisabled() для флажка). Почему они имеют родословную? Никаких других причин, кроме того, что у них были общие методы.
В наши дни композиция предпочтительнее наследования. В то время как Java допускает наследование по умолчанию, .Net использует (более современный) подход, запрещающий его по умолчанию, что, на мой взгляд, является более правильным (и более совместимым с принципами Джоша Блоха).
DI / IoC также доказали необходимость композиции.
Джош Блох также указывает, что наследование нарушает инкапсуляцию, и приводит несколько хороших примеров того, почему. Также было продемонстрировано, что изменение поведения коллекций Java более согласованно, если это делается делегированием, а не расширением классов.
Лично я в значительной степени рассматриваю наследование как нечто большее, чем деталь реализации в наши дни.
Я не думаю, что эти два утверждения противоречат друг другу. Тип может быть открыт для расширения и все еще закрыт для наследования.
Один из способов сделать это - использовать внедрение зависимости. Вместо создания экземпляров своих собственных вспомогательных типов, тип может получить их при создании. Это позволяет изменять части (т.е. открытые для расширения) типа без изменения самого типа (т.е. закрывать для модификации).
По принципу открытого-закрытого (открытый для расширения, закрытый для модификации) вы все равно можете использовать модификатор final. Вот один пример:
public final class ClosedClass {
private IMyExtension myExtension;
public ClosedClass(IMyExtension myExtension)
{
this.myExtension = myExtension;
}
// methods that use the IMyExtension object
}
public interface IMyExtension {
public void doStuff();
}
ClosedClass
закрыто для модификации внутри класса, но открыто для расширения через другой. В этом случае это может быть все, что реализует IMyExtension
интерфейс. Этот трюк представляет собой вариант внедрения зависимости, так как мы передаем закрытый класс другому, в данном случае через конструктор. Поскольку расширение является interface
это не может быть final
но его реализующий класс может быть.
Использование final в классах для их закрытия в Ja va похоже на использование sealed
в C#. Подобные обсуждения обсуждаются на стороне.NET.
Я очень уважаю Джошуа Блоха и считаю, что Effective Java в значительной степени является библией Java. Но я думаю, что автоматически по умолчанию private
доступ часто является ошибкой. Я склонен делать вещи protected
по умолчанию, чтобы к ним можно было как минимум получить доступ путем расширения класса. В основном это связано с необходимостью модульного тестирования компонентов, но я также считаю, что это удобно для переопределения поведения классов по умолчанию. Я нахожу это очень раздражающим, когда я работаю в кодовой базе моей собственной компании, и в итоге мне приходится копировать и изменять исходный код, потому что автор решил "спрятать" все. Если это вообще в моих силах, я лоббирую, чтобы изменить доступ на protected
чтобы избежать дублирования, что гораздо хуже, ИМХО.
Также имейте в виду, что опыт Блоха заключается в разработке общедоступных базовых API-библиотек; планка для получения "правильного" кода должна быть очень высокой, так что скорее всего это не та же самая ситуация, что и для большинства кода, который вы будете писать. Важные библиотеки, такие как сама JRE, имеют тенденцию быть более строгими, чтобы гарантировать, что язык не используется. Видите все устаревшие API в JRE? Их практически невозможно изменить или удалить. Ваша кодовая база, вероятно, не установлена, поэтому у вас есть возможность исправить ситуацию, если окажется, что вы изначально допустили ошибку.
В настоящее время я использую модификатор final по умолчанию, почти рефлексивно, как часть стандартного шаблона. Об этом легче думать, когда вы знаете, что данный метод всегда будет функционировать, как видно из кода, который вы просматриваете прямо сейчас.
Конечно, иногда бывают ситуации, когда иерархия классов - это именно то, что вам нужно, и было бы глупо тогда ее не использовать. Но следует бояться иерархий более двух уровней или тех, где неабстрактные классы еще больше подклассифицированы. Класс должен быть абстрактным или окончательным.
Большую часть времени использование композиции - это путь. Объедините все общие механизмы в один класс, поместите разные случаи в разные классы, затем составьте экземпляры, чтобы они работали как единое целое.
Вы можете назвать это "внедрение зависимости", или "шаблон стратегии", или "шаблон посетителя", или как угодно, но суть этого в том, чтобы использовать композицию вместо наследования, чтобы избежать повторения.
Два заявления
Программные объекты (классы, модули, функции и т. Д.) Должны быть открыты для расширения, но закрыты для модификации.
а также
Оформить и оформить документ на наследство, иначе запретить.
не находятся в прямом противоречии друг с другом. Вы можете следовать принципу открытого-закрытого, пока вы разрабатываете и документируете его (согласно совету Блоха).
Я не думаю, что Блох заявляет, что вы должны предпочесть запретить наследование с помощью модификатора final, просто вы должны явно разрешить или запретить наследование в каждом создаваемом вами классе. Он советует вам подумать об этом и решить для себя, а не просто принять поведение компилятора по умолчанию.
Я не думаю, что принцип Open/closed, как он был представлен изначально, позволяет интерпретировать, что конечные классы могут быть расширены посредством внедрения зависимостей.
В моем понимании, принцип заключается в том, чтобы не допускать прямых изменений в коде, который был введен в производство, и способ достижения этого, в то же время позволяя модифицировать функциональность, состоит в использовании наследования реализации.
Как указано в первом ответе, это имеет исторические корни. Несколько десятилетий назад наследование было в пользу, тестирование разработчиков было неслыханным, а перекомпиляция кодовой базы часто занимала слишком много времени.
Кроме того, учтите, что в C++ подробности реализации класса (в частности, частные поля) обычно отображаются в заголовочном файле ".h", поэтому, если программисту потребуется изменить его, всем клиентам потребуется перекомпиляция. Обратите внимание, что это не так с современными языками, такими как Java или C#. Кроме того, я не думаю, что разработчики могли рассчитывать на сложные интегрированные среды разработки, способные выполнять анализ зависимостей на лету, избегая необходимости частых полных перестроений.
По собственному опыту, я предпочитаю делать прямо противоположное: "классы должны быть закрыты для расширения (final
) по умолчанию, но открыты для модификации ". Подумайте об этом: сегодня мы предпочитаем такие методы, как управление версиями (упрощает восстановление / сравнение предыдущих версий класса), рефакторинг (что побуждает нас изменять код для улучшения дизайна или как прелюдия к внедрению новых функций) и тестирование разработчика, которое обеспечивает безопасность при изменении существующего кода.