Посоветуйте паттерн/архитектуру

kill-still

Есть задача: парсить файлы, которые пишет другая программа. В зависимости от версии этой программы, она пишет данные в этот файл по-разному (причем формат от версии к версии меняется не целиком, а только отдельные блоки). В результате парсинга вне зависимости от версии должен получиться объект фиксированного формата. Сейчас всё реализовано деревом с условиями (if version > 100500 ... else ...). Как сделать чтобы было просто, красиво и удобно и т.д.?

schipuchka1

Фабрикой абстрактных интерфейсов! ;-)
Для меня твоя задача звучит как "я имею один декодер и кучу маппингов для декодирования". Маппинги сложить в коллекцию по версиям и обрабатывать общим декодером. Если декодер слишком сложный, то поделить на элементарные части и собирать по маппингу при запуске, в коллекцию класть декодеры сконструированные.

durka82

Можно ссылки на обработчики в словарь запихать, а в качестве ключей как раз номер блока и версию.

yroslavasako

Пробуй банальное наследование. Для каждой версии сделай отдельный класс с каким-нибудь методом parse и фиксированным outputом. Если влом иметь столько разных классов, когда версий много - вместо этого делай объекты-теги и делай между ними switch (или мультидиспатч по тегу и по контенту)

kill-still

Можно ссылки на обработчики в словарь запихать, а в качестве ключей как раз номер блока и версию.
Допустим, блок А записывался методом Р1, а в версии 1.2.3 изменился на Р2. Любая версия >= 1.2.3 должна парсится по правилу Р2, а до этой версии - по Р1. Множество версий - неизвестно. Как ключ считать будешь?

kill-still

Не, switch это плохая практика.
http://sourcemaking.com/refactoring/replace-conditional-wit...

kill-still

А есть какая-нибудь тулза в идее, которая собирала бы исходник класса из его предков?

danilov

А нахер это нужно?
И куда деть статики?
По сабжу если версия известна, то Map. Заплнять можно через какой-нибудь ClassIndexer, их много разных. Если неизвестна, то список. Заплняется в одном месте в строгом историческом порядке

kill-still

А нахер это нужно?
И куда деть статики?
это был вопрос к Айвенго, по его предложению.
По сабжу если версия известна, то Map. Заплнять можно через какой-нибудь ClassIndexer, их много разных. Если неизвестна, то список. Заплняется в одном месте в строгом историческом порядке
Можешь поподробнее написать, а то я не улавливаю ход твоей мысли.

yroslavasako

switch это нормальная практика. Ты же не указал язык. Я заодно тебе и double dispatch предложил, если с полиморфизмом хорошо заходит

yroslavasako

Делаешь функцию, которая по версии генерит тег нужного типа. Тег параметризуешь типом входного и выходного блока. Можешь во внешний объект вынести обработку тега, а можешь прям внутри самого тега делать парсинг.

danilov

Map нужен чтобы понять, с какой версии начинать парсить. А сами парсеры я бы предложил разбить на 2 группы: собственно, парсеры (их в идеале должно быть немного, это только те, которые не могуть быть во второй группе) и делегаты, умеющие сконвертить свой вход во вход какому-нибудь персеру или делегату (чаще всего предыдущей версии). У первой группы может быть пошареный код и, соответственно, иерархия, а второй нахер не нужно. Здесь будет небольшая просада по производительности, но логические структуры должны почти полностью уйти. Ещё есть смысл поглядеть, как с этим справляются существующие системы разметки данных, поддерживающие версионирования.

kill-still

А нахер это нужно?
Ну кстати иногда может быть полезно для ковыряния в говнокоде (Yo-yo антипаттерн). :(

durka82

Допустим, блок А записывался методом Р1, а в версии 1.2.3 изменился на Р2. Любая версия >= 1.2.3 должна парсится по правилу Р2, а до этой версии - по Р1. Множество версий - неизвестно. Как ключ считать будешь?
У тебя по сути есть кроме версии проги ещё и версии блоков.
Что мешает на каждую версию проги хранить список версий блоков?
Даже если хранить только изменения, написать функцию, которая по номеру версии проги будет выдавать список версий актуальных блоков, проблемы никакой не вижу.
Тут кстати и БД может прийтись в пору.

Papazyan

(if version > 100500 ... else ...). Как сделать чтобы было просто, красиво и удобно и т.д.?
Прекрасные простые if и процедурное программирование великолепно решают эту задачу. Для чего тут городить ООП говнокод?

Maurog

Посоветуйте паттерн
паттерн называется Стратегия :grin:

kill-still

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

Maurog

В зависимости от версии этой программы, она пишет данные в этот файл по-разному (причем формат от версии к версии меняется не целиком, а только отдельные блоки)
что за формат? ini/xml/json ? или бинарная проприетарная хрень?

sania1974

Можно реализовать автоматную модель. В зависимости от текущего состояния ищешь элементарный декодер определенной версии, который разрешено применять в данном состоянии. Считываешь порцию данных и переходишь в новое состояние. Дополнительно, если в порции данных содержится версия последующего блока данных, включаешь или отключаешь декодеры, которые сможешь задействовать потом.
То есть у тебя есть бинарная таблица декодер/состояние и на каждом шаге ты по ней перемещаешься.

6yrop

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

kill-still

ага, gas factory.

kill-still

json вперемешку с бинарными данными.

Maurog

json вперемешку с бинарными данными.
подобные форматы удобны тем, что позволяют делать манипуляцию без понимания семантики. всякие xslt/xml этим спекулируют.
для адаптации одного json к другому json можно использовать аналогичный подход:
1) определяем пути (аналог XPATH, можно просто склеивать названия имен узлов от корня до текущего), которые требуют трансформации, мапим путь или паттерн пути (это могут быть regexp) на трансформатор. трансформатор получает узел и выплевывает другой узел
2) енумерим узлы\атрибуты, поддерживаем текущий путь к узлу\атрибуту.
3) по пути можно поискать трансформатор, если такой есть, то запустить его и схавать выхлоп
мета-код (не спрашивайте что это за язык :grin: )

class JSonNode;
interface ITransformer
{
JSonNode Transform(JSonNode)
}

clas MySrcTransformer : ITransformer
{
JSonNode Transform(JSonNode node)
{
return node.Clone().ReplaceAttribute("src", "MySrc", "C:\windows.jpg");
}
}

TraverseNodes(in / out JSonNode node, string xpath, func)
{
xpath += "." + node.Name;
node.Children().ForEach(c = > TraverseNodes(c, xpath, func));
func(node, xpath);
});

dict<string, ITransformer> transformers;
transformers.Add("root.Image.*", MySrcTransformer()); /// << тут нужно положить всех трансформеров, которые соответствуют версии документа
JSonNode root = LoadDocument();
TraverseNodes(root, "root",
(node, path) = >
{
ITransformer t;
if (transformers.TryGetMatching(path, t))
{
node.CopyFrom(t.Transform(node));
}
});

SaveDoc(root);

schipuchka1

Прекрасные простые if и процедурное программирование великолепно решают эту задачу. Для чего тут городить ООП говнокод?
:facepalm:
каждое повторение кода увеличивает вероятность ошибки и усложняет отладку и исправление.

kill-still

а вот допустим с такими данными твой алгоритм не справится:

//версия 1.0
...
"block1" : [
{
"id" : "1",
"name" : "aaaa"
},
{
"id" : "2",
"name" : "bbbb"
}
],
...
"block2" : [
{
"id" : "1",
"amount" : 5
},
{
"id" : "2",
"amount" : 7
}
],
...


//версия 2.5
...
"block1" : [
{
"id" : "1",
"name" : "aaaa",
"amount" : 5
},
{
"id" : "2",
"name" : "bbbb",
"amount" : 7
}
],
...

  :grin: :grin:

6yrop

:facepalm:каждое повторение кода увеличивает вероятность ошибки и усложняет отладку и исправление.
давайте посчитаем, в каком варианте повторений кода больше

schipuchka1

давай посчитаем. Моя абстрактная фабрика абстрактных интерфейсов выглядит примерно так:
 

public class JustAnotherAbstractInterfaceFactory {

  private final ImmutableSortedMap<String, FileMapping> mappings;
  
  public JustAnotherAbstractInterfaceFactory() {
    // reading mapping and set it to mappings
  }
  
  public List<JustAnotherBean> parseFile(String version, String content) {
    FileMapping mapping = mappings.floorEntry(version);
    if (mapping == null) {
     throw new IllegalStateException("No mapping for version " + version);
    }
    return new Parser(mapping).parse(content);
  }
}

public class Parser {
private final FileMapping mapping;
private final Map<Long, JustAnotherBean> beans;

public Parser(FileMapping mapping) {
this.mapping = mapping;
this.beans = new HashMap<Long, JustAnotherBean>();
}

public Collection<JustAnotherBean> parse(String content) {
for (String pathToBlock : mapping.getAllBlocks()) {
BlockMapping blockMapping = mapping.getBlockMapping(pathToBlock);
List<String> blocks = extractAllBlocks(content, pathToBlock);
for (String block : blocks) {
createOrUpdateBean(block, blockMapping);
}
}
return beans.values();
}
///
}

т.е. немного простого кода, который легко протестировать. Думаю при достаточно большом количестве полей в энтри он будет компактней ифов версии к третьей + легче тестировать. Из минусов - большая вложенность и возможная потеря при рефлекшне (можно избежать, если у нас будет не бин, а, скажем, Map).

luna89

Сейчас всё реализовано деревом с условиями (if version > 100500 ... else ...). Как сделать чтобы было просто, красиво и удобно и т.д.?
Оставь как есть, самое простое решение - самое хорошее. Если бы у тебя была задача сделать какой-то абстрактный парсер в вакууме, оформить его в виде библиотеки и отдать другим разработчикам, чтобы они могли его расширять без правки кода самой библиотеки, то тогда мудреж имел бы смысл. Если у тебя просто есть код который ты сам пишешь и контролируешь, то введение полиморфизма просто усложняет код на ровном месте.

schipuchka1

Ага, оставь как есть, пусть у тебя будет слаботестируемый разрастающийся кусок... кода.

6yrop

Я вижу, что в твоем коде 16 строк тупо повторены два раза. Причем твой код ничего полезного не делает.
Повторение кода в варианте if пока нету.

6yrop

слаботестируемый
с чего ты это взял?
 
разрастающийся

а фактори не разрастаются? Потому что уже никто не может в них внести изменение? Потому что в них реализовано достаточно уровней сложности?

schipuchka1

Повторение кода в варианте if пока нету.
У тебя в каждой ветке if .. elseif повторяются примерно одни и те же действия по перекладыванию из JSON в объект X. Сколько веток - столько повторений.

Papazyan

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

schipuchka1

с чего ты это взял?
Для того чтобы проверить if .. elseif простыню тебе нужно скармливать объекты целиком и проверять, что получается то, что ты хочешь. И так для каждой версии. Если есть необязательные поля, то сложность набора достаточных тестов получается экспоненциальная. И в любом случае тебе доступны только end-to-end тесты, проверить конкретный if нельзя.
В случае разделения маппинга и реализации ты можешь отдельно протестить сам компонент, отдельно проверить маппинг. Сложность тестирования снижается в разы - разделяй и властвуй, да.
а фактори не разрастаются? Потому что уже никто не может в них внести изменение? Потому что в них реализовано достаточно уровней сложности?
Потому что в них обычно не нужно вносить изменения. При появлении новой версии ты просто можешь написать маппинг (формальный) и собрать таким образом новый парсер из уже протестированных компонентов. Менять фактори тебе нужно будет только если добавится новый тип филда и то тестировать только его нужно будет.

schipuchka1

Я вижу, что в твоем коде 16 строк тупо повторены два раза.
да, да, спасибо что заметил. Исправил

schipuchka1

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

Papazyan

У тебя в каждой ветке if .. elseif повторяются примерно одни и те же действия по перекладыванию из JSON в объект X. Сколько веток - столько повторений.
Начать с того, что 90% различий в формате скорее всего касаются добавления-удаления-переименования какого-нибудь поля. Эта задача решается смехотворно просто мердженьем дефолтного объекта с тем что распарсено (такие алгоритмы у вас спрашивают на собеседовании) и кучкой if-ов для файнтюнинга пары изменившихся полей.

yroslavasako

а при чём тут одно к другому? На java можно прогать процедурно. И бывает не java OOP. Тот же CLOS, например.

schipuchka1

А теперь расскажи как это протестировать

luna89

У меня была одно время коварная идея для стартапа. Надо разработать два продукта, они будут вирусно
распространяться и стимулировать продажи друг друга.
Первый продукт можно называть enterprisify, он будет делать эквивалентные source to source преобразование java кода, добавляя в него уровней косвенности и "паттернов", заодно генерируя тавтологические юнит тесты. Инструмент должен сохранять форматирование и применять некоторые эвристики, чтобы было похоже на "рефакторинг" сделанный человеком. Возможен human-assisted режим.
Второй инструмент будет осуществлять обратные преобразования, его можно назвать dumbify. Тут важно, что dumbify будет на голову выше конкурентов, потому что он будет использовать секретные эвристики из enterprisify.
Можно продавать пак два в одном. Предполагается, что разработчик может пользоваться обоими инструментами (в разных ситуациях).

Maurog

а вот допустим с такими данными твой алгоритм не справится
но это не моя проблема, это - твоя проблема :grin:
идея в целом: подготавливаешь движок, в который уже можно пихать относительно маленькие стратегии, весь движок когда надо их вызовет и трансформирует документ
в случае json можно делать потоковые трансформации, как это делают функциональные flatten/map/filter/zip/sort/etc. это очень мощный фундамент, так они позволяют делать pipeline, то бишь офигенно все composable
можешь взять какой-нибудь mongoDB или что-то вокруг json (первая ссылка из гугла http://danski.github.io/spahql/#core_concepts ) для реюза или просто понять идею
тут нет серебряной пули: проще заложиться на конкретные виды преобразований. это как с SQL - вроде мощный, но далеко не все сценарии можно на нем реализовать
в твоем конкретном примере не ясно, в каком формате ты хочешь получить выхлоп.
если надо из v1.0 перегнать в v2.5, то надо создать трансформер, который сделать примерно следующее

JSonNode MyTransformer::Transorm(JSonNode node)
{
return new JSonNode().SetChildren(ZipWith(node["block1"].Children, node["block2"].Children, (a,b) => new JSonNode("name" = a["name"], "amount" = b["amount"]));
}

если надо из v2.5 перегнать в v1.0, то делаем так

JSonNode MyTransformer::Transorm(JSonNode node)
{
var result = new JSonNode()
result.AddChild("block1", FilterAttribute(node["block1"], "name"));
result.AddChild("block2", FilterAttribute(node["block1"], "amount"));
return result;
}

6yrop

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

Теории сложности тестирования на сегодняшний день не существует (могу привести авторитетные ссылки). О каком снижении ты говоришь? В каких единицах? В попугаях? Как сравнивать два тестирования, какое больше какое меньше?

Papazyan

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

schipuchka1

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

schipuchka1

Я думал, почему продукты гугл все хуже и хуже. Теперь становится понятно.
У тебя есть шанс! Возьми кредит в банке, создай гугль-киллер!
Оставить комментарий
Имя или ник:
Комментарий: