После более 10 лет программирования на Java / C # я создаю либо:
- абстрактные классы : контракт не предназначен для создания как есть.
- заключительные / закрытые классы : реализация не предназначена для использования в качестве базового класса для чего-то другого.
Я не могу представить себе ситуацию, в которой простой «класс» (т.е. ни абстрактный, ни окончательный / запечатанный) не был бы «мудрым программированием».
Почему класс должен быть чем-то отличным от "абстрактного" или "окончательного / запечатанного"?
РЕДАКТИРОВАТЬ
Эта замечательная статья объясняет мои проблемы гораздо лучше, чем я.
object-oriented
programming-practices
Николя Репике
источник
источник
Open/Closed principle
, а неClosed Principle.
Ответы:
По иронии судьбы я нахожу обратное: использование абстрактных классов является скорее исключением, чем правилом, и я склонен не одобрять окончательные / запечатанные классы.
Интерфейсы являются более типичным механизмом разработки по контракту, потому что вы не указываете какие-либо внутренние компоненты - вы не беспокоитесь о них. Это позволяет каждому исполнению этого контракта быть независимым. Это является ключевым во многих областях. Например, если вы строили ORM, было бы очень важно, чтобы вы могли передавать запрос в базу данных единообразным способом, но реализации могут быть совершенно другими. Если вы используете абстрактные классы для этой цели, вы в конечном итоге подключите компоненты, которые могут или не могут применяться ко всем реализациям.
Для окончательных / запечатанных классов единственное оправдание, которое я когда-либо вижу для их использования, - это когда на самом деле опасно разрешать переопределение - возможно, алгоритм шифрования или что-то в этом роде. Кроме этого, вы никогда не знаете, когда вы захотите расширить функциональность по местным причинам. Запечатывание класса ограничивает ваши варианты получения, которые отсутствуют в большинстве сценариев. Гораздо более гибко писать свои классы таким образом, чтобы они могли быть расширены позже.
Эта последняя точка зрения была укреплена для меня благодаря работе со сторонними компонентами, которые запечатали классы, тем самым предотвращая некоторую интеграцию, которая сделала бы жизнь намного проще.
источник
То есть большая статья Эрик Липперт, но я не думаю , что она поддерживает вашу точку зрения.
Он утверждает, что все классы, выставленные для использования другими, должны быть запечатаны или иным образом сделаны нерасширяемыми.
Ваша предпосылка заключается в том, что все классы должны быть абстрактными или запечатаны.
Большая разница.
В статье EL ничего не говорится о (предположительно, многих) классах, созданных его командой, о которых мы с вами ничего не знаем. Как правило, открытые классы в платформе являются лишь подмножеством всех классов, участвующих в реализации этой платформы.
источник
new List<Foo>()
чем-то вродеList<Foo>.CreateMutable()
.С точки зрения Java, я думаю, что последние классы не так умны, как кажется.
Многие инструменты (особенно AOP, JPA и т. Д.) Работают с изменением времени загрузки, поэтому им приходится расширять ваши предложения. Другой способ - создать делегатов (не делегатов .NET) и делегировать все исходному классу, что было бы намного более беспорядочным, чем расширение пользовательских классов.
источник
Два распространенных случая, когда вам понадобятся классы без ванили:
Технический: если у вас иерархия, которая имеет глубину более двух уровней , и вы хотите иметь возможность создавать что-то в середине.
Принцип: иногда желательно написать классы, которые бы были простыми и безопасными . (Это часто случается, когда вы пишете такой API, как Эрик Липперт, или когда вы работаете в команде над большим проектом). Иногда вы хотите написать класс, который прекрасно работает сам по себе, но он разработан с учетом расширяемости.
Мысли Эрики Липперты на уплотнительном имеет смысл, но он также признает , что они делают дизайн для расширяемости, оставив класс «открытым».
Да, многие классы запечатаны в BCL, но огромное количество классов нет, и могут быть расширены всеми возможными способами. Один пример, который приходит на ум, - это Windows Forms, где вы можете добавить данные или поведение практически к любому с
Control
помощью наследования. Конечно, это можно было бы сделать другими способами (шаблон декоратора, различные типы композиции и т. Д.), Но наследование также работает очень хорошо.Два специальных замечания .NET:
virtual
функциональностью, за исключением явных реализаций интерфейса.internal
вместо герметизации класса, что позволяет ему наследоваться внутри вашей кодовой базы, но не за ее пределами.источник
Я полагаю, что реальная причина, по которой многие люди считают, что классы должны быть
final
/sealed
заключается в том, что большинство неабстрактных расширяемых классов не документированы должным образом .Позвольте мне уточнить. Начиная издалека, некоторые программисты считают, что наследование как инструмент в ООП широко используется и злоупотребляется. Мы все прочитали принцип подстановки Лискова, хотя это не помешало нам нарушать его сотни (а то и тысячи) раз.
Дело в том, что программисты любят повторно использовать код. Даже когда это не очень хорошая идея. И наследование является ключевым инструментом в этом «повторном использовании» кода. Вернуться к последнему / закрытому вопросу.
Соответствующая документация для окончательного / запечатанного класса является относительно небольшой: вы описываете, что делает каждый метод, каковы аргументы, возвращаемое значение и т. Д. Все обычные вещи.
Однако, когда вы правильно документируете расширяемый класс, вы должны включить по крайней мере следующее :
super
ли вы реализацию? Вы вызываете это в начале метода или в конце? Think constructor vs destructor)Это только с моей головы. И я могу предоставить вам пример того, почему каждый из них важен, и если вы пропустите его, это испортит расширяющийся класс.
Теперь рассмотрим, сколько усилий по документированию должно быть уделено правильному документированию каждой из этих вещей. Я полагаю, что класс с 7-8 методами (который может быть слишком много в идеализированном мире, но слишком мало в реальном) может также иметь документацию около 5 страниц, только для текста. Таким образом, вместо этого мы уклоняемся на полпути и не запечатываем класс, чтобы другие люди могли использовать его, но также не документировали его должным образом, поскольку это займет огромное количество времени (и, вы знаете, это может в любом случае никогда не будет продлен, так зачем?).
Если вы разрабатываете класс, у вас может возникнуть соблазн запечатать его так, чтобы люди не могли использовать его так, как вы не предвидели (и не подготовились). С другой стороны, когда вы используете чужой код, иногда из общедоступного API нет видимой причины, по которой класс будет окончательным, и вы можете подумать: «Черт, это просто стоило мне 30 минут в поисках обходного пути».
Я думаю, что некоторые из элементов решения:
Также стоит упомянуть следующий «философский» вопрос: является ли класс
sealed
/final
частью контракта для класса, или деталь реализации? Теперь я не хочу идти туда, но ответ на этот вопрос также должен повлиять на ваше решение о том, закрывать класс или нет.источник
Класс не должен быть ни окончательным / запечатанным, ни абстрактным, если:
Например, возьмите
ObservableCollection<T>
класс в C # . Нужно только добавить возбуждение событий к обычным операциям aCollection<T>
, поэтому он подклассыCollection<T>
.Collection<T>
жизнеспособный класс сам по себе, и так оно и естьObservableCollection<T>
.источник
CollectionBase
(абстрактно),Collection : CollectionBase
(запечатано),ObservableCollection : CollectionBase
(запечатано). Если вы внимательно посмотрите на Collection <T> , вы увидите, что это недоделанный абстрактный класс.Collection<T>
"недоделанный абстрактный класс"?Collection<T>
раскрывает много внутренностей через защищенные методы и свойства и явно предназначен для использования в качестве базового класса для специализированной коллекции. Но не ясно, куда вы должны поместить свой код, так как нет абстрактных методов для реализации.ObservableCollection
наследуется отCollection
и не запечатан, так что вы можете снова наследовать от него. И вы можете получить доступ кItems
защищенному свойству, позволяя добавлять элементы в коллекцию, не вызывая событий ... Приятно.Проблема с заключительными / запечатанными классами состоит в том, что они пытаются решить проблему, которая еще не произошла. Это полезно только тогда, когда проблема существует, но это разочаровывает, потому что стороннее наложило ограничение. Редко ли класс герметизации решает текущую проблему, из-за чего трудно утверждать, что она полезна.
Есть случаи, когда класс должен быть запечатан. Как пример; Класс управляет выделенными ресурсами / памятью таким образом, что он не может предсказать, как будущие изменения могут изменить это управление.
За эти годы я обнаружил, что инкапсуляция, обратные вызовы и события были гораздо более гибкими / полезными, чем абстрактные классы. Я вижу очень много кода с большой иерархией классов, где инкапсуляция и события сделали бы жизнь разработчика проще.
источник
«Запечатывание» или «завершение» в объектных системах допускает определенную оптимизацию, поскольку известен полный график диспетчеризации.
То есть мы затрудняем превращение системы во что-то другое, что является компромиссом для производительности. (Это суть большинства оптимизаций.)
Во всем остальном это потеря. Системы должны быть открытыми и расширяемыми по умолчанию. Должно быть легко добавлять новые методы ко всем классам и расширять их произвольно.
Мы не получаем никакой новой функциональности сегодня, предпринимая шаги, чтобы предотвратить будущее расширение.
Поэтому, если мы делаем это ради самой профилактики, то, что мы делаем, это пытаемся контролировать жизнь будущих сопровождающих. Это про эго. «Даже когда я больше не работаю здесь, этот код будет поддерживаться на моем пути, черт возьми!»
источник
Тестовые занятия, похоже, приходят на ум. Это классы, которые вызываются автоматически или «по желанию» в зависимости от того, что пытается сделать программист / тестировщик. Я не уверен, что когда-либо видел или слышал о готовом классе частных тестов.
источник
Время, когда вам нужно рассмотреть класс, который должен быть расширен, - это когда вы делаете реальное планирование на будущее. Позвольте мне привести пример из моей работы.
Я трачу много времени на написание инструментов интерфейса между нашим основным продуктом и внешними системами. Когда мы совершаем новую продажу, один большой компонент - это набор экспортеров, которые предназначены для регулярной работы, которые генерируют файлы данных с подробным описанием событий, которые произошли в тот день. Эти файлы данных затем используются системой клиента.
Это отличная возможность для расширения классов.
У меня есть класс экспорта, который является базовым классом каждого экспортера. Он знает, как подключиться к базе данных, узнать, куда он попал в последний раз, когда он запускался, и создать архивы файлов данных, которые он генерирует. Он также обеспечивает управление файлами свойств, ведение журнала и некоторую простую обработку исключений.
Кроме того, у меня есть другой экспортер для работы с каждым типом данных, возможно, есть активность пользователя, данные транзакций, данные управления денежными средствами и т. Д.
Поверх этого стека я размещаю слой, специфичный для клиента, который реализует структуру файла данных, которая нужна клиенту.
Таким образом, базовый экспортер очень редко меняется. Основные экспортеры типов данных иногда изменяются, но редко, и, как правило, только для обработки изменений схемы базы данных, которые в любом случае следует распространить на всех клиентов. Единственная работа, которую мне приходится выполнять для каждого клиента, - это та часть кода, которая специфична для этого клиента. Идеальный мир!
Итак, структура выглядит так:
Моя основная мысль заключается в том, что, создавая код таким образом, я могу использовать наследование в первую очередь для повторного использования кода.
Я должен сказать, что не могу думать ни о какой причине, чтобы пройти три слоя.
Я использовал два слоя много раз, например, чтобы иметь общий
Table
класс, который реализует запросы к таблице базы данных, в то время как подклассыTable
реализуют конкретные детали каждой таблицы, используяenum
для определения полей. Предоставлениеenum
реализации интерфейса, определенного вTable
классе, имеет все виды смысла.источник
Я нашел абстрактные классы полезными, но не всегда необходимыми, и закрытые классы становятся проблемой при применении модульных тестов. Вы не можете издеваться или заглушить запечатанный класс, если вы не используете что-то вроде Teleriks justmock.
источник
Я согласен с вашей точкой зрения. Я думаю, что в Java по умолчанию классы должны быть объявлены как «окончательные». Если вы не сделаете его окончательным, то специально подготовьте его и задокументируйте для расширения.
Основная причина этого заключается в том, чтобы гарантировать, что любые экземпляры ваших классов будут соответствовать интерфейсу, который вы изначально проектировали и документировали. В противном случае разработчик, использующий ваши классы, может потенциально создать хрупкий и противоречивый код и, в свою очередь, передать его другим разработчикам / проектам, что сделает объекты ваших классов недоверяемыми.
С более практической точки зрения, в этом есть и обратная сторона, поскольку клиенты интерфейса вашей библиотеки не смогут вносить изменения и использовать ваши классы более гибкими способами, о которых вы изначально думали.
Лично, при всем существующем коде плохого качества (и поскольку мы обсуждаем это на более практическом уровне, я бы сказал, что разработка Java более склонна к этому), я думаю, что эта жесткость - небольшая цена, которую нужно заплатить в нашем стремлении упростить поддерживать код.
источник