Как стать автором
Обновить

Комментарии 26

А где здесь конкретно написано, что класс это не абстракция? У вас здесь как-то так получается:

> За годы преподавания и коммерческой разработки я повстречал много студентов и разработчиков с одним и тем же заблуждением насчет ООП: класс = абстракция.

Через несколько абзацев.

> Классы — это, по сути, абстракции механизмов группировки и обобщения человеческого мозга.

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

Вы бы лучше в статье исправили акценты, а-то у меня лично создалось впечатление, что мне сейчас статьей неоспоримо на пальцах докажут то, что класс != абстракция ни при каких обстоятельствах.
Поправил Спасибо.
Интересная статья, спасибо.

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

Спорное утверждение. Проектирование «сверху вниз» тоже имеет место, и, как мне кажется, более плодотворно. Сначала продумываются интерфейсы, и потом уже делается их реализация, с постепенным увеличением количества деталей. Особенно этот подход справедлив в функциональных языках вроде Haskell, но и в объектно-ориентированных он тоже применим.
Спорное утверждение. Проектирование «сверху вниз» тоже имеет место, и, как мне кажется, более плодотворно. Сначала продумываются интерфейсы, и потом уже делается их реализация, с постепенным увеличением количества деталей. Особенно этот подход справедлив в функциональных языках вроде Haskell, но и в объектно-ориентированных он тоже применим.

Классический пример, программа Copy, описанная в статье The Dependency Inversion Principle
void Copy() { int ch; while ((ch = Keyboard()) != EOF) { WritePrinter(c); } }
У меня есть метод для чтения с клавиатуры, метод для записи на принтер. Мне надо написать программу, которая читает с клавиатуры и выводит на принтер. Я взял буквально и написал. Здесь Keyboard() и WritePrinter() — более низкоуровневые компоненты, а Copy более высокоуровневый.Получили, что Copy жестко связана с низкоуровневыми компонентами. Обычно люди (с кем я сталкивался) так и поступают.

А принцип инверсии зависимостей как раз и говорит, что так делать не надо. Как раз и нужно продумывать интерфейсы для устройств четния и записи, а уж через них реализовывать Copy, то есть то, о чем Вы говорите.
Это если есть Keyboard() и WritePrinter(), ибо если нету, следующим шагом как раз начать писать их. И будет самое что ни на есть сверху вниз.
Согласен
Хорошо раскрыта тема хождения в магазин. Но статья, вероятно, хотела раскрыть тему роли абстракций в ООП. Наверное, это — один из способов частичного их выражения. Тема не раскрыта. Перечислены признаки абстракций, признаки ООП, ссылки на авторов, на принципы, отдельные упоминания принципов и терминов. А где вся тема? Где чётко очерченные границы вопроса? Например, англ. Википедия при немного большем объёме статьи даёт в несколько раз больше содержательной информации. en.wikipedia.org/wiki/Abstraction_%28computer_science%29. И список ссылок там гораздо ближе к теме.
статья, вероятно, хотела раскрыть тему роли абстракций в ООП. Наверное, это — один из способов частичного их выражения. Тема не раскрыта. Перечислены признаки абстракций, признаки ООП, ссылки на авторов, на принципы, отдельные упоминания принципов и терминов. А где вся тема? Где чётко очерченные границы вопроса?


В своей статье я хотел показать, что абстракции в ООП это следствия более общего механизма — абстрагирования, который свойственен вообще человеческому мышлению. Зачем это нужно знать? Многие ограничения классов, за которые их не любят, проистекают не из самой концепции классов, а из наших представлений о процессе мышления. Например, классы часто критикуют за то, что они жестко фиксируют набор методов, в то время как в жизни объект может менять свое поведение. Но, если призадуматься, например, когда я был маленький я представлял себе, что все дома параллелепипеды. Когда я вырос, и стал делать ремонт в квартире, то понял, как я жестоко ошибался. Но у меня по прежнему в голове сохранились эти две абстракции. Только теперь я знаю, когда удобнее пользоваться одной, а когда — другой. То есть не сама абстракция поменялась, а добавилась новая, плюс правило, когда их нужно применять. Так и в ООП: The Open-Colsed Principle, который фактически утверждает, что для нового поведения надо писать новый класс.

Википедия при немного большем объёме статьи даёт в несколько раз больше содержательной информации. en.wikipedia.org/wiki/Abstraction_%28computer_science%29. И список ссылок там гораздо ближе к теме.

Статья действительно хороша. Но она опять же она говорит про абстракцию именно «in Computer Science» и никакой связи с общим механизмом. Цель моей статьи показать эту связь.

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

Неужели этого не понимают (студенты)? Абстрагирование — понятие изначально из философии, появление аналогичного термина в ООП наводит на мысль, что неспроста: ).

Но не суть. Основное, что я хотел бы сообщить с позиции стороннего наблюдателя — статья — в ряду таких же других, туманно объясняющих связь понятий абстракции, не вызывает эффекта «Вау, так вот для чего ООП!». Перечислили свойства абстракций (правильно, хорошо, находчиво выбраны примеры) — упомянули принципы ООП. Каждый читатель уныло отмечает: «Да, потому что так надо», откровения не происходит. Но Вы — не первый, так что всё нормально: ). Потому что ООП — конгломерат нескольких принципов (правил, паттернов — кому как угодно). Чтобы его понять, надо понять, почему все они сошлись в одном месте. (Я сам не могу показать, где и у кого это лучше читать, просто констатирую.)
Неужели этого не понимают (студенты)? Абстрагирование — понятие изначально из философии, появление аналогичного термина в ООП наводит на мысль, что неспроста: ).


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

Каждый читатель уныло отмечает: «Да, потому что так надо», откровения не происходит. Но Вы — не первый, так что всё нормально: ).


Но это не значит, то не надо пытаться.

P.S.: Жаль, что моя статья ничем Вам не помогла. Буду работать на улучшениями дальше
Но на мой взгляд про них лучше прочитать в статьях по первой ссылке.

Простите, не понял, что за первая ссылка?
Извините, поправил — вставил ссылки на сами статьи.
Класс квадрат нельзя унаследовать от прямоугольника?!
Да как нефиг делать!

Если функции, изменяющие объект суперкласса, ломают контракт субкласса, — то нужно уточнить контракт этих функций, вот и всё.
Например, вместо требования
rect.setExtent(w,h); assert(rect.width() == w && rect.height() == h);
где rect — это или Rect, или унаследованный от него Square,
выдвинуть требование
  • if(rect.setExtent(w,h) == SUCCESS) assert(rect.width() == w && rect.height() == h); — допустить возможность неудачи
  • rect.setExtent(w,h); assert(max(rect.width(), rect.height()) == max(w,h)); — допустить неточное следование в желаемых рамках

Кстати об изменениях. В языке C++ есть такое явление, как срезка: когда объекту наследника присваивают значение предка, выполняется унаследованное присваивание предку, и в результате получается химера из старых и новых значений.
Это напрочь ломает принцип подстановки Лисков, но вполне вписывается в систему наследования.
Уточните, пожалуйста, что Вы имеете ввиду

… но вполне вписывается в систему наследования.
Я имею в виду, что наследование в С++ не является субклассированием в терминах Лисков.
Наследник дополняет предка своими членами-данными и новыми функциями, переопределяет виртуальные функции (в этом месте соблюдение контрактов и следование LSP — личная ответственность программиста, а язык ему лишь помогает) и перегружает и прячет невиртуальные функции. В том числе, оператор присваивания.

Вот эта перегрузка-перепрятывание — это уже прямая поломка LSP.
template<class T> void assignBase(T& dst) { dst = Base(1,2,3); }
int main()
{
  Base b; assignBase(b); // ok
  Derived d; assignBase(d); // ошибка: нужная функция куда-то пропала!
}


Ну хорошо, наследник ещё и размером отличается от предка, и типом, поэтому передавать его в шаблон или в макрос — это не совсем тот случай, который имела в виду Лисков.
Давайте по ссылке передадим:
void assignBase(T& dst) { dst = Base(1,2,3); }
int main()
{
  Base b; assignBase(b); // ok
  Derived d; assignBase(d); // функция, оказывается, никуда не пропала, но возможно, работает не так, как мы хотим
}

И здесь уже всё зависит от того, устойчив ли наследник к срезке, или нет. Позволительно ли без ведома объекта поковыряться в его базе.
Язык здесь не препятствует возможным нарушениям LSP, а заодно затрудняет его соблюдение.
(Чтобы соблюсти, — возможно, придётся в операторе присваивания сделать вызов виртуальной функции — фактического обработчика присваиваний; а это громоздко).

Это я не в упрёк С++, а просто для того, чтобы подчеркнуть: наследование и субклассирование — это вовсе не одно и то же.

_____
Очень простой пример субкласса, безо всяких «квадрат-прямоугольник».
class Number
{
   int value;
public:
  explicit Number(int v) : value(v) {}
  .....
};
class Number0To100 : Number // подкласс целых чисел
{
public:
  explicit Number0To100(int v) : Number( (assert(v>=0 && v<100), v) ) {}
};

Для in-параметров — источники типа Number0To100 являются подклассом типа Number; но для out-параметров — наоборот, приёмники Number являются подклассом типа Number0To100.
Фигня в том, что наследование соединяет в себе свойства и источника, и приёмника.

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

Поэтому — или LSP, или вся полнота возможностей языка. Верёвка заряжена разрывными.

Отвечу Вам словами Бьярна Страуструпа: "должна быть предоставлена возможность описать ожидаемое поведение объекта базового класса таким образом, чтобы можно было составить программу, ничего не зная о производном классе. " (Язык программирования С++. Специальное издание Глава 24. Проектирование и программирование. 24.3 Классы стр. 813). Автор C++ за LSP!

Срезка — это расплата за возможность работать с объектами по значению. "Этот эффект часто называют срезкой; он приводит к ошибкам и чреват сюрпризами. Одной из причин передачи указателей и ссылок на объекты в иерархии является желание избежать срезки." (Страуструп, Язык С++. Специальное издание. Глава 12. Производные классы. 12.2.3. Копирование стр. 355) Так что LSP еще одно подтверждение, что срезку стоит избегать.

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

Пример с квадратом и прямоугольником показывает, что нарушить LSP можно в любом ОО языке программирования, не вдаваясь в особенности синтаксиса.

LSP, если сравнивать со естественным языком, это одно из правил грамматики, которое накладывает ограничение на совместное использование слов (классов). Человек может писать с ошибками, игнорируя или просто не зная правила, и язык ему в этом воспрепятствовать никак не сможет.
(Чтобы соблюсти, — возможно, придётся в операторе присваивания сделать вызов виртуальной функции — фактического обработчика присваиваний; а это громоздко).

Насчет виртуальных функций в операторе присваивания — был такой прием — двойная диспетчеризация — это как то, о чем Вы пишите.
Если функции, изменяющие объект суперкласса, ломают контракт субкласса, — то нужно уточнить контракт этих функций, вот и всё.
Например, вместо требования
rect.setExtent(w,h); assert(rect.width() == w && rect.height() == h);
где rect — это или Rect, или унаследованный от него Square,
выдвинуть требование
if(rect.setExtent(w,h) == SUCCESS) assert(rect.width() == w && rect.height() == h); — допустить возможность неудачи
rect.setExtent(w,h); assert(max(rect.width(), rect.height()) == max(w,h)); — допустить неточное следование в желаемых рамках


Вообще-то, в примере из статьи The Liskov Substitution Principle функция, устанавливая высоту 5, а ширину 4, на выходе выдает прямоугольник площади 20! Поэтому, когда мы на вход подаем квадрат, получаем фигуру площади 25 или 16 в зависимости, какая из размерностей будет установлена последней. Ослабление постусловия в этом случае, приведет к потере всякого смысла от использования самой функции.
Это если мы в контракт функции запишем: передать туда 4 и 5, получить обратно 20.
А если в контракте будет «вписать фигуру в указанные габариты» (например, мышкой за угол картинки в фотошопе потянули… а у ней пропорции залочены) — то всё честно: туда 4 и 5, обратно 4 и 4, или 5 и 5, по вкусу.
Ваш пример только подтверждает мысль, которую я озвучил в статье
Еще одно важное наблюдение — невозможно по самим абстракциям определить насколько удачными они получились. Это можно сделать, только если мы попытаемся их использовать на практике. И тут уж выясняется, что одни абстракции лучше подходят для задачи, а другие — хуже. А если еще немного изменить исходные условия, то и прежний «хороший» набор абстракций уже может не работать.

Только Вы идете от обратного — если немного изменить исходные условия, то «плохие» абстракции могут вполне работать.
Это Бродвей там, на картинке Барселоны, поперек города по диагонали идет?
Хорошая картинка, ее можно использовать как иллюстрацию фрактальной модели и ZUI.

Еще можно предложить попытку поделить абстракции на исследовательские и инженерные. Исследовательские абстракции это совокупность ключевых элементов и связей, несущественных элементов и связей, и границы между ними по отношению к объекту.
А инженерные это в основном первое - близко к тексту любого учебника: все самое важное БЕЗ лишнего.
Нигде не встречал акцента на этом, но разница если подумать - чудовищна. Исследовательская абстракция, а точнее культура применения именно такого типа абстракций, подразумевает что все ее части заданы для носителя в явном виде, что он понимает перемещаемость границы, рефлексирует ее расположение. А человек с инженерной абстракцией - может владеть ею и ничем вышеперечисленным - одновременно.

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

Зарегистрируйтесь на Хабре, чтобы оставить комментарий