Здравствуйте, Кодт, Вы писали:
vsb>>Речь о ручном делегировании? Если язык поддерживает делегирование на уровне синтаксиса, почему оно дороже?
К>Синтаксис уменьшает писанину, но ненамного. В ATL делегирование интерфейса агрегату (и, одновременно, предоставление ему своих интерфейсов) займёт ровно одну строчку. К>Плюс пару строк обвязки в коклассах главного объекта и агрегата, — но они так и этак добавятся.
В каком-нибудь Go это тоже одна строчка, без оговорок на "обвязку".
В D примерно две строчки.
Здравствуйте, 0BD11A0D, Вы писали:
К>>Чем делегирование хуже: оно дороже по памяти, по скорости, ну и по писанине тоже. BDA>А вот это хотелось бы в виде кода, поскольку там вероятны какие-то проблемы с декомпозицией.
Данные:
Наследник содержит данные свои и предков в одном блоке памяти. Также там содержатся один или несколько vfptr'ов, по количеству интерфейсов и базовых классов. Данные делегата живут в отдельном блоке. (Исключение — C++, где делегата можно добавить прямо в структуру владеющего объекта).
У владельца есть указатели (за исключением) на делегатов. У делегатов — есть vfptr'ы их собственных интерфейсов, плюс, скорее всего, указатели на владельца (и вот тут уже исключение сделать не получится). На ровном месте получили в 3 раза больше указателей!
Статический вызов метода: при наследовании — сместить базу (если надо) и вызвать функцию; при делегировании — взять смещение поля делегата, разыменовать (за исключением, см.выше), вызвать.
Динамический вызов:
при наследовании —
— разыменовать vfptr (обычно он под нулевым смещением),
— взять адрес функции в vtable и вызвать,
— если наследование со смещением базы, — то в vtable находится промежуточная функция, которая сместит базу и перейдёт к функции предка
при делегировании —
— взять смещение поля делегата,
— разыменовать,
— разыменовать его vfptr (у нас ведь всё равно есть наследование интерфейсов),
— взять адрес функции в vtable и вызвать (одна радость, там не будет кода по смещению базы)
Обратный вызов, из базы в наследника, из делегата во владельца
при наследовании —
— взять vfptr, далее понятно
при делегировании —
— взять поле указателя на владельца, разыменовать... далее понятно
Ещё один важный момент.
При наследовании — мы "из коробки" получаем объект, внешне выглядящий монолитно.
class Composite; .....
struct IA; struct IB; // неважно, как они там раскиданы по базам
Composite* c;
IA* a = dynamic_cast<IA>(c); // иногда и static_cast'а достаточно, ну неважно
IB* b = dynamic_cast<IB>(a);
IA* a2 = dynamic_cast<IA>(b);
assert(a == a2);
При делегировании — надо специально позаботиться, чтобы делегат мог вернуться к владельцу. Иначе это будет дорога в тупик.
Это можно делать руками в каждом конкретном случае, или руками на уровне инфраструктуры (пример из COM — специальные опции при создании CComObject и реализация GetControllingIUnknown), или компилятором — понапихать служебных указателей в каждый потенциально-делегируемый класс.
К>>>Чем делегирование хуже: оно дороже по памяти, по скорости, ну и по писанине тоже. BDA>>А вот это хотелось бы в виде кода, поскольку там вероятны какие-то проблемы с декомпозицией.
К>Данные: К>Наследник содержит данные свои и предков в одном блоке памяти. Также там содержатся один или несколько vfptr'ов, по количеству интерфейсов и базовых классов. К>Данные делегата живут в отдельном блоке. (Исключение — C++, где делегата можно добавить прямо в структуру владеющего объекта). К>У владельца есть указатели (за исключением) на делегатов. У делегатов — есть vfptr'ы их собственных интерфейсов, плюс, скорее всего, указатели на владельца (и вот тут уже исключение сделать не получится). На ровном месте получили в 3 раза больше указателей!
К>Статический вызов метода: при наследовании — сместить базу (если надо) и вызвать функцию; при делегировании — взять смещение поля делегата, разыменовать (за исключением, см.выше), вызвать.
К>Динамический вызов: К>при наследовании - К>- разыменовать vfptr (обычно он под нулевым смещением), К>- взять адрес функции в vtable и вызвать, К>- если наследование со смещением базы, — то в vtable находится промежуточная функция, которая сместит базу и перейдёт к функции предка К>при делегировании - К>- взять смещение поля делегата, К>- разыменовать, К>- разыменовать его vfptr (у нас ведь всё равно есть наследование интерфейсов), К>- взять адрес функции в vtable и вызвать (одна радость, там не будет кода по смещению базы)
К>Обратный вызов, из базы в наследника, из делегата во владельца К>при наследовании - К>- взять vfptr, далее понятно К>при делегировании - К>- взять поле указателя на владельца, разыменовать... далее понятно
К>Ещё один важный момент. К>При наследовании — мы "из коробки" получаем объект, внешне выглядящий монолитно. К>
К>class Composite; .....
К>struct IA; struct IB; // неважно, как они там раскиданы по базам
К>Composite* c;
К>IA* a = dynamic_cast<IA>(c); // иногда и static_cast'а достаточно, ну неважно
К>IB* b = dynamic_cast<IB>(a);
К>IA* a2 = dynamic_cast<IA>(b);
К>assert(a == a2);
К>
К>При делегировании — надо специально позаботиться, чтобы делегат мог вернуться к владельцу. Иначе это будет дорога в тупик. К>Это можно делать руками в каждом конкретном случае, или руками на уровне инфраструктуры (пример из COM — специальные опции при создании CComObject и реализация GetControllingIUnknown), или компилятором — понапихать служебных указателей в каждый потенциально-делегируемый класс.
Я очень сильно извиняюсь, что заставил столько текста написать, но я имел в виду совсем другое. Где промышленный код, в котором все эти проблемы заложены? С размытиями контрактов и т.д.
Вот я беру несколько проектов из реальной жизни и открываю их код. Чаще всего я вижу голые процедуры. В проектах вообще нет классов, поскольку они простые, как дверная ручка. Какой там vtbl.
Например, проект, написанный мною вчера вечером, когда я узнал, что мой кухонный компьютер не уходит в sleep из-за того, что в Windows 7 какие-то умники запретили сон при наличии открытых файлов из сетевой шары. То есть, вывел его с утра из сна пультом, нажал Play, тем самым открыв плейлист с файлами из сетевого хранилища, позавтракал под музыку, ушел из дома, вернулся вечером, а он весь день, оказывается, жарил стенку, потому, что — шара же. Сначала я, как водится, почитал доку. Оказалось, что есть powercfg /requests, который показывает, почему не произошло sleep'а. Потом стал смотреть, как отменить такое поведение. Оказалось, надо в реестре нашаманить, чтобы только опция в GUI появилась. Настроил опцию — не работает. Ну хорошо, написал код, который форсит сон, заодно повесил выключение на пультовую кнопку.
Бывают проекты сильно, очень сильно, сложнее в плане функционала, которые годами пишутся несколькими людьми, но они недалеко ушли по структуре от «скрипта», показанного выше. Их GUI (самое ООшное место обычно) вынесен в HTML и полностью отвязывается от т.н. «бизнес»-логики, а сама эта «бизнес»-логика объективно оказывается набором действий. Добавление классов не улучшает читаемость, а только все усложнит (как усложнило бы все добавление класса PowerManager в примере выше — совершенно искуственная сущность). Конечно, чужие классы при этом используются активно, например, строки и контейнеры.
Это, напомню, то, что я вижу чаще всего.
Реже я встречаю классы. Например, вот такой проектик: есть база данных, с которой работает администратор. Администратор создает таблицы для простых пользователей, а потом добавляет разные автоматизирующие штуки. Например, если в одной таблице подставлена ссылка на справочник, то при изменении в справочнике изменения копируются в исходную таблицу. Получается реляционность, прилепленная к чужой программе типа Excel'а. Или, скажем, суммирование. Если одна таблица содержит колонку, которую можно просуммировать, сумма должна автоматически обновляться во всех таблицах, куда она включена (как сумма).
В мануале администратора написано: вы можете автоматизировать работу пользователей вашего, условно говоря, Excel'а, поставив нашу утилиту и создав специальную таблицу с правилами. Если вы хотите реляционность — укажите тип правила «Реляционность». Хотите суммирование — укажите тип правила «Суммирование». Хотите, чтоб при изменениях юзеру в мессенджер приходило извещение, или генерировался документ на каждую запись, или еще чего-нибудь — укажите соответствующий тип правила. Для каждого правила, независимо от типа, все равно нужно заполнить колонки «Таблица-источник», «Колонка-источник», «Целевая таблица», «Целевая колонка».
Это естественнейшим образом ложится на базовый класс Rule, который, например, считывает источники и таргеты, и на производные классы LookupRule, SumTotalRule и так далее.
Когда новый программист читает мануал администратора (то есть, пользователя проекта), он напитывается понятиями этого самого пользователя («правила администратора», «источники», «цели»), а потом смотрит в код и видит результат декомпозиции: класс Rule, дочерние классы LookupRule, SumTotalRule, строковые поля SourceColumn, SourceTable и так далее.
Отсюда ясно читается, что все правила имеют общие черты и показано, какие именно. И это соответствует тому, что видит пользователь. А кроме того, показано, что правило суммирования внезапно — частный случай подстановки из справочника. Для того и классы (все сложные сущности из мануала, чтобы было легче искать привязки к коду). Для того и наследование (чтобы видеть, что является частным случаем чего).
***
Дело в том, что программирование вообще — очень простое занятие, если специально не усложнять. Чем программисты любят заниматься, либо чтоб не уволили, либо из естественного любопытства, не добавляя ценности. Если думать над ценностью, грамотно проводить декомпозицию, код со всей его структурой будет очень прост. Я просто не могу представить себе, какова должна быть предметная область, чтобы делегирование в коде ее адекватно передавало и было способом упростить чтение кода. Я и прошу: может кто-нибудь привести пример?
***
Что касается ручной возни с vtbl'ом, я вообще не понимаю, что это и зачем. Знать про vtbl полезно, если ты компиляторы пишешь. Или если нечаянно испортил память, найти место, где ломаешь вызовы и крэшишь все. Оптимизация, когда расходы на виртуальные вызовы важны? Бывает крайне редко, но тогда пиши на голом C с хендлами. Здесь о другом, здесь об организации кода.
Здравствуйте, 0BD11A0D, Вы писали:
BDA>Здравствуйте, AlexRK, Вы писали:
BDA>>>С моей точки зрения, делегирование по сути своей — весьма спорный паттерн ARK>>А в чем именно он спорный? Мне всегда казалось, что в правильно реализованном делегировании нет мест, где можно случайно наломать дров.
BDA>А вы приведите хоть один пример с правильным делегированием, его и разберем.
Большие компоненты строятся из маленьких, часть поведения напрямую делегируется внутренним элементам (подэлементам). Или наоборот маленькие строятся из больших — когда для проекта необходима только часть свойств некой системы, то для упрощения кода нужный интерфейс выделяется в отдельный класс.
Если же говорить не про делегирование вообще, а про "делегирование vs наследование", то вот пример: имеешь реализованную структуру "список", надо построить "стек". Можно конечно унаследовать стек от списка, да вот только стек это не список. Лучше спрятать список внутри реализации, а некоторые команды стека делегировать списку.
Здравствуйте, Буравчик, Вы писали:
BDA>>А вы приведите хоть один пример с правильным делегированием, его и разберем. Б>Если же говорить не про делегирование вообще, а про "делегирование vs наследование", то вот пример: имеешь реализованную структуру "список", надо построить "стек". Можно конечно унаследовать стек от списка, да вот только стек это не список. Лучше спрятать список внутри реализации, а некоторые команды стека делегировать списку.
Вот нет бы сразу код написать.
1. Какой интерфейс у списка, а какой у стека? С моей точки зрения, список должен иметь Add(), Insert(), Remove(), а стек — Push() и Pop(). Даже в STL, который образец нечитаемого кода, в std::vector метод называется push_back(), а в std::stack — просто push().
В вашем же вопросе неявно задано, что стыковка с аггрегируемым объектом происходит по именам методов. Иначе, какая может быть немногословность? Следовательно, как-то надо унифицировать интерфейсы. И как унифицированный интерфейс после этого выглядит? Интересно взглянуть на смесь бульдога с носорогом.
2. Допустим, немногословность подразумевает явный маппинг одинаковых сигнатур. Заодно решается проблема со скрытием ненужной части аггрегированного класса в делегирующем классе. Тогда замапив push() на push_back(), pop() на pop_back(), а top() на back() вы получите искомое. Проблема в том, что:
а. В классе списка вам надо таскать back() или first(), или top() или еще что-то, нужное только для этой цели. Потому, что иначе гораздо проще вызвать list[0] или list[list.size — 1]. Ненужный метод усложняет чтение. Ни в FCL, ни в Java такого изврата нет. Поэтому я и назвал STL'ный код нечитаемым.
б. Возможна только та реализация стека, в которой важно быстрое добавление/удаление элементов, а не быстрая выгрузка списка, поскольку он перевернутый.
Короче, надо сделать два разных несвязанных интерфейса у этих классов, иначе проблем много вылезает.
Можно сказать, что top() надо реализовать как { return list[0]; }, но тогда что тут от делегирования остается? Так, извините, любое поле строкового класса добавил — уже делегировал обработку текста в string. В любом случае, даже если мы считаем это делегированием, о чем тогда ваш вопрос? Многословный синтаксис у вас уже есть. А про него я написал:
Здравствуйте, 0BD11A0D, Вы писали:
BDA>Здравствуйте, Буравчик, Вы писали:
BDA>Можно сказать, что top() надо реализовать как { return list[0]; }, но тогда что тут от делегирования остается? Так, извините, любое поле строкового класса добавил — уже делегировал обработку текста в string. В любом случае, даже если мы считаем это делегированием, о чем тогда ваш вопрос? Многословный синтаксис у вас уже есть. А про него я написал:
Согласен, не очень удачный пример про список и стек. И, действительно, вопрос был навеян именно нежеланием писать методы типа
method1(x,y,z) { obj.method1(x,y,z) }
Т.е. чисто делегирующие методы, в которых сигнатуры совпадают.
Но в целом, интересует вопрос шире, насколько в современных языках делегирование явно выделяется (как некая конструкция языка). Что-то типа
method1, method2, method3 delegated_to obj
или
method1 delegated_to obj.goodmethod1
Здравствуйте, 0BD11A0D, Вы писали:
BDA>Я очень сильно извиняюсь, что заставил столько текста написать, но я имел в виду совсем другое. Где промышленный код, в котором все эти проблемы заложены? С размытиями контрактов и т.д.
Вы там в Москве совсем офигели, ламборгини за 300 миллионов рублей! А у нас в Рязани гречка по 70 рублей за кило.
(Кстати, в пересчёте на массу ламборгини — порядка 100 тысяч, сравните с 300 миллионами).
BDA>Дело в том, что программирование вообще — очень простое занятие, если специально не усложнять. Чем программисты любят заниматься, либо чтоб не уволили, либо из естественного любопытства, не добавляя ценности. Если думать над ценностью, грамотно проводить декомпозицию, код со всей его структурой будет очень прост. Я просто не могу представить себе, какова должна быть предметная область, чтобы делегирование в коде ее адекватно передавало и было способом упростить чтение кода. Я и прошу: может кто-нибудь привести пример?
Какова предметная область, чтобы делегирование адекватно передавало? Да ты прямо сейчас её смотришь!
Оконная система. Не мега-наследник от HWND реализовал всю эту красоту, "как нарисовать и как реагировать на мышь в каждой конкретной точке экрана" (если помыслить Шейнфинкелем-Карри, то у экрана есть 1920*1080 маленьких интерфейсиков), а десктоп делегировал это окнам приложений, окна приложений — дочерним окнам, и т.д.
BDA>Что касается ручной возни с vtbl'ом, я вообще не понимаю, что это и зачем. Знать про vtbl полезно, если ты компиляторы пишешь. Или если нечаянно испортил память, найти место, где ломаешь вызовы и крэшишь все. Оптимизация, когда расходы на виртуальные вызовы важны? Бывает крайне редко, но тогда пиши на голом C с хендлами. Здесь о другом, здесь об организации кода.
Спросил про цену вопроса, я ответил. Делегирование объективно жрёт больше памяти и времени. Кому это критично, должны это учитывать, а кому некритично, зачем спрашиваете.
Здравствуйте, Кодт, Вы писали:
К>Какова предметная область, чтобы делегирование адекватно передавало? Да ты прямо сейчас её смотришь! К>Оконная система. Не мега-наследник от HWND реализовал всю эту красоту, "как нарисовать и как реагировать на мышь в каждой конкретной точке экрана" (если помыслить Шейнфинкелем-Карри, то у экрана есть 1920*1080 маленьких интерфейсиков), а десктоп делегировал это окнам приложений, окна приложений — дочерним окнам, и т.д.
Вы передергиваете, надеюсь, что несознательно.
Во-первых, делегирование понимается и как консультирование, обычно в форме аггрегирования (оригинальная трактовка), и (после статьи Либермана) как языковой механизм диспетчеризации. Топикстартер своим вопросом однозначно сузил это понятие до второго значения. На самом деле, даже больше, поскольку его диспетчеризация — исключительно compile-time. Мой вопрос, зачем ему это нужно, был в том же контексте. Он вскользь написал, что сам не знает, дает ли это какие-то возможности. Похоже, действительно нахрен это не надо, раз никто не смог ни одного примера привести.
Во-вторых, в любом из значений делегирование относится к ООП, в то время как API «оконной системы с HWND», да и сама ее реализация — процедурно-хендловые.
Здравствуйте, 0BD11A0D, Вы писали:
Б>>Согласен, не очень удачный пример про список и стек.
BDA>А есть ли более удачный? Представьте любой, самый волшебный синтаксис. На какой задаче вы его будете применять?
В тех же задачах, где применяется наследование. Только получившийся объект не будет автоматически совместим по присваиванию с делегатом (только в том случае, если оба объекта реализуют общие интерфейсы). А наследование — на помойку.
Здравствуйте, 0BD11A0D, Вы писали:
BDA>Вы передергиваете, надеюсь, что несознательно.
Это был ответный удар. Потому что сводить программирование к написанию скриптиков и клиентов БД — толсто.
BDA>Во-первых, делегирование понимается и как консультирование, обычно в форме аггрегирования (оригинальная трактовка), и (после статьи Либермана) как языковой механизм диспетчеризации. Топикстартер своим вопросом однозначно сузил это понятие до второго значения. На самом деле, даже больше, поскольку его диспетчеризация — исключительно compile-time. Мой вопрос, зачем ему это нужно, был в том же контексте. Он вскользь написал, что сам не знает, дает ли это какие-то возможности. Похоже, действительно нахрен это не надо, раз никто не смог ни одного примера привести.
Это не "нахрен не надо", а потому что про классовое ООП написана тьма монографий (начиная с Гради Буча и Гаммы-et-al), и народ следует этим идеям по нужде и без нужды. Поэтому и мыслить в этой парадигме легко, и примеров готовых полно.
А компонентное ООП не является таким мейнстримом. И даже ФП не является. Хотя они все взаимозаменяемы и выразимы друг через друга.
(Зато говнокодное ад-хок-ООП по образцу — является мейнстримом, потому что есть в каждом браузере).
BDA>Во-вторых, в любом из значений делегирование относится к ООП, в то время как API «оконной системы с HWND», да и сама ее реализация — процедурно-хендловые.
В мире полным-полно оконных систем, где ООП самое что ни есть классово правильное. То, что в винде оно замаскировано в угоду чисто-сишному API, чтобы кто угодно на чём угодно мог обращаться, без расизма "только ObjC, только какава с чаем" или "только C++/Qt", или "только смолток, только весёлые 60-е" — это даже не детали реализации, а мелочи. Так-то там и объекты, и классы всё равно есть. Плюс ещё надстройки в виде COM и WPF.
Кстати про COM. Я уже говорил, что эта компонентная модель недружественна к наследованию, зато дружественна к делегированию. (Собственно, любая компонентная модель имеет смещение баланса в эту сторону).
Хочешь примеров, возьми любую программу на васике.
Здравствуйте, AlexRK, Вы писали:
ARK>В тех же задачах, где применяется наследование. Только получившийся объект не будет автоматически совместим по присваиванию с делегатом
То есть private наследование?
#include <vector>
using namespace std;
struct custom : private vector<int>
{
using vector<int>::push_back;
};
void func(const vector<int>&);
int main()
{
custom x;
x.push_back(1);
func(x);
}
Здравствуйте, Evgeny.Panasyuk, Вы писали:
ARK>>В тех же задачах, где применяется наследование. Только получившийся объект не будет автоматически совместим по присваиванию с делегатом
EP>То есть private наследование?
Здравствуйте, Буравчик, Вы писали:
Б>Но в целом, интересует вопрос шире, насколько в современных языках делегирование явно выделяется (как некая конструкция языка). Б>И дает ли это языку какие-то особые возможности.
Идиома пимпл — даёт уменьшение зависимостей. К сожалению, она не оформлена как часть языка С++, чтобы пользователя избавить от писанины, переложив домысливание на плечи компилятора.
Сама по себе фича "пробрасывать интерфейсы не только между базой и наследником" не так уж и плоха.
Как и любая другая фича, — если она дешёвая, её будут популяризовывать, если нет — будут избегать.
На языке Си вполне можно писать в духе классового ООП, но — руками или препроцессором CFront. Да ну в топку, эти классы. А тем более, это наследование.
Здравствуйте, Evgeny.Panasyuk, Вы писали:
EP>>>То есть private наследование? ARK>>Да, но с точностью до метода (при необходимости). EP>В примере выше именно с точностью до метода:
А, пардон, затупил. Тогда да, это оно. Если, конечно, еще есть варианты, как разрулить конфликты при делегировании групп методов от нескольких объектов.
Собственно, это и не наследование никакое, название только сбивает с панталыку.