Открыто-закрытый принцип и Java "финальный" модификатор - PullRequest
21 голосов
/ 18 марта 2009

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

Тем не менее, Джошуа Блох в своей знаменитой книге «Эффективная Java» дает следующий совет: «Проектируйте и документируйте для наследования, или же запрещайте его», и рекомендует программистам использовать модификатор «final» для запрета подклассов.

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

Ответы [ 7 ]

25 голосов
/ 18 марта 2009

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

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

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

DI / IoC также обосновали композицию.

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

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

8 голосов
/ 18 марта 2009

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

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

7 голосов
/ 18 марта 2009

В принцип открытый-закрытый (открыт для расширения, закрыт для модификации) вы все равно можете использовать модификатор 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 в классах для их закрытия в Java аналогично использованию sealed в C #. Есть подобных обсуждений об этом на стороне .NET.

5 голосов
/ 18 марта 2009

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

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

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

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

4 голосов
/ 18 марта 2009

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

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

3 голосов
/ 18 марта 2009

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

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

и

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

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

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

1 голос
/ 28 июня 2009

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

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

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

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

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

...