Dmitry_Milk
> Скажем, упомянутая выше сериализация.
Откуда вызывающая сторона знает, как правильно сериализовать объект, чтобы не нарушить его внутреннюю логику? Откуда вызывающая сторона вообще знает, какие есть поля у объекта?
Не знает. Но решение "вынести все в виртуальные функции" тоже не является идеальным. Например, мне приходится иметь дело с группой однотипных проектов, где сериализация объектов в базу данных и в сеть отличается (скажем, какие-то поля передаются в другом формате или вообще не передаются), а еще при сериализации в сеть учитываются версии протоколов, от которых тоже зависят формат или наличие/отсутствие полей.
Реализовано (С++) как ты и написал, через виртуальный метод сериализации, заполняющий байтовый буфер. Но в результате получается в методах сериализации нагромождения IF-ов от "назначения сериализации (сеть/БД)" а также от версии протокола. За каким хреном типы этих объектов должны знать про какие-то "версии сетевых протоколов", когда их назначение вовсе не сетевое?
Dmitry_Milk
> Реализовано (С++) как ты и написал, через виртуальный метод сериализации,
> заполняющий байтовый буфер. Но в результате получается в методах сериализации
> нагромождения IF-ов от "назначения сериализации (сеть/БД)" а также от версии
> протокола.
а не проще для этого друзей использовать? т.к. сериализация это как раз задача для друзей?
тогда виртуальные методы нафик не нужны
skalogryz
> а не проще для этого друзей использовать?
Наверное можно было бы комбинацию из виртуальной функции и друзей. Чтоб виртуальный метод Serialize внутри себя вызывал соответствующую дружественную перегруженную функцию из файла, ответстветственного за сериализации. Но уже что сделано, то сделано, да и не я разработчик.
Но в любом случае, уже в красивую схему разграничения полномочий не укладывается - все равно сторонний (хоть и дружественный) код должен быть в курсе внутреннего устройства и иметь соответствующий доступ.
Dmitry_Milk
> Нет, конечно можно сказать - у всех сериализуемых типов должен быть метод
> Serialize, выделяющий/заполняющий байтовый буфер, который потом можно
> использовать куда угодно - в файл, на консоль, в сеть... Но если тебе в файл
> надо "как быстрее", в сеть - "как компактнее", а на консоль - "как понятнее
> человеку", то такой подход начинает хромать.
Я вовсе не утверждаю, что такое решение здесь подойдёт, но, возможно, оно подарит полезные мысли.
Кратко - вместо того, чтобы сериализовать напрямую объект→поток; мы формализуем некоторую очень простую промежуточную форму (у меня - подобие json, только ещё проще); и затем реализуем два раздельных, независимых этапа преобразования - объект→форма и форма→поток.
Dmitry_Milk
> Тогда алгебраический тип и паттерн-матчинг.
Не совсем понимаю о чем ты и как это стыкуется с темой.
Пример можно?
pahaa
> ну так я и прошу конкретный пример
class Something class Boolean : Something class Number : Something class String : Something something = new String json.Serialize(something) xml.Serialize(something) yaml.Serialize(something)
Great V.
> Здесь данные (Something и остальная иерархия) и алгоритмы их обработки (json,
> xml, yaml) разделены.
Как раз наоборот, здесь большая связанность. Алгоритмы не удастся применить в программе, для которой не определён любой из подклассов Something.
Если сделать так:
json.Serialize(something.getDataProvider())
pahaa
> json.Serialize(something.getDataProvider())
чё-т... как-то... хм...
вариант №1.
Класс сам себя сереализует в конкретный носитель данных.
something.Serialize(json);
something.ToJson();
вариант №2
Класс сам себя сериализует, в абстрактный носитель данных (i.e. ISerializer)
something.Serialize(json.getSerializer( ));
вариант №3
Класс предлагает, абстрактный источник данных, с которым умеет работать носитель данных
json.Serialize(something.getDataProvider( ))
вариант №4
Дополнительный класс, который умеет в конкертый источник данных
somethingJsoner.Serialize(json, something);
вариант №5
Дополнительный класс, который умеет в абсрактный источник данных
somethingDataProvider.Serialize(json.getSerializer( ), something);
Dmitry_Milk
> Но в любом случае, уже в красивую схему разграничения полномочий не
> укладывается - все равно сторонний (хоть и дружественный) код должен быть в
> курсе внутреннего устройства и иметь соответствующий доступ.
всё то, что может/должно сериализоваться должно быть в public
всё то, что не должно сериализоваться может быть в protected/private
это решает проблему о "внутреннем устройстве". Т.к. всегда можно проверить, все ли необходимые данные сериализуются.
Что ещё интереснее, написание класса для сериализации можно автоматизировать (т.е. тупо проходить по public секции класса)
pahaa
Ты не шаришь, getDataProvider вообще не нужен. Но даже если в нем и будет какая-нибудь необходимость, json.Serialize под капотом и сам сможет вызвать его, something.getDataProvider() лишний. Кроме того, json.Serialize сам может выбирать те классы из иерархии Something, которые нужно сериализовать.
В связности, от которой ты так убегаешь, нет ничего плохого. Если обойтись без нее то получится еще одно промежуточное звено в виде getDataProvider. И вместо того чтобы JSON знал о тех Something которые ему интересны, Something должны будут уметь превращаться в этот самый getDataProvider. А в общем случае getDataProvider просто будет дублировать всю иерархию Something и мы получим бесполезное дублирование кода.
Но это все оффтоп. Суть примеры была в том, что виртуальные методы это всего лишь инструмент. И в некоторых случаях есть более удобные инструменты.
Но как ты заметил и это в определенной мере оффтоп. Поведение, которого я добивался заключалось в том, чтобы строго ограничить иерархию. Т.е. чтобы мы знали что у класса A есть определенное количество производных классов. И чтобы существовали гарантии что другие классы от него унаследовать уже не получиться. И подход A supers X, Y, Z предоставляет такую гарантию (по крайней мере если после этого мы уже не можем написать W extends A).
Great V.
> Не совсем понимаю о чем ты и как это стыкуется с темой.
Ну алгебраические типы с паттерн-матчингом тоже ведь можно рассматривать как вариант "суперкласса", определяемого задним числом, и его динамической диспетчеризации. Только за кулисами в объекте лежит не ссылка на таблицу виртуальных функций, а непосредственно идентификатор типа.
Модуль с типом Rectangle
struct Rectangle { left: f64, top: f64, right: f64, bottom: f64, }
Модуль с типом Circle
struct Circle { x: f64, y: f64, radius: f64, }
Модуль с типом Triangle
struct Triangle { x1: f64, y1: f64, x2: f64, y2: f64, x3: f64, y3: f64, }
Модуль с "суперклассом" Shape
enum Shape { Circle(Circle), Rectangle(Rectangle), Triangle(Triangle), }
Модуль, ответственный за сериализацию
fn serialize_shape(f: &Shape) -> String { match f { Shape::Circle(c) => { return format!("S:C({},{},{})", c.x, c.y, c.radius) }, Shape::Triangle(t) => { return format!("S:T({},{},{},{},{},{})", t.x1, t.y1, t.x2, t.y2, t.x3, t.y3) } Shape::Rectangle(r) => { return format!("S:R({},{},{},{})", r.left, r.top, r.right, r.bottom) } } }
То есть, это вариант, чем-то аналогичный if(obj instance of ...), но безопасный, т.к. компилятор заставит тебя либо перечислить все возможные варианты, либо сделать дефолтную ветку, и при этом не даст тебе случайно перепутать конкретные "дочерние" типы. И при этом с локализацией кода, описывающего поведение, как ты и хотел - прямо в одной функции (в данном случае функцию можно было написать еще проще, но я для демонстрации мысли выделил фигурными скобками альтернативные блоки).
skalogryz
> чё-т... как-то... хм...
skalogryz
> вариант №3
> Класс предлагает, абстрактный источник данных, с которым умеет работать
> носитель данных
К чему этот комментарий вообще был?
Great V.
> Кроме того, json.Serialize сам может выбирать те классы из иерархии Something,
> которые нужно сериализовать.
Т.е. получится алгоритм, который и не алгоритм вовсе. Если захочется пошарить такой "алгоритм" между проектами, то придётся его перелопачивать.
Great V.
> будет дублировать всю иерархию Something
С иерархией этот класс будет делать ровно то же самое, что в твоём варианте будет делать метод serialize()
Более того, такой обход придётся писать у каждого "алгоритма"
P.S. Не так давно допиливал один такой сериализатор. Вся игра сериализовалась через 1 метод. И нужно было создать toJson() вместо toBinary(). А ещё там был метод toYaml(), который отличался от toBinary(), хотя вроде как не должен. Так вот, этой радостью пришлось страдать целую неделю. И потом ещё два дня писать валидатор, чтобы убедиться, что расхождений нет. А было бы гораздо быстрее и приятнее, если б можно было просто написать методы записи и скормить им уже готовый DataProvider.
Great V.
> И чтобы существовали гарантии что другие классы от него унаследовать уже не
> получиться.
Но ведь для этого и так есть механизмы. В плюсах, например, можно сделать приватный конструктор и указать допустимые подклассы в качестве френдов.
pahaa
> С иерархией этот класс будет делать ровно то же самое, что в твоём варианте
> будет делать метод serialize()
Хотя в итоге работа будет выполнена та же, но в моем варианте алгоритм будет отделен от данных.
И это хорошо, ведь Something не обязательно должен отвечать за сериализацию в JSON.
Слышал когда-нибудь о шаблоне проектирования "посетитель"?
pahaa
> Но ведь для этого и так есть механизмы.
Из того что существуют механизмы которые позволяют получить "похожий" результат еще не следует что эти механизмы не костыли для конкретной задачи.
Например, кроме конструкторов френды передадут производным классам дополнительные полномочия.
Но это не так уж и важно, потому что для более общего случая стандартных механизмов для обеспечения подобного поведения уже не найдется.
Great V.
> но в моем варианте алгоритм будет отделен от данных
дык, наоборот же
прям внутри алгоритма придётся хардкодить структуру данных
а в случае с дата-провайдером можно при особом желании разнести данные, сериализатор и дата-провайдер не только по разным модулям, но даже по трём разным библиотекам, и зависимость будет всего одна: дата-провайдер зависит от объекта
тут единственный минус будет в объёме кода при малом количестве классов, зато это легко масштабируется и при разрастании иерархии такой подход обгонит хардкоженные сериализаторы даже по этому параметру
В общем, не убедил. Я, честно, надеялся на что-нибудь новенькое и интересное))
Great V.
> не следует что эти механизмы не костыли для конкретной задачи
Совершенно согласен. Но суперкласс не особо отличается в этом плане. Просто задача довольно странная. Концептуально потребность в этом не ясна. Ограничение допустимых вариантов объекта противоречит принципам наследования (впрочем, как и protected и private наследование). Она выглядит странно что в процедурном, что в объекто-ориентированном, что в функциональном, что в декларативном подходе. Другие парадигмы мне неизвестны. Я, конечно, понимаю, что такое может увеличить надёжность некоторых костылей "здесь и сейчас", но применять такое систематически - выглядит как головная боль и признак плохой архитектуры.
pahaa
> К чему этот комментарий вообще был?
просто, ни разу такого подхода, как ты предлагаешь, не встречал.
skalogryz
> просто, ни разу такого подхода, как ты предлагаешь, не встречал.
И поэтому под номером 3 написал дословно то же самое?
Тема в архиве.