Автор: Sergey Teplyakov

Камрад @ViIvanov в своем блоге поднял интересный вопрос о значимых типах и ООП. По сути, это был развернутый комментарий к моему посту Пишем простой анализатор с помощью Roslyn, в котором я упомянул, что структуры плохо дружат с ООП. Вячеслав высказал очень разумную мысль, что наследование не является ключевой особенностью ООП, а значит ограничение значимых типов в этой части не должны делать из них граждан второго сорта. Ну что ж, пришло время развернуть мою мысль поглубже.

Начнем с того, чем же для меня является и не является ООП. Мое ИМХО в том, что ООП, как и проектирование в целом, основано на двух китах абстракции и инкапсуляции (а не только на инкапсуляции, как выразился Вячеслав). Абстракция позволяет выделить существенные аспекты поведения, а инкапсуляция является тем инструментом, который позволяет спрятать ненужные подробности и детали реализации с глаз долой.

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

Теперь давайте посмотрим, в чем выражается абстракция и инкапсуляция в модных нынче ОО языках, типа C#.

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

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

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

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

Наследование является производным элементом, к тому же, далеко не самым простым и эффективным.

По сути, наследование и полиморфизм обеспечивают дополнительный уровень косвенности и аналогичную слабую связанность (low coupling) можно получить и другими способами: например, с помощью ad-hoc полиморфизма (строготипизированной утиной типизации) или с помощью делегатов.

Теперь, давайте перейдем к структурам и их связи с ООП. Структуры, как известно, обладают двумя ключевыми особенностями (в контексте ООП):

От них нельзя относледоваться (хотя они могут реализовывать интерфейсы) и

У них всегда есть конструктор по умолчанию (речь о C# и VB)

ПРИМЕЧАНИЕ
Здесь и далее я использую термин конструктор по умолчанию для структур. Говоря формально, default(CustomStruct) или new CustomStruct() не очень-то является конструктором по умолчанию. По сути, это способ создания экземпляра структуры с дефолтными значениями всех его полей. Но, поскольку термин прижился, я буду использовать именно его.

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

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

Если конструктор по умолчанию вполне подходит, как, например, для типа DateTime, то все нормально. А если не подходит? Тут, конечно, можно попросить клиента структуры им не пользоваться. Да, это вариант, но для этого клиент нашей структуры должен обладать тайным знанием, что этого делать не нужно. А это, ничто иное, как отсутствие того самого сокрытия информации, которое является ключевой характеристикой ООП.

Помогла статья? Оцените её!
0 из 5. Общее количество голосов - 0
 

You have no rights to post comments

Дмитрий Крикунов

Публикую статьи, обучающие курсы и новости по программированию: алгоритмам, языкам (С++, Java), параллельному программированию, паттернам и библиотекам (Qt, boost).