вторник, 1 декабря 2015 г.

О дружбе значимых типов с ООП

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

49 комментариев:

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

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

    Например, я не настаиваю, что это пример для подражания, но всё же: https://msdn.microsoft.com/en-us/library/jj873953.aspx

    "You can’t use the new operator to instantiate WRL components. Therefore, we recommend that you always use Make or MakeAndInitialize to instantiate a component directly."

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

      Ну и по поводу этого:

      > А вообще, не надо делать это тайным знанием и тогда это будет контрактом, а не анти-ООПшным раскрытием информации.

      Идея ведь конструктора в том, что инварианты выставлены. А в случае с дефолтным конструктором они могут быть нарушены. Да, можно это обойти это тем, что давать предусловие на каждый открытый метод: WasProperlyInitialize, который выставляется в True только в конструкторе с параметрами. Но это сложно и на практике этого никто не делает.

      В качестве пруфа, попробуй создать енумератор листа с дефолтным конструктором и вызове его метод MoveNext: new List.Enumerator().MoveNext();. В этом случае ты не получишь никаких InvalidOperationException. Этот метод тупо грохнется с NullReferenceException-ом, поскольку будет попытка обратиться к полю List, который будет null. Что, на самом деле, очень даже логично. Ведь структуры по своей сути - это механизмы оптимизации, а никто в высокооптимизированном коде не будет добавлять на потенциально горячий путь дополнительный проверки внутреннего состояния.

      Удалить
    2. Кстати, по поводу проверок - я бы их добавлял. Для DEBUG-а. Тогда никаких накладных расходов при работе, но при чтении исходников отличное понимание того, что это не случайный NRE - и даже специальные поясняющие комментарии не нужны.

      Удалить
    3. Мне кажется, что структуры больше для интеропа. Но тут конечно сложно сказать что важнее.

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

      Удалить
  2. >Идея ведь конструктора в том, что инварианты выставлены. А в случае с дефолтным конструктором они могут быть нарушены.

    но так как struct является тем, чем он является других вариантов просто нет. Если допустить, что C# разрешит убрать/засунуть в private дефолтный конструктор у struct, то моментально перестанет собираться код типа MyStruct[] tmp = new MyStruct[1];

    прям в как в плюсах ))

    ОтветитьУдалить
    Ответы
    1. Денис, в C++/CLI у структур есть полноценные конструкторы по умолчанию, да и их хотели добавить в C# 6.0 и отказались лишь за пару месяцев до релиза. В целом, иногда полезно их иметь, опять таки, жертвуя приведенным кодом.

      Удалить
    2. А кстати, чем мотивирован отказ?

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

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

      Ну а что касается примера с сокрытым дефолтным конструктором, то я думаю, сочинить синтаксис для инициализаторов можно было бы тоже, например:
      MyStruct[] tmp = new MyStruct[10] (index)=>{new MyStruct(index)};
      Такой syntax sugar должно быть можно сделать на уровне компилятора, не трогая платформу.

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

      Удалить
    3. Я не помню, чем отказ мотивирован, но даже сейчас есть возможность явного вызова кастомного дефолтного конструктора структур в массиве (ведь в других языках есть вомзожность создать дефолтный конструктор в структурах).

      Для вызова:

      var array = new CustomValueType[]{};
      array.Initialize();

      Удалить
    4. >Денис, в C++/CLI у структур есть полноценные конструкторы по умолчанию, да и их хотели добавить в C# 6.0 и отказались лишь за пару месяцев до релиза. В целом, иногда полезно их иметь, опять таки, жертвуя приведенным кодом.

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

      У меня, например, со struсt две типичных боли:
      1) хочется кастомный parameterless конструктор
      2) хочется избавиться от parameterless конструктора вообще

      кстати

      >MyStruct[] tmp = new MyStruct[10] (index)=>{new MyStruct(index)};

      прикольная идея

      Удалить
  3. С точки зрения инскапсуляции важно не сокрытие абстракции, но сокрытие данных от несанкционированного доступа и всё. Никаким инвариантам это не мешает, просто среди инвариантов появляется ещё одно состояние.

    Утёкшая абстракция - это совсем другого рода проблема. Как показывает моя например практика использования ImmutableArray<> (в котором как раз описанная ситуация), проблема совсем не стоящая беспокойства, хотя пока сам не начал пользоваться казалось совсем иначе.

    ОтветитьУдалить
    Ответы
    1. Слава, вот с первым определением я не очень согласен:).

      Вот определение инкапсуляции из книги Гради Буча:

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

      Так что инкапсуляция - это не только закрытые поля, это, все же, ИМХО, более широкое понятие.

      > Никаким инвариантам это не мешает, просто среди инвариантов появляется ещё одно состояние.

      Инвариант - это что-то, что истино всегда. Поэтому наличие дефолтного конструктора препятствует появлению инвариантов в структурах (именно поэтому инварианты из Code Contracts становятся не применимыми).

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

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

      Удалить
    2. Еще раз перечитал статью и все равно не смог побороть ощущение, что лично для меня ООП - это не столько наследование или инкапсуляция, сколько полиморфизм как способ абстракции. А инкапсуляция - это уже следствие. Я думаю, что язык в котором можно было бы реализовывать интерсейсы, но нельзя было бы делать приватные члены в реализациях, вполне бы мог претендовать на звание ОО. Просто изо всех щелей торчали бы фабрики, отдающие интерфейсы. И в этом плане то, что в C# value-типы могут реализовывать интерфейсы, - это здорово.

      С другой стороны, понятно, что в деле борьбы с ростом сложности и tight-coupling-ом все способы важны.

      Удалить
    3. Михаил, но ведь у вас в коде наверняка есть десятки классов без наследования или интерфейсов. Означает ли это что они не являются абстракциями? Например, StringBuilder получается не абстракция? Там явно есть инкапсуляция, ведь при переходе на .NET 4.0 реализация серьезно поменялась. Или Dictionary который вы используете неполиморфно. Означает ли это, что такой класс не является абстракцией если используется напрямую? ;)

      Удалить
    4. Сергей, конечно вы правы! Конечно, инкапсуляция важна, и конечно это способ абстрагирования. И вообще мы на самом деле говорим об одном и том же.

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

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

      Касательно же примера со StringBuilder — если воспринимать интерфейс как контракт, то у StringBuilder вполне есть контракт ) . Вот например в яве у StringBuffer и StringBuilder контракт один, просто первый еще и дает гарантии потокобезопасности. Ну а то, что для них не стали делать общий интерфейс — это отдельный разговор.

      Удалить
    5. Жаль, в блогспоте нормально плюсовать комментарии нельзя:). Плюсую предыдущий комментарий:)

      Удалить
    6. Согласен с Сергеем насчёт неотделимости инкапсуляции от абстракции. Выражусь более конкретно: я считаю, что инкапсуляция неразрывно связана с бизнес-логикой. Если объект не поддерживает инварианты = объект не работает. Как кто-то в таком случае может не задумываться об инвариантах и каким таким магическим образом они сами появятся?

      Удалить
  4. @Viacheslav: (делаю отдельную ветку, чтобы упростить беседу).

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

    ОтветитьУдалить
    Ответы
    1. По поводу твоих структур я ничего не могу сказать. Конечно, не каждый класс можно переделать в структуру. Как из этого может следовать то, что структуры не дружат ООП?

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

      Я, наоборот, показываю, что есть примеры структур, прекрасно с ООП дружащих. Это опровергает утверждение, что "структуры плохо дружат с ООП" как я его понял. Тут надо или возразить, что я ошибаюсь и в этих структурах ООП нет и рядом или признать, что да, _некоторые_ структуры конечно не объектно-ориентированны. Я вовсе не утверждаю, что любая структура объектно-ориентированна. Как и совсем не любой класс ;о)

      Попробуй применить свою теорию на практике на примере ImmutableArray<>. Что с точки зрения ООП там не так, что эту структуру нельзя считать тру ООПэшной? Это, подскажу, как раз пример стуктуры, в которой дефолтовый конструктор немного усложнил жизнь, но это не привело ни к чему страшному. Какие там проблемы с инвариантами? А с абстракциями что не так?

      Возрази мне на то, как здорово "отделяется поведение и устройство" в DateTime.

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

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

      Удалить
    2. Да, я полностью согласен что некоторые структуры (и их не мало) полностью объектно-ориентированы. Для этого нужно, чтобы не нужно было от них наследоваться и их конструкторы по умолчанию должны устанавливать валидные инварианты.

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

      Полностью согласен!

      З.Ы. "Плохо дружат с ООП" !== "Не дружат с ООП совсем" ;)

      Удалить
    3. Класс!!! А на счёт ревью - это в одной из следующих статей? Мне что-то показалось, что прямо тут должна быть отсылка на продолжение про ревью, что мы твиттере обсужали :о)

      Удалить
    4. Это будет в следующий раз:))

      Удалить
  5. Статья просто вовремя. Вот как раз с сокамерником спорили об абстракции и инкапсуляции. Как это выразить просто, понятно и без примеров. Вот в самое то статья. Спасибо

    ОтветитьУдалить
  6. Привет! Попробую адвокатом дьявола поработать :).
    >> А если не подходит? Тут, конечно, можно попросить клиента структуры им не >>пользоваться. Да, это вариант, но для этого клиент нашей структуры должен обладать >>тайным знанием, что этого делать не нужно. А это, ничто иное, как отсутствие того >>самого сокрытия информации, которое является ключевой характеристикой ООП.
    Т.е знание контракта (что класс должен делать) и знание о дефолтовом конструкторе - это разная информация? А если относиться к дефолтовому конструктору - как к контракту. Инварианты можно добиться по разному... Или например, некоторые классы требуют, чтобы вызовы методов осуществлялись только после Initialize (или что угодно инициализирующего). Опустим, что такое проектирование не айс, но все же. В общем, мое мнение - что использовать класс без какой-то информации КАК его использовать - фигню получишь. В этом смысле - знание о дефолтном конструкторе - такое же ограничение...

    ОтветитьУдалить
    Ответы
    1. Ну тут наверное скорее о том, какие выразительные средства предоставляет тебе язык. Если язык сам поддталкивает тебя к ногострелу, то наверное что-то не так.

      Удалить
    2. Позвольте тут не согласиться. На языке можно и матом, а можно и высоким политесом... Я видел код на С++, который выглядел как Perl, но видел и похожий на C# (в хорошем его виде). Сказать, что тут что-то не так - у меня язык не поворачивается :). Мой пост был о том, что в ЛЮБОМ случае - использование неких сущностей подразумевает ЗНАНИЯ об этих сущностях. И ограничение дефолтного конструктора - одно из таких знаний.

      Удалить
    3. Спору нет. Наверное можно и на брейнфаке писать красиво. Можно и на голом C сочинить ООП, но зачем? Я просто к тому, что особенности языка могут помогать выражать свои мысли, а могут мешать.
      На мой взгляд (и, если я правильно понял Сергея 1 то и на его взгляд тоже), особенности C# в работе со структурами имеют некоторые прискорьные стороны.

      Удалить
    4. Эти стороны - обратная сторона оптимизации. Value types хрень - "шо бы было быстро как в C++". Если хотите - можно провести аналогию - переменные циклов имеют названия i,j,k bla-bla-bla. Но с точки зрения self-explained названий переменных это беда-беда. Т.е в коде имеем нормальные переменные и i,j,k. Так и здесь - имеем нормальное наследование и инварианту и особенные value types. Если знаешь, где использовать i,j,k и value types - будет профит.

      Удалить
  7. Этот комментарий был удален автором.

    ОтветитьУдалить
  8. Вообще складывается ощущение, что в области базовых принципов ООП образовалось терминологическое болото. С некоторых пор наследование для меня это не базовая концепция ООП, а технический трюк, скрещивание полиморфизма с переиспользованием кода, wow эффект от которого был так велик, что его второпях записали в базовые концепции ООП. Подозреваю что если бы интерфейсы как тип появились раньше, то наследования классов могло бы и не случиться.
    Очень грустно видеть, что такие авторитеты как Гради Буч утверждают, что инкапсуляция это отделение интерфейса от реализации. Это само собой вытекающее следствие из такого свойства как полиморфизм. Гораздо полезнее под этим термином понимать что это упаковка данных и поведения в единую сущность, причем обязательно и данных и поведения, а не как написано в википедии и/или, потому как если у объекта нет поведения то это не объект, а просто структурированные данные.
    Учитывая выше написанное, считаю, что структуры хорошо дружат с ООП, единственное чем омрачается их дружба — это отсутствие дефолтных конструкторов, но, как и многое в программировании эта проблема решается добавлением дополнительного уровня абстракции.

    ОтветитьУдалить
    Ответы
    1. Александр, правильно ли я понимаю, что у этого класса все хорошо с инкапсуляцией:

      class Foo {
      public object data;
      public void DoSomething() { // uses data }
      }

      Ведь в нем есть упаковка объекта с данными.

      Ну хорошо, это может быть перебор. А вот такой пример:

      class Foo {
      // everything is private
      public void Setup() {}
      public void Initialize() {}
      public void Method3() {}
      // Методы должны вызываться в следующем порядке Initialize, Setup, Method3
      }

      Т.е. здесь нужно вызывать методы в правильном порядке. Обладает ли этот класс правильной инкапсуляцией? Данные от поведения отделены, но сокрытие инфомрации (а.к.а. тайных знаний о потенциальном устройстве) выставлено наружу.

      Критиковать бучей и мейеров можно и нужно, но при этом нужно быть готовым написать десяток статей и пару книг:))

      Ну и напоследок. Правильно ли я понимаю, что понятие сокрытия информации (а.к.а. инкапсуляция) полностью отсутствует в ФП, где данные и поведение четко разеделно? Если это так, то это странно, ведь без сокрытия информации построить сложные системы невозможно, а на ФП языках построено много чего;)

      Удалить
    2. А я соглашусь с Александром в том, что наследование — over-rated фича ООП, которая вероятно была важна в момент зарождения (темные века, программисты едят программистов) для демаркации ОО-мира от скажем структурного или модульного программирования. Но то, что оно до сих пор преподносится как чуть ли не основополагающая концепция — это прискорбно. Приводит к тому, что программист пытается наследоваться везде, где видит хоть чуть-чуть схожее поведение (в этом плане как раз очень показательна вошедшая в анналы ошибка с Point и Circle в сэмплах к Borland Pascal).

      Я вообще считаю, вслед за Блохом, что если вы оставляете класс доступным для наследования, то тем самым подразумеваете, что он спроектирован с таким расчетом. Поэтому на мой взгляд довольно странно, что в C# классы не sealed по-умолчанию. Ну а то, что в Яве методы по-умолчанию виртуальные — это вообще печаль.

      Возвращаясь к ответу Александра, соглашусь и со второй частью, что инкапсуляция есть логическое следствие полиморфизма. Если с классом ты работаешь через (хорошо спроектированный) интерфейс, то вся реализация и так получается инкапсулированной. Соответственно в примере с Method3 и порядком вызова проблема не в отсутствии инкапсуляции, а в неправильном проектировании интерфейса.

      Про ФП не скажу, ибо плаваю в этой теме, но мне кажется там совсем другие проблемы ;) На самом деле хотелось бы больше услышать про практику реального применения ФП в продакшен-системах.

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

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

      Вторая нелогичность связана со следующей фразой:

      > Если с классом ты работаешь через (хорошо спроектированный) интерфейс, то вся реализация и так получается инкапсулированной.

      Но если здесь нет полиморфизма, то откуда же инкапсуляция? или я неправильно понял "согласие" со второй частью?

      Реальное применение многих ФП практик сейчас найти довольно просто: в Roslyn-е используются многие ФП практики - иммутабельность, чистые функции. LINQ - это ФП в чистом виде. Closure со всякими неизменяемыми структурами данными, который написан на самом Closure - это пример ФП. Есть Scala, F#.

      Удалить
    4. Вот я об этом и говорю, вроде и говорим на одном языке и даже хорошо и плохо понимаем примерно одинаково, но терминологические расхождения очень велики.
      Попробую объяснить свою тз поподробнее.
      ООП это не язык и не фичи языка, это подход к декомпозиции проблемы и способ организации кода. Языки с поддержкой ООП позволили отобразить абстракции в коде более явным образом. Тогда как процедурные языки тоже позволяли иметь абстракции в коде только менее явно, в виде каких то соглашений по техник организации кода между программистами, т.е. уровня кодстайла. Поэтому хороший с тз ООП код, это набор качественных абстракций и как соответственно качественный набор абстракций дает хороший ООП код.
      Но хороший с тз ООП код еще не достаточен чтобы быть хорошим кодом с тз программного продукта и тем более большого программного продукта. С этой точки зрения очень легко ответить на вопрос почему так легко поддерживать в согласованном состоянии код когда пишешь программу один и так тяжело когда много и почему при не надлежащем контроле даже хороший -код быстро превращается в ком грязи. Абстракции которые заложил автор не очень хорошо видны последователям, вернее даже не сами абстракции, а границы этих абстракций, и со временем они размываются и абстракции становится плохими что ведет к плохому коду с тз ООП. Многим наверно знакомо это ощущение, когда формально все требования к хорошему коду выполнены, а все равно что то не так? ООП это набор взаимодействующих абстракций и только от качества этих самых абстракций и зависит качество взаимодействия между ними и как следствие понятность кода.
      Инкапсуляция(упаковка данных и поведения) это способ организации кода в современных ООП языках для лучшей организации абстракций в коде, это наименее адаптированная современными программистами часть методологии. Причем интересно что 3 амиго Буч, Якобсон и Рамбо толи не придавали большого значения, толи понимали по своему этот термин, что вполне возможно и послужило причиной такой низкой адаптации этого принципа в инструментариях современных программистов. В частности Якобсона с его юз-кейс анализом обвиняли что его метод провоцирует нарушение инкапсуляции(отделение данных от поведения).
      Возвращаясь к вашим вопросам. Первый класс может быть отличной абстракций с отличной инкапсуляцией. Второй класс плохая абстракция даже если у него тоже хорошая инкапсуляция.
      Публичное поле это не плохое ООП, это совсем другая категория.
      Про ФП сказать ничего не могу, подозреваю что там должны использоваться не совсем объектные категории.

      Удалить
    5. Сергей,

      > Но если здесь нет полиморфизма, то откуда же инкапсуляция? или я неправильно понял "согласие" со второй частью?

      Я тут имел в виду примерно то же, что и в случае со StringBuilder. Полиморфизм может быть просто подразумеваемым — когда проектируешь класс, то всегда подспудно помнишь, что на месте этого класса в клиентском коде может быть использован другой с таким же контрактом. Из этого естественным образом вытекает инкапсуляция реализации.

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

      Возвращаясь к изначальной теме поста, беда со структурами в C# как раз и заключается в том, что чтобы сделать правильный интерфейс (без состояния с нарушенными инвариантами), приходится идти на дополнительные уловки, например - выделять явный интерфейс и прятать реализацию в factory-метод, что в общем-то подрывает смысл value-типов.

      Удалить
    6. Александр,

      > Инкапсуляция ... это наименее адаптированная современными программистами часть методологии.

      Вот тут я что-то недопонял. Получается, что у нас сейчас нет правильного понимания/применения инкапсуляции? А можно по-подробнее? Мне казалось, что с инкапсуляцией все в общем-то хорошо и просто пока не приходится идти на компромиссы ради производительности.

      Удалить
    7. Про всех так сказать нельзя, но в среднем по больнице, я думаю, что нет именно явного понимания важности данного явления. А если человек под этим термином понимает тоже что и Гради Буч, тогда в его словаре вообще отсутствует прямой и понятный концепт, отвечающий за упаковку данных и поведения. Зато должно быть много понятий которые косвенно требуют того же, что плохо.
      Почему плохо. В качестве примера могу привести дядю Боба с его солидом. СОЛИД это не более чем еще один набор метрик для ООП кода. Это характеристика имеющейся объектной модели, а не способ ее создания. Но народ то, прочитав интересную и понятную книжку дяди Боба начинает код писать так чтобы он сразу удовлетворял солиду. Кто сталкивался тот понимает, о чем я. Т.е. у хорошей объектной модели и с солидом все будет хорошо, тогда как обратное не обязательно.

      Удалить
    8. С СОЛИДом на практике не сталкивался, да и вообще не слышал про него так давно, что аж побежал в википедию освежать в памяти.

      И про определение инкапсуляции от Гради Буча, я не очень понял в чем беда. Сергей его выше приводит, и во второй части мне кажется неплохо, хоть и расплывчато:

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

      как по-твоему надо его определить, чтобы всем было все понятно? Может правда стоит сформулировать хорошее определение, а чтобы не путать с "испорченным" термином - и термин другой придумать тоже?

      Удалить
    9. Оно слишком расплывчато. Если заменить термин на абстракция, полиморфизм или information hiding, то ничего не измениться.
      Вот две реализации делающие одно и тоже.
      1. point.Distance(anotherPoint)
      2. DistanceMeter.Distance(point, anotherPoint)
      Так вот определение Гради Буча не говорит какой из них более правильный.

      Уже все давно придумано. К сожалению нет одной правильной всесторонней книги по ООП прочитав которую можно спать спокойно.

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

      Если вы сравниваете объектно-ориентированный и структурный подход - то тут все (практически) однозначно. ОО вариант лучше.

      Но если второй вариант - это ФП, то тут ответ не будет столь очевидным. Точнее, он не будет очевидным вовсе.

      За примером далеко ходить не нужно: весь LINQ построен по принципу 2 и является очень удобным инструментом, который хоть и противоречит принципам ООП, является очень качественным и удобным.

      > К сожалению нет одной правильной всесторонней книги по ООП прочитав которую можно спать спокойно.
      Такая книга есть. Это книга Бертрана Мейере "Объектно-ориентированное конструирование программных систем";)

      З.Ы. Я правда очень рекомендую посмотреть на ФП. Это другой мир, расширяющий сознание с точки зрения техник проектирования. Очень-но рекомендую.

      Удалить
    11. @Mikhail: Про солид я тут писал недавно: http://sergeyteplyakov.blogspot.com/2014/10/solid.html

      Удалить
    12. Сергей,

      > З.Ы. Я правда очень рекомендую посмотреть на ФП. Это другой мир, расширяющий сознание с точки зрения техник проектирования. Очень-но рекомендую.

      Я был бы благодарен за рекомендации, как лучше подступиться к ФП. Дело в том, что без практического использования абстрактные концепции в программировании у меня быстро забываются. А подобрать задачу что-то не получется.

      Я к примеру, давно поглядываю на Цейлон (http://ceylon-lang.org/), там вроде бы много интересных идей реализовано, но что-то никак не придумаю, где мне его применить.

      > весь LINQ построен по принципу 2 и является очень удобным инструментом, который хоть и противоречит принципам ООП, является очень качественным и удобным.

      А в чем именно LINQ противоречит принципам ООП?

      Удалить
    13. Сергей, я не утверждаю, что ООП в принципе лучше, чем ФП, я скорее полагаю, что у каждого подхода своя ниша где он доминирует. Но когда мы выбрали парадигму, то давайте применять ее осознано. Поэтому мой вопрос, будучи заданным в контексте ООП, имеет однозначный ответ и вполне внятное обоснование.
      Кстати, поддерживаю Михаила, было бы хорошо если бы вы со своим уровнем владения ООП, смогли подобрать пример и показать преимущества ФП.
      Я несколько раз встречал утверждения, что ФП позволяет писать аналогичный функционал за существенно меньшее количество строк кода. Но я не понимаю за счет чего это происходит.

      Удалить
    14. Михаил,

      > Я был бы благодарен за рекомендации, как лучше подступиться к ФП.

      Я бы рекомендвал книгу Томаса Петричека "Real-World Functional Programming".

      > А в чем именно LINQ противоречит принципам ООП?

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

      К тому же, в LINQ-е есть куча downcast-ов с проверками конкретных типов коллекций.

      Удалить
    15. Александр,
      В своем предыдущем комментарии вы написали:

      > Вот две реализации делающие одно и тоже.
      > 1. point.Distance(anotherPoint)
      > 2. DistanceMeter.Distance(point, anotherPoint)
      > Так вот определение Гради Буча не говорит какой из них более правильный.

      > Уже все давно придумано.

      Я же возразил, что ничего не придумано. Поскольку тот же LINQ использует синтаксис #2, но скрывает это за методами расширения.

      Алексей, я опишу в отдельной статье сравнение, поскольку эта тема невероятно обширна.

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

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

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

      З.Ы. Можно погуглить "Failue of State" от ОО-гуру Боба Мартина.
      З.Ы.Ы. Сейчас на тему ФП ну очень много вещей есть. Тот же Розлин весь неизменяемый, те же Task-и тоже эхо ФП, те же Reactive Extensions - это все оттуда.

      Удалить
    16. Сергей,

      > Я бы рекомендвал книгу Томаса Петричека "Real-World Functional Programming".

      Спасибо, буду добывать на прочтение!

      > Ну, ООП говорит о единстве данных и поведения, а LINQ - это набор поведения, который прилеплен сверху от данных (от коллекций). Благодаря синтаксическому сахару вызовы методов LINQ-а выглядят экземплярными, но, очевидно, что это не так.

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

      > что вопрос разделения или объединения данных и поведения давно решен. Он решен в ООП по сравнению со стуктурным походм, но в ФП он активно используется.

      Спасибо за пищу для размышлений! Буду думать. И книгу читать.

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

      Истинная правда! Но что можно сделать? Только immutable-state? Но для больших состояний это очень накладно, да и вообще может быть нереализуемо.

      Удалить
  9. Мне кажется, проблема несколько надуманна.

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

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

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

    Данный вопрос в отрыве от контекста не имеет смысла. Например, Double(-1) ничем не хуже Double(1). Но только не в случае, когда это значение длины. Но объект-значение как правило бессмысленно без сущности, содержащей атрибут с этим объектом-значением.

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

    ОтветитьУдалить
    Ответы
    1. Александр, ставлю +100 500 под каждой мыслью! Если структуры используются для моделирования значений (т.е. неизменяемых объектов), то почти всегда значение, которое создается дефолтным коснтруктором будет валидным. Но, в реальном мире струткры используются не для моделирования значений, точнее не только для этого. Очень часто структуры используются для опимизации. Хороший пример - итераторы. Итераторы всех стандартных коллекций являются изменяемыми структурами, конструктор по умолчанию которого создает невалидный объект. Чтобы убедиться в этом, достаточно вызвать этот код: new List.Enumerator().MoveNext() и получить NullReferenceException.

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

      Удалить