love5an: (Default)
[personal profile] love5an
Я тут подумал.

Вот как обычно делается обработка IO во всяком там ООП?
Вот у нас есть какой-то скажем TextReader, из него можно читать буквы.
Окей. Но вот нам надо сделать читалку XML-нодов из текста, XmlReader.

Так вот - почему обычно делается так, что TextReader биндится к объекту XmlReader в конструкторе последнего и остается там до окончания его, XmlReader'a, жизни? Т.е. почему вышележащие потоки обычно хранят используемые объекты внутри себя? Да, это может быть, неплохо ложится на C++ное RAII, но если подумать:

Возникают такие проблемы:
  1. Фактически, с такой моделью, мы считаем низкоуровневый поток частью высокоуровневого, и таким образом, узурпируем его функциональность исключительно для реализации работы одного конкретного объекта высокоуровневого потока. Почему узурпируем? Потому что раз один объект - часть другого, то у нас появляется обязанность в деструкторе/финализаторе делать "освобождение" ресурсов первого. Соответственно, ни один другой объект н.у. поток хранить в себе не может, и значит, пользоваться им тоже не может.

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


  2. Возникает проблема реюза состояния высокоуровневого потока. Потому что, как я уже выше написал, для каждой комбинации потоков мы должны создавать новые. Часто, для особенно сложных потоков данных, это создает невыносимо большие нагрузки на систему.



На самом деле, я еще про другие минусы этой модели недавно думал, но сейчас уже не вспомню - написал самые очевидные

А вот что вы думаете о противоположной модели, использующейся, в частности, в контексте "считывателя" Common Lisp - считыватель(штуковина, формирующая лисповые объекты из текста) и текстовые потоки друг с другом не связаны, и "высокоуровневый" поток, при вызове "read" принимает в аргументы низкоуровневый, как бы подключаясь к нему, когда нужно совершить операцию чтения. Это позволяет отделить состояние собственно считывателя(оно хранится в глобальных переменных - *read-base* и пр.) от состояния да и вообще, разновидности, используемого текстового потока, от которого нужен только интерфейс(он должен уметь считывать буквы).

Аналогия из реальной жизни - водоснабжение. Вот у нас есть труба, из которой по необходимости можно брать воду. Когда нам нужно, скажем, постирать что-либо, мы берем и подключаем к трубе стиральную машинку. Постирали - отключаем. Когда машинка, допустим, сломалась, и ее нужно поменять, нам не приходится ломать всю систему водоснабжения в доме, и строить ее заново - машинка не приварена к трубе, мы просто берем ее, выкидываем, или относим на ремонт, и приносим новую. Если в трубе вдруг пропала вода, мы подключаем машинку к другой трубе. Ну и так далее.

Конечно, в контексте программирования - если делать так, как это все делается в контексте лиспового reader'а, то возникает проблема разрастания количества аргументов функций по мере повышения количества слоев обработчиков. Но, тут можно пойти по тому пути, который используется, например, в обработчиках аудио и видео контента, например в виндовом DirectShow - там есть объекты фильтров, которые как-то перегоняют информацию из одной формы в другую. Фильтры создаются заранее, и потом, когда надо, "подключаются" друг к другу(у каждого фильтра есть т.н. разъемы, которые можно друг с другом соединять).

Вобщем, вот так вот. Что думаете?

upd. Вот пример того, о чем я говорю, в терминах интерфейсов на C#: http://pastebin.com/jvcb2F8g
(все-таки не совсем точно описал, что уже заметно по ответам в juick. Да, модель .NET предполагает что весь стек "читалок" стоит на фундаменте Stream, который основан на неуправляемых ресурсах. А я говорю про то, что архитектура должна быть слабосвязной, и "параллельной" (потоки должны связываться входами и выходами, динамически, как в графе фильтров DShow, а не скрывать друг друга или какой-то один "низкоуровневый" поток))

upd2. Короче, вот картинка идеи:

Кстати, на такую модель идеально ложится асинхронность, например.

Date: 2012-01-31 12:50 pm (UTC)
From: [identity profile] 4da.livejournal.com
s/функционал/функциональность

Date: 2012-01-31 12:53 pm (UTC)
From: [identity profile] 4da.livejournal.com
Т.е весь смысл поста - глобальные потоки и их комбинация + реюз против инкапсуляция потоков через агрегацию объектов в классах.

Date: 2012-01-31 01:00 pm (UTC)
From: [identity profile] love5an.livejournal.com
Ээээ, не инкапсуляция, а агрегация и сокрытие данных.
Инкапсуляция это отделение интерфейса от реализации. Низкоуровневый поток _в_любом_случае_ соприкасается с интерфейсом в.у. Сюда же про глобальность - если мы поставляем н.у. поток в конструктор - мы откуда-то его берем, и он там уже как-бы виден, да?

Date: 2012-01-31 04:03 pm (UTC)
From: [identity profile] linkfly.livejournal.com
Чувствую, что недопонимаю происходящее, но тем не менее ...
"Низкоуровневый поток" - насколько низкоуровневый? Допустим, поток читающий байты с обычного файла на жётском диске - уже довольно высокоуровнеая конструкция.
Если создаются и уничтожаются объекты представляющие так называемые н.у. потоки и при этом явно присутствует возможность повторного использования, это понятное дело - хрень. Кроме того, если такая возможность повторного использования возможна, так может вообще применяется неправильная модель? Объект должен представлять собой (в классическом ООП), помимо методов, ещё и данные которые имеют значение непосредственно для объекта. А если после выполнения своего предназначения в объекте остались данные которые хочется использовать повторно - уже что-то не то.

P.S. И вообще всё эта тусовка объектов агрегированных и агрегирующих нужна тогда когда нужно, в частности, наследование. А нужно ли это в данном контексте? Не перебор ли это? Нет ли возможности получить не меньшую гибкость создав/формализовав некоторые условия для удобного комбинирования ф-уий. Ну это так - для затравки:)

Date: 2012-01-31 04:07 pm (UTC)
From: [identity profile] love5an.livejournal.com
низкоуровневый в том плане, что ниже уровнем чем поток, использующий его. Ну, формально. Я имею ввиду, то, что используется для генерации данных - н.у., а сами данные - в.у.(для следующего потока в цепочке - предыдущий как бы ниже уровнем)
Ну: BinaryStream < TextStream < LispObjectStream

Date: 2012-01-31 04:10 pm (UTC)
From: [identity profile] love5an.livejournal.com
Да я как раз против агрегирования в данном контексте, и за комбинирование. Но, в контексте ООП, а не ФП.

Date: 2012-02-01 04:17 pm (UTC)
From: [identity profile] linkfly.livejournal.com
Агрегирование в данном контексте (и вообще в контексте ООП) я понимаю как содержание в объекте (который представляет поток), другого объекта, который представляет другой, "более низкоуровневый поток", ему кстати (этому объекту) делегируется обработка части запросов. Суть - композиция. А как понимается в данном контексте комбинирование? Композиция в сущности тоже комбинирование. Может есть смысл говорить тогда уж о "более оптимальном комбинировании функционала"? Ну да ладно, это вопрос терминологии.

Вопрос наверное в том, как более оптимально распредилить фукционал, или "как оптимально поделить зоны отвестственности" между сущностями представляющими обработчики, будь то объекты или функции. В частности в разработке API для подключения этих самых обработчиков.

По поводу свежей картинки: ну на уровне агрегации/композиции объектов вполне можно реализовать представленную логику. Интересно было бы некоторое простое API для с такими вот цепочками. В духе:
- узнать есть ли в такой-то позиции обработчик(фильтр)
- добавить/удалить обработчик
- получении иерархии обработчиков
- создание новой цепочки обработчиков
- возможно что-то ещё для логирования/отладки.

Ну как то так..

Date: 2012-01-31 06:29 pm (UTC)
From: [identity profile] dmitry-vk.livejournal.com
Я думаю, что такие API характерны для окружений, в которых сложно или не удобно или не принято делать корутины (или по треду на каждый поток). Поэтому и возможности взаимодействия и протаскивания состояния потоков зачастую строго ограничиваются чем-то одним. Какие-то иные модели взаимодействия реализуются, только если явная необходимость (да, не все видят вообще убогость и ограниченность вложения стримов).
Пример с DirectShow, кстати, очень показателен (многие системы устроены схожим образом: GStreamer, Jack; так же многие инструменты для machine learning).

Date: 2012-02-05 11:42 am (UTC)
From: (Anonymous)
Ничего нового.
Картинка на зеленом фоне напомнила пример использования 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"

Profile

love5an: (Default)
Dmitry Ignatiev

June 2020

S M T W T F S
 123456
78910 111213
14151617181920
21222324252627
282930    

Most Popular Tags

Style Credit

Expand Cut Tags

No cut tags
Page generated Jul. 16th, 2025 03:56 pm
Powered by Dreamwidth Studios