Давайте попробуем.
Пусть у нас есть абстрактные интерфейсы чтения и записи двоичных данных. И разные классы, способные хранить двоичные данные (допустим, кольцевой буфер, файл, ещё какой-нибудь буфер, файл в памяти, BLOB, да что угодно).
Ну, самая грубая ошибка проектирования была бы, конечно, реализовать интерфейсы чтения и записи прямо в классе-носителе данных. Ну такую ошибку вряд ли допустит кто-то, кроме студентов, но всё может быть...
Следуя BlackBox-овому паттерну Carrier-Rider-Mappper (точнее, его первой части Carrier-Rider) мы сделаем объекты чтения и записи (riders - объекты доступа), реализующие соотв. интерфейсы, и позволим иметь много объектов доступа к одному носителю. Это несколько похоже на итераторы.
Кроме возможности "быть в разных позициях" одновременно, это даёт возможность разделять полномочия кода - если алгоритм просит только Reader, то он не может получить доступ к носителю на модификацию (правда, в стандартных ББ-шных компонентах можно и через Reader добраться до его носителя, но я лично обычно изолирую).
Далее. У объектов доступа есть возможность позиционирования (некая пара Pos(): INTEGER, SetPos (INTEGER)). Но не все объекты допускают позиционирование (например, буфер поступающих из сети данных с однократным чтением). Решение в рамках интерфейсов - вынести операции позиционирования в отдельный интерфейс Slider - и пусть объекты доступа одних носителей этот интерфейс реализуют, а других - нет.
Но ещё лучше "допроектировать" и сделать этот Slider отдельным объектом, тогда можно, опять же явно декларировать и гарантировать, что некоторый алгоритм - "однопроходный", т.к. он не имеет доступа к Slider-у. Например, он имеет право дописывать с того места, на котором ему дали writer, а модифицировать что-то ещё в буфере - нет.
Особенно актуальны такие вещи, если мы работаем через такие интерфейсы с ОЗУ и разными хранилищами, а не тупо с файлами или сетью...
Нужно отметить, что идею с отделением объекта Slider некогда у нас тут высказал Сергей Губанов.
Живой пример, как это выглядит (с "купюрами", правда):
Код:
DEFINITION SomemylibData;
TYPE
Carrier = POINTER TO ABSTRACT RECORD
(c: Carrier) NewReader (): Reader, NEW, ABSTRACT;
(c: Carrier) NewWriter (): Writer, NEW, ABSTRACT;
(c: Carrier) Slider (): Slider, NEW, ABSTRACT
END;
Rider = POINTER TO ABSTRACT RECORD END;
Reader = POINTER TO ABSTRACT RECORD (Rider)
(r: Reader) Available (): LONGINT, NEW, ABSTRACT;
(r: Reader) LookBytes (OUT x: ARRAY OF BYTE; beg, len: INTEGER), NEW, ABSTRACT;
(r: Reader) ReadBytes (OUT x: ARRAY OF BYTE; beg, len: INTEGER), NEW, ABSTRACT;
(r: Reader) Skip (len: INTEGER), NEW, ABSTRACT
END;
Writer = POINTER TO ABSTRACT RECORD (Rider)
(w: Writer) WriteBytes (IN x: ARRAY OF BYTE; beg, len: INTEGER), NEW, ABSTRACT
END;
Slider = POINTER TO ABSTRACT RECORD
(s: Slider) Length (rider: Rider): LONGINT, NEW, ABSTRACT;
(s: Slider) PosOf (rider: Rider): LONGINT, NEW, ABSTRACT;
(s: Slider) SetPos (rider: Rider; pos: LONGINT), NEW, ABSTRACT
END;
PROCEDURE CopyBytes (reader: Reader; writer: Writer);
END SomemylibData.
Теперь обратите внимание на важную "фишку". Если Reader и Writer - это объекты доступа, которые создаются (NewReader(), NewWriter()), потому что они должны хранить контекст взаимодействия с носителем - позицию, то любая реализация Slider - это синглтон. Реализация слайдера не имеет состояния, создана в единственном экземпляре, возвращается одна и та же процедурой Slider() для всех носителей некоторого вида. А уже объект доступа, который нужно отпозиционировать, передаётся ей через параметр. Т.е., например, для некоторого модуля, реализующего какие-нить буферы, будет:
Код:
MODULE...
TYPE
Slider = POINTER TO RECORD (Data.Slider) END;
VAR slider: Slider;
PROCEDURE (buf: Buffer) Slider (): Slider;
BEGIN
RETURN slider
END Slider;
BEGIN
NEW(slider)
END ...
Получаем, что интерфейс реализован даже без накладных расходов, т.е. количество объектов в памяти не умножается... Доп. интерфейс к объекту реализуется через объект-синглтон, который несёт на себе только функции виртуализации вызова.