![[personal profile]](https://www.dreamwidth.org/img/silk/identity/user.png)
Я тут подумал.
Вот как обычно делается обработка IO во всяком там ООП?
Вот у нас есть какой-то скажем TextReader, из него можно читать буквы.
Окей. Но вот нам надо сделать читалку XML-нодов из текста, XmlReader.
Так вот - почему обычно делается так, что TextReader биндится к объекту XmlReader в конструкторе последнего и остается там до окончания его, XmlReader'a, жизни? Т.е. почему вышележащие потоки обычно хранят используемые объекты внутри себя? Да, это может быть, неплохо ложится на C++ное RAII, но если подумать:
Возникают такие проблемы:
На самом деле, я еще про другие минусы этой модели недавно думал, но сейчас уже не вспомню - написал самые очевидные
А вот что вы думаете о противоположной модели, использующейся, в частности, в контексте "считывателя" Common Lisp - считыватель(штуковина, формирующая лисповые объекты из текста) и текстовые потоки друг с другом не связаны, и "высокоуровневый" поток, при вызове "read" принимает в аргументы низкоуровневый, как бы подключаясь к нему, когда нужно совершить операцию чтения. Это позволяет отделить состояние собственно считывателя(оно хранится в глобальных переменных - *read-base* и пр.) от состояния да и вообще, разновидности, используемого текстового потока, от которого нужен только интерфейс(он должен уметь считывать буквы).
Аналогия из реальной жизни - водоснабжение. Вот у нас есть труба, из которой по необходимости можно брать воду. Когда нам нужно, скажем, постирать что-либо, мы берем и подключаем к трубе стиральную машинку. Постирали - отключаем. Когда машинка, допустим, сломалась, и ее нужно поменять, нам не приходится ломать всю систему водоснабжения в доме, и строить ее заново - машинка не приварена к трубе, мы просто берем ее, выкидываем, или относим на ремонт, и приносим новую. Если в трубе вдруг пропала вода, мы подключаем машинку к другой трубе. Ну и так далее.
Конечно, в контексте программирования - если делать так, как это все делается в контексте лиспового reader'а, то возникает проблема разрастания количества аргументов функций по мере повышения количества слоев обработчиков. Но, тут можно пойти по тому пути, который используется, например, в обработчиках аудио и видео контента, например в виндовом DirectShow - там есть объекты фильтров, которые как-то перегоняют информацию из одной формы в другую. Фильтры создаются заранее, и потом, когда надо, "подключаются" друг к другу(у каждого фильтра есть т.н. разъемы, которые можно друг с другом соединять).
Вобщем, вот так вот. Что думаете?
upd. Вот пример того, о чем я говорю, в терминах интерфейсов на C#: http://pastebin.com/jvcb2F8g
(все-таки не совсем точно описал, что уже заметно по ответам в juick. Да, модель .NET предполагает что весь стек "читалок" стоит на фундаменте Stream, который основан на неуправляемых ресурсах. А я говорю про то, что архитектура должна быть слабосвязной, и "параллельной" (потоки должны связываться входами и выходами, динамически, как в графе фильтров DShow, а не скрывать друг друга или какой-то один "низкоуровневый" поток))
upd2. Короче, вот картинка идеи:

Кстати, на такую модель идеально ложится асинхронность, например.
Вот как обычно делается обработка IO во всяком там ООП?
Вот у нас есть какой-то скажем TextReader, из него можно читать буквы.
Окей. Но вот нам надо сделать читалку XML-нодов из текста, XmlReader.
Так вот - почему обычно делается так, что TextReader биндится к объекту XmlReader в конструкторе последнего и остается там до окончания его, XmlReader'a, жизни? Т.е. почему вышележащие потоки обычно хранят используемые объекты внутри себя? Да, это может быть, неплохо ложится на C++ное RAII, но если подумать:
Возникают такие проблемы:
- Фактически, с такой моделью, мы считаем низкоуровневый поток частью высокоуровневого, и таким образом, узурпируем его функциональность исключительно для реализации работы одного конкретного объекта высокоуровневого потока. Почему узурпируем? Потому что раз один объект - часть другого, то у нас появляется обязанность в деструкторе/финализаторе делать "освобождение" ресурсов первого. Соответственно, ни один другой объект н.у. поток хранить в себе не может, и значит, пользоваться им тоже не может.
Это ведет, в свою очередь к проблеме использования "многоцелевых" потоков, типа стандартного ввода(оно либо сильно усложняется либо становится вообще невозможным), во-вторых, повышает нагрузку на среду исполнения(для каждой отдельной комбинации потоков мы должны создавать их несколько каждого типа каждый раз), и в-третьих, усложняет комбинирование обработчиков(фактически, становится возможным только банальная линейная модель, цепь обработчиков ввода). И кстати, это, по моему мнению, довольно сильно противоречит идеологии "истинного" ООП, которая заключается в куче взаимодействующих объектов, между собой не связанных нигде, кроме точек взаимодействия. - Возникает проблема реюза состояния высокоуровневого потока. Потому что, как я уже выше написал, для каждой комбинации потоков мы должны создавать новые. Часто, для особенно сложных потоков данных, это создает невыносимо большие нагрузки на систему.
На самом деле, я еще про другие минусы этой модели недавно думал, но сейчас уже не вспомню - написал самые очевидные
А вот что вы думаете о противоположной модели, использующейся, в частности, в контексте "считывателя" Common Lisp - считыватель(штуковина, формирующая лисповые объекты из текста) и текстовые потоки друг с другом не связаны, и "высокоуровневый" поток, при вызове "read" принимает в аргументы низкоуровневый, как бы подключаясь к нему, когда нужно совершить операцию чтения. Это позволяет отделить состояние собственно считывателя(оно хранится в глобальных переменных - *read-base* и пр.) от состояния да и вообще, разновидности, используемого текстового потока, от которого нужен только интерфейс(он должен уметь считывать буквы).
Аналогия из реальной жизни - водоснабжение. Вот у нас есть труба, из которой по необходимости можно брать воду. Когда нам нужно, скажем, постирать что-либо, мы берем и подключаем к трубе стиральную машинку. Постирали - отключаем. Когда машинка, допустим, сломалась, и ее нужно поменять, нам не приходится ломать всю систему водоснабжения в доме, и строить ее заново - машинка не приварена к трубе, мы просто берем ее, выкидываем, или относим на ремонт, и приносим новую. Если в трубе вдруг пропала вода, мы подключаем машинку к другой трубе. Ну и так далее.
Конечно, в контексте программирования - если делать так, как это все делается в контексте лиспового reader'а, то возникает проблема разрастания количества аргументов функций по мере повышения количества слоев обработчиков. Но, тут можно пойти по тому пути, который используется, например, в обработчиках аудио и видео контента, например в виндовом DirectShow - там есть объекты фильтров, которые как-то перегоняют информацию из одной формы в другую. Фильтры создаются заранее, и потом, когда надо, "подключаются" друг к другу(у каждого фильтра есть т.н. разъемы, которые можно друг с другом соединять).
Вобщем, вот так вот. Что думаете?
upd. Вот пример того, о чем я говорю, в терминах интерфейсов на C#: http://pastebin.com/jvcb2F8g
(все-таки не совсем точно описал, что уже заметно по ответам в juick. Да, модель .NET предполагает что весь стек "читалок" стоит на фундаменте Stream, который основан на неуправляемых ресурсах. А я говорю про то, что архитектура должна быть слабосвязной, и "параллельной" (потоки должны связываться входами и выходами, динамически, как в графе фильтров DShow, а не скрывать друг друга или какой-то один "низкоуровневый" поток))
upd2. Короче, вот картинка идеи:

Кстати, на такую модель идеально ложится асинхронность, например.
no subject
Date: 2012-01-31 12:50 pm (UTC)no subject
Date: 2012-01-31 12:53 pm (UTC)no subject
Date: 2012-01-31 01:00 pm (UTC)Инкапсуляция это отделение интерфейса от реализации. Низкоуровневый поток _в_любом_случае_ соприкасается с интерфейсом в.у. Сюда же про глобальность - если мы поставляем н.у. поток в конструктор - мы откуда-то его берем, и он там уже как-бы виден, да?
no subject
Date: 2012-01-31 04:03 pm (UTC)"Низкоуровневый поток" - насколько низкоуровневый? Допустим, поток читающий байты с обычного файла на жётском диске - уже довольно высокоуровнеая конструкция.
Если создаются и уничтожаются объекты представляющие так называемые н.у. потоки и при этом явно присутствует возможность повторного использования, это понятное дело - хрень. Кроме того, если такая возможность повторного использования возможна, так может вообще применяется неправильная модель? Объект должен представлять собой (в классическом ООП), помимо методов, ещё и данные которые имеют значение непосредственно для объекта. А если после выполнения своего предназначения в объекте остались данные которые хочется использовать повторно - уже что-то не то.
P.S. И вообще всё эта тусовка объектов агрегированных и агрегирующих нужна тогда когда нужно, в частности, наследование. А нужно ли это в данном контексте? Не перебор ли это? Нет ли возможности получить не меньшую гибкость создав/формализовав некоторые условия для удобного комбинирования ф-уий. Ну это так - для затравки:)
no subject
Date: 2012-01-31 04:07 pm (UTC)Ну: BinaryStream < TextStream < LispObjectStream
no subject
Date: 2012-01-31 04:10 pm (UTC)no subject
Date: 2012-02-01 04:17 pm (UTC)Вопрос наверное в том, как более оптимально распредилить фукционал, или "как оптимально поделить зоны отвестственности" между сущностями представляющими обработчики, будь то объекты или функции. В частности в разработке API для подключения этих самых обработчиков.
По поводу свежей картинки: ну на уровне агрегации/композиции объектов вполне можно реализовать представленную логику. Интересно было бы некоторое простое API для с такими вот цепочками. В духе:
- узнать есть ли в такой-то позиции обработчик(фильтр)
- добавить/удалить обработчик
- получении иерархии обработчиков
- создание новой цепочки обработчиков
- возможно что-то ещё для логирования/отладки.
Ну как то так..
no subject
Date: 2012-01-31 06:29 pm (UTC)Пример с DirectShow, кстати, очень показателен (многие системы устроены схожим образом: GStreamer, Jack; так же многие инструменты для machine learning).
no subject
Date: 2012-02-05 11:42 am (UTC)Картинка на зеленом фоне напомнила пример использования coroutines в python.
Все 4 коробки(source, filter, sink), можно назвать генераторами. Каждый последующий получает то что сгенерировал предыдущий.
Вот Source:
def grep(pattern):
__while True:
____line = (yield)
____if pattern in line:
______print line
Эта функция возвращает экземпляр объекта реализующего интерфейс iterator, так как она содержит оператор yield.
Используем:
g = grep("python")
g.send("lorem ipsum python")
g.send("lisp bla bla")
g.close()
То что передаётся методу send() присваивается переменной line в функции grep, while проходит один цикл и снова приостанавливается на операторе yield, ожидая следующего вызова send.
Чтобы выстроить конвейер, или цепь этих фильтров, нужно передавать в функции второй параметр:
def grep(pattern, target):
__while True:
____line = (yield)
____if pattern in line:
______target.send(line)
где параметр target это функция которая тоже содержит оператор yield.
Получится цепь фильтров, или генераторов: grep()->target()->и так далее.
От grep можно послать данные сразу нескольким следующим фильтрам, получается broadcasting:
def grep(pattern, targets):
__while True:
____line = (yield)
____if pattern in line:
______for target in targets:
________target.send(line, next_generator_after_target)
Фильтры которые в списке target могут направить свой результат одному конечному фильтру(next_generator_after_target).
Ответвления фильтров сходятся к одному фильтру, потом опять могут расходится.
Конвейер любой конфигурации, который может изменятся в зависимости от условий в динамической среде языка программирования.
To есть yield в python представляет собой connection с твоего чертежа.
Метод close() запускает exception и завершение работы предыдущего генератора можно "поймать":
def grep(pattern, target):
__try:
____while True:
______line = (yield)
______if pattern in line:
________target.send(line)
__except GeneratorExit:
____print "Goodbye"