Подтвердить что ты не робот

Открытый принцип закрытия и Java "final" модификатор

Принцип open-closed гласит, что "объекты программного обеспечения (классы, модули, функции и т.д.) должны быть открыты для расширения, но закрыты для модификации".

Однако Джошуа Блох в своей знаменитой книге "Эффективная Ява" дает следующий совет: "Дизайн и документ для наследования или запрет", и призывает программистов использовать "окончательный" модификатор для запрещения подкласса.

Я думаю, что эти два принципа явно противоречат друг другу (я не прав?). Какой принцип вы придерживаетесь при написании кода и почему? Вы оставляете свои классы открытыми, запрещаете наследование некоторых из них (какие?) Или используете последний модификатор, когда это возможно?

4b9b3361

Ответ 1

Откровенно говоря, я считаю, что принцип open/closed более анахронизм, чем нет. Это sems с 80-х и 90-х годов, когда структуры OO были построены по принципу, что все должно наследовать от чего-то другого и что все должно быть подклассы.

Это наиболее типично для интерфейсов пользовательского интерфейса той эпохи, как MFC и Java Swing. В Swing у вас есть смехотворное наследование, когда кнопка (iirc) увеличивает флажок (или наоборот), давая одно из них поведение, которое не используется (я думаю, что это его setDisabled() call on checkbox). Почему они разделяют родословную? Нет причин, кроме, ну, у них были некоторые общие методы.

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

DI/IoC также дополнительно делали случай для композиции.

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

Лично я в большинстве своем рассматриваю наследование как нечто большее, чем подробное описание в наши дни.

Ответ 2

В принцип open-closed (открыт для расширения, закрыт для модификации), вы все равно можете использовать последний модификатор. Вот один пример:

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 в классах для их закрытия в java аналогично использованию sealed в С#. Есть подобные обсуждения об этом на стороне .NET.

Ответ 3

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

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

Ответ 4

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

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

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

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

Ответ 5

Я очень уважаю Джошуа Блоха, и считаю, что Эффективная Java - это Java-библия. Но я думаю, что автоматическое дефолтирование private доступа часто является ошибкой. Я обычно делаю вещи protected по умолчанию, так что к ним можно, по крайней мере, получить доступ, расширив класс. В основном это объяснялось необходимостью компонентов unit test, но я также считаю это удобным для переопределения поведения классов по умолчанию. Я нахожу это очень раздражающим, когда я работаю в своей собственной кодовой базе и в конечном итоге вынужден копировать и изменять источник, потому что автор решил "скрыть" все. Если это вообще не в моих силах, я лоббирую, чтобы доступ был изменен на protected, чтобы избежать дублирования, что намного хуже ИМХО.

Также имейте в виду, что фон Блоха заключается в разработке очень общедоступных библиотек API bedrock; бар для получения такого кода "правильный" должен быть установлен очень высоким, поэтому, скорее всего, это не та же ситуация, что и большинство кода, который вы будете писать. Важные библиотеки, такие как сам JRE, имеют тенденцию быть более ограничительными, чтобы гарантировать, что язык не подвергается насилию. Просмотрите все API устаревшие в JRE? Их практически невозможно изменить или удалить. Ваша кодовая база, вероятно, не установлена ​​в камне, поэтому у вас есть возможность исправить ситуацию, если окажется, что вы сделали ошибку вначале.

Ответ 6

Два утверждения

Программные объекты (классы, модули, функции и т.д.) должны быть открыты для расширения, но закрыты для модификации.

и

Дизайн и документ для наследования, а также запретить его.

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

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

Ответ 7

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

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

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

Кроме того, учтите, что в С++ детали реализации класса (в частности, частные поля) обычно отображались в заголовочном файле ".h", поэтому, если программисту необходимо было его изменить, всем клиентам потребуется перекомпиляция. Обратите внимание, что это не относится к современным языкам, таким как Java или С#. Кроме того, я не думаю, что разработчики тогда могли рассчитывать на сложные IDE, способные выполнять анализ зависимости на лету, избегая необходимости частых полных перестроек.

В моем собственном опыте я предпочитаю делать абсолютно противоположное: "классы должны быть закрыты для расширения (final) по умолчанию, но открыты для модификации". Подумайте об этом: сегодня мы предпочитаем такие методы, как контроль версий (упрощает восстановление/сравнение предыдущих версий класса), рефакторинг (что побуждает нас модифицировать код для улучшения дизайн или как прелюдия к внедрению новых функций) и тестирование разработчика, которое обеспечивает защиту при изменении существующего кода.