love5an: (Default)
Dmitry Ignatiev ([personal profile] love5an) wrote2012-01-31 04:36 pm
Entry tags:

Ъ ООП I/O

Я тут подумал.

Вот как обычно делается обработка 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. Короче, вот картинка идеи:

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

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