?

Log in

No account? Create an account
   Journal    Friends    Archive    Profile    Memories
 

Современное ООП - morfizm


Dec. 1st, 2017 06:41 pm Современное ООП

Помню 20 лет назад учил детей на курсах, что инкапсуляция это круто. Каноническим примером был объект круг, умеющий себя рисовать. Правда же, здорово: описание данных (координаты круга, радиус) находятся рядом с описанием действий (метод draw). Был также родственный канонический пример наследования: цветной круг, в котором наследуются координаты и радиус, но появляется также атрибут "цвет", он же канонический пример полиморфизма: метод рисования перекрыт, потому что теперь нужно рисовать в цвете.

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

Что я забыл упомянуть, так это то, что инкапсуляция тоже хороша далеко не всегда. Хрестоматийный пример с кружком и рисованием это как раз неудачный пример инкапсуляции. Хорошие примеры атрибутов в классе это сложные состояния объекта, приватные мутабельные поля. Мутабельные объекты нужно инициализировать. Это накладывает жёсткие ограничения на сериализацию коллекций таких объектов. Поэтому поля сериализации обычно объединяют в отдельный класс, который "простой", в идеале в нём немутабельные публичные поля и больше ничего. Можно добавить методов, которые не требуют промежуточного состояния. Например, разные конструкторы или всевозможные хелперы для конвертации или вычисляемые атрибуты. Релизаций рисования может быть много, поэтому рисование выделяется в интерфейс, и может быть много классов, его по-разному реализующих. Понятно, что при этом описание круга (координаты, радиус, цвет) должны быть выделены в отдельный класс, рисовальщик будет принимать объекты-круги через параметры.

Я, вот, столкнулся с библиотечным классом, в котором этот принцип был нарушен (в нём было нечто похожее на класс круг как с координатами, так и с рисованием), и сразу возникло неприятное ощущение. Не понятно, как расширять функционал. Наследовать от такого класса - противно. Нужно будет внимательно следить, чтобы все методы были статичными, иначе прозеваешь и не сможешь корректно сериализовать, или будешь платить overhead'ом на сохранение ненужного тебе состояния. Делать же всё по уму (выносить данные в отдельный класс, а методы в интерфейс) требует создания кучи адаптеров. Тоже противно. Ну, значит, чтобы потом не было противно, лучше изначально так не делать. Круг, умеющий себя рисовать, это плохой круг. Хороший круг - пассивный круг, просто знающий, где он и какого он размера :) А рисовальщик живёт от него отдельно.

Upd.: если наследовать, то возникает ещё такая проблема. Расширенный функционал может требовать дополнительных параметров. Например, крутому рисовальщику-v2 нужен объект "кисть". Но в параметрах метода draw нет кисти. Если кисть достаточно сложна, что нельзя создать временную кисть прямо в draw, то ссылку на кисть можно передать только в конструкторе. Это накладывает кучу ограничений, например:
- вот теперь уже точно нужно специально выкручиваться, чтобы сериализовать класс, потому что придётся исключать из сериализации эту кисть.
- чтобы нарисовать круг другой кистью, придётся создавать его заново.
Бррр.

41 comments - Leave a commentPrevious Entry Share Next Entry

Comments:

From:birdwatcher
Date:December 2nd, 2017 02:57 am (UTC)
(Link)
Интересно, я вообще не думаю о классе как о модели какого-то реально существующего артефакта предметной области. А только как о машинке внутри моей собственной программы на C++, с помощью которой можно веселее и короче получать результат, чем если бы то же самое писать на K&R C.
From:morfizm
Date:December 2nd, 2017 03:06 am (UTC)
(Link)
Классы нужны для maintainability, когда в системе много всего, её пишут разные люди, включая людей, не работающих в вашей компании (библиотеки).

Мне кажется, моделирование артефакта предметной области классом, как таковое, не требуется, просто оно нередко даёт какие-то автоматические удобства. Рисуешь на доске артефакт предметной области, разговариваешь с ребятами о его взаимодействии с другими артефактами, и потом идёшь пишешь код. Естественным образом получается 1-1 mapping из артефактов в классы.

Но разные полезные практики, которые, в целом, на длинном забеге, повышают maintainability, нередко требуют вспомогательных классов. Например, "рисовальщик" это не артефакт предметной области. Это вымышленная и притянутая за уши абстракция. Но если не выделить его в отдельный класс, то потом пожалеешь. Я только что обновил пост и добавил ещё один пример проблемы.
From:morfizm
Date:December 2nd, 2017 03:08 am (UTC)
(Link)
Ещё, конечно, они нужны чтобы писать generic код. Всевозможные контейнеры и алгоритмы. Generic иногда полезно писать даже когда нет reusability, просто потому что generic легче тестировать. Допустим, тебе нужен алгоритм, работающий с каким-то сложным типом данных, который тащит кучу разных соплей. Но если параметризовать и использовать тип A, то, во-первых, сопли тащить не придётся, они нужны будут только в месте инстанциирования (а там, возможно, они уже притащены по каким-то другим причинам), а, во-вторых, для тестов можно вместо типа A подставить какой-нибудь Int, и тесты будут простые, читаемые, без сложного setup'а.
From:birdwatcher
Date:December 2nd, 2017 03:11 am (UTC)
(Link)
В принципе ничто не мешает добавить поддержку темплейтов в K&R. Это ортогонально объектности.
From:morfizm
Date:December 2nd, 2017 03:34 am (UTC)
(Link)
Там где сегодня скаляр, завтра захочется использовать struct. Структуры это уже почти объекты. Наборы функций для работы именно с этими структурами можно конечно, выделить комментариями, типа так:
/*
 * -- Helpers for MyStruct --
 */
helper1...
helper2...
helper3...


А можно поместить в класс, назвав его интерфейсом MyStructOperations.
По сути это тот же комментарий, только parseable.

А потом возникают всякие side benefits, например, возможность переопределить эти операции но так, чтобы какой-то другой алгоритм менять не пришлось, чтобы он автоматически юзал новую имплементацию. Тут уже без объектов обойтись ещё сложнее (передавать closures с колбеками?)
From:morfizm
Date:December 2nd, 2017 03:10 am (UTC)
(Link)
Ну и ещё, когда language framework весь насквозь ООП-шный, то нередко приходится использовать классы хотя бы по этой причине. Например, если на C++ и Python ещё можно обойтись длинной лапшой, то на Java/Scala без классов далеко не уедешь.
From:birdwatcher
Date:December 2nd, 2017 03:15 am (UTC)
(Link)
Всегда можно иметь один класс, без состояния, с только статическими функциями, и всю свою программу поместить в метод main(). Если люди так не делают, то это не из любви цветным кругам, наследующим палитрам и черно-белым кругам, а по практическим соображениям.
From:psilogic
Date:December 2nd, 2017 06:42 am (UTC)
(Link)
[ современное ООП трансформировалось, как наследование, так и полиморфизм, в общем виде признаны опасными ]

- на научной конференции дебилов и имбецилов? :)

для кисти сериализуешь только параметры, в draw 1 раз ее создаешь, если еще не создана, и оставляешь првязанной к объекту
From:natpy
Date:December 2nd, 2017 03:31 pm (UTC)
(Link)
Мне тоже кажется что многие утверждения притянуты за уши.
Интерфейсы так же не нарушают общую оопшную парадигму, только делают ее более гибкой
From:psilogic
Date:December 2nd, 2017 06:18 pm (UTC)
(Link)
Периодически возникают гуру, которые с пафосом заявляют, что тот или этот инструмент не следует использовать.

Начиналось все невинно - с оператора goto, а сейчас уже и до наследования добрались. Адепты плоской земли :)
From:morfizm
Date:December 2nd, 2017 07:04 pm (UTC)
(Link)
Не, не так. Гуру придумывают всякие гибкие абстракции, ООП, UML, а потом инженеры упрощают и выпиливают всё лишнее.
From:morfizm
Date:December 2nd, 2017 07:03 pm (UTC)
(Link)
Интерфейсы не добавляют гибкости, они её отнимают (!), чтобы решить проблемы с теми абстракциями, что были. Наследование намного гибче, ведь оно позволяет делать всё, что позволяют интерфейсы, плюс ещё много. Только использовать его более опасно, чем использовать интерфейсы.
From:morfizm
Date:December 2nd, 2017 06:57 pm (UTC)
(Link)
А что, признают что-то опасным только на конференциях? Никаких проблем, я смягчу формулировку. Заменил "признаны" на "считаются".

Твои оба примера - хороший путь к созданию трудносопровождаемого кода.
Ты предложил:
1. Сериализовать поля выборочно. Кастомная сериализация это крепкое усложнение, сразу повышает шанс багов по невнимательности.
2. Создавать сторонний объект прямо в draw. Расскажи мне, как ты напишешь к этому юнит тест, я посмеюсь.
3. Создавать сторонний объект, кэшировать его и повторно использовать. Во-первых, всё равно не решаешь проблему с разными кистями (основной объект нужно будет пересоздать если кисти нужны разные), во-вторых, такое вот кэширование это дополнительный мутабельный state, значительное усложнение, выше шанс багов.
From:psilogic
Date:December 2nd, 2017 09:41 pm (UTC)
(Link)
[ А что, признают что-то опасным только на конференциях? ]

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

[ 1,2,3 ]

Как правило альтернативы выгладят раз в 10 сложнее, но Васям и Петям пофиг, у них идиосинкразия к goto, наследованию... (тут подставить что угодно), и ради этого они готовы делать в 10 раз сложнее.
From:morfizm
Date:December 2nd, 2017 09:48 pm (UTC)
(Link)
Я не против, если что-то выглядит в 10 раз сложнее, если на самом деле оно проще.

Прежде, чем спорить, я бы хотел убедиться, что метрика сложности у нас одинаковая. Я не измеряю сложность в количестве строк кода или синтаксических элементов. Я её измеряю в количестве денег, требуемых на саппорт проекта, при условии, что над ним работают как минимум человек 10, а люди приходят и уходят. Ну т.е. если какая-то конструкция добавляет 5 минут к разработке первой версии, но экономит человеко-неделю на саппорте, потому что минимизирует шанс бага, который, будучи возникнутым, потребует два дня разбрираться, что сломалось, день фиксить и два дня тестировать и выкатывать, то я считаю, что эта конструкция упрощает разработку. Её net benefit одна человеко-неделя минус 5 человеко-минут.