пятница, 3 октября 2014 г.

Коротко. Опять о фабриках

По мотивам:

Коротко. Про IStorage
Про "рефакторинг"
Коротко. Про контроль типов

Пусть у нас есть ресурс. Например файл, который "часто" открывается на чтение.

Ну бизнес-логика - "так устроена".

Ну как-то так:

type
 TReadFile = class(TInterfacedObject, IStream)
  public
   constructor Create(const aFileName: String);
 end;//TReadFile

...

var
 l_F : IStream;
begin
 l_F := TReadFile.Create('aFileName');
end;

И мы (вдруг) понимаем, что "открытие файла" (в главном потоке) это - "бутылочное горлышко".

(Это - надуманный пример, но он "близок к жизни")

Как можно "улучшить ситуацию"?

Ну для начала сделаем так:

type
 TReadFile = class(TInterfacedObject, IStream)
  public
   constructor Create(const aFileName: String);
   class function Make(const aFileName: String): IStream;
 end;//TReadFile

...

class function TReadFile.Make(const aFileName: String): IStream;
begin
 Result := Create(aFileName);
end;

...

var
 l_F : IStream;
begin
 l_F := TReadFile.Make('aFileName');
end;

-- что мы тут сделали?

Да пока - "ничего", просто заменили конструктор, на фабрику.

Что можно сделать дальше?

Ну например можно попробовать "собрать статистику" (потокозащищённость - оставлю "за скобками"):

type
 TReadFileInfo = record
  public
   rFileName : String;
   rOpenCount : String;
   constructor Create(const aFileName: String);
 end;//TReadFileInfo

 TReadFileInfoList = class(TList<TReadFileInfo>)
  protected
   procedure IncOpenCount(const aFileName: String);
 end;//TReadFileInfoList

 TReadFile = class(TInterfacedObject, IStream)
  private
   class var f_ReadFileInfoList : TReadFileInfoList;
  protected
   class constructor Create;
   class destructor Destroy;
  public
   constructor Create(const aFileName: String);
   class function Make(const aFileName: String): IStream;
 end;//TReadFile

...

constructor TReadFileInfo.Create(const aFileName: String);
begin
 inherited;
 rFileName := aFileName;
 rOpenCount := 0;
end;

...

procedure TReadFileInfoList.IncOpenCount(const aFileName: String);
var
 l_Index : Integer;
 l_Item : TReadFileInfo;
begin
 for l_Index := 0 to Pred(Count) do
 begin
  l_Item := Items[l_Index];
  if (l_Item.rFileName = aFileName) then
  begin
   Inc(l_Item.rOpenCount);
   Items[l_Index] := l_Item;
   Exit;
  end;//l_Item.rFileName = aFileName
  Add(TReadFileInfo.Create(aFileName));
 end;//for l_Index
end;

...

class constructor TReadFileInfoList.Create;
begin
 inherited;
 f_ReadFileInfoList := TReadFileInfoList.Create;
end;

class destructor TReadFileInfoList.Destroy;
begin
 FreeAndNil(f_ReadFileInfoList);
 inherited;
end;

...

class function TReadFile.Make(const aFileName: String): IStream;
begin
 if (CurrentTreadID = MainThreadID) then
  f_ReadFileInfoList.IncOpenCount(aFileName);
 Result := Create(aFileName);
end;

...

var
 l_F : IStream;
begin
 l_F := TReadFile.Make('aFileName');
end;

-- что мы тут сделали?

А мы тут посчитали число открытий конкретного файла в основном потоке.

Зачем?

Пойдём дальше.

Пусть у нас есть "оценка" - cTooMuchOpened.

Взятая "с потолка", точнее из "практики".

Что можно сделать?

Ну сделаем примерно так:

type
 TReadFileInfo = record
  public
   rFileName : String;
   rOpenCount : String;
   constructor Create(const aFileName: String);
 end;//TReadFileInfo

 TReadFileInfoList = class(TList<TReadFileInfo>)
  protected
   pocedure IncOpenCount(const aFileName: String; out theTooMuchIndex: Integer);
 end;//TReadFileInfoList

 TReadFile = class(TInterfacedObject, IStream)
  private
   class var f_ReadFileInfoList : TReadFileInfoList;
  protected
   class constructor Create;
   class destructor Destroy;
  public
   constructor Create(const aFileName: String);
   class function Make(const aFileName: String): IStream;
 end;//TReadFile

...

constructor TReadFileInfo.Create(const aFileName: String);
begin
 inherited;
 rFileName := aFileName;
 rOpenCount := 0;
end;

...

const
 cTooMuchOpened = 1000;

procedure TReadFileInfoList.IncOpenCount(const aFileName: String; out theTooMuchIndex: Integer);
var
 l_Index : Integer;
 l_Item : TReadFileInfo;
begin
 theTooMuchIndex := -1;
 for l_Index := 0 to Pred(Count) do
 begin
  l_Item := Items[l_Index];
  if (l_Item.rFileName = aFileName) then
  begin
   Inc(l_Item.rOpenCount);
   Items[l_Index] := l_Item;
   if (l_Item.rOpenCount >= cTooMuchOpened) then
    theTooMuchIndex := l_Index;
   Exit;
  end;//l_Item.rFileName = aFileName
  Add(TReadFileInfo.Create(aFileName));
 end;//for l_Index
end;

...

class constructor TReadFileInfoList.Create;
begin
 inherited;
 f_ReadFileInfoList := TReadFileInfoList.Create;
end;

class destructor TReadFileInfoList.Destroy;
begin
 FreeAndNil(f_ReadFileInfoList);
 inherited;
end;

...

class function TReadFile.Make(const aFileName: String): IStream;
var
 l_TooMuchIndex : Integer;
begin
 if (CurrentTreadID = MainThreadID) then
 begin
  f_ReadFileInfoList.IncOpenCount(aFileName, l_TooMuchIndex);
  if (l_TooMuchIndex >= 0) then
   SystemLog.ToLog('File "' + aFileName + '" too much opened');
 end;//CurrentTreadID = MainThreadID
 Result := Create(aFileName);
end;

...

var
 l_F : IStream;
begin
 l_F := TReadFile.Make('aFileName');
end;

-- что мы тут сделали?

А тут мы научились "понимать", что какие-то файлы "слишком много открываются".

Что будем делать дальше?

Попробуем "закешировать" эти файлы. Т.е. "объекты их реализующие".

Ну как-то так:

type
 TReadFileInfo = record
  public
   rFileName : String;
   rOpenCount : String;
   rStream: IStream;
   constructor Create(const aFileName: String);
 end;//TReadFileInfo

 TReadFileInfoList = class(TList<TReadFileInfo>)
  protected
   pocedure IncOpenCount(const aFileName: String; out theTooMuchIndex: Integer);
 end;//TReadFileInfoList

 TReadFile = class(TInterfacedObject, IStream)
  private
   class var f_ReadFileInfoList : TReadFileInfoList;
  protected
   class constructor Create;
   class destructor Destroy;
  public
   constructor Create(const aFileName: String);
   class function Make(const aFileName: String): IStream;
 end;//TReadFile

...

constructor TReadFileInfo.Create(const aFileName: String);
begin
 inherited;
 rFileName := aFileName;
 rOpenCount := 0;
 rStream := nil;
end;

...

const
 cTooMuchOpened = 1000;

procedure TReadFileInfoList.IncOpenCount(const aFileName: String; out theTooMuchIndex: Integer);
var
 l_Index : Integer;
 l_Item : TReadFileInfo;
begin
 theTooMuchIndex := -1;
 for l_Index := 0 to Pred(Count) do
 begin
  l_Item := Items[l_Index];
  if (l_Item.rFileName = aFileName) then
  begin
   Inc(l_Item.rOpenCount);
   Items[l_Index] := l_Item;
   if (l_Item.rOpenCount >= cTooMuchOpened) then
    theTooMuchIndex := l_Index;
   Exit;
  end;//l_Item.rFileName = aFileName
  Add(TReadFileInfo.Create(aFileName));
 end;//for l_Index
end;

...

class constructor TReadFileInfoList.Create;
begin
 inherited;
 f_ReadFileInfoList := TReadFileInfoList.Create;
end;

class destructor TReadFileInfoList.Destroy;
begin
 FreeAndNil(f_ReadFileInfoList);
 inherited;
end;

...

class function TReadFile.Make(const aFileName: String): IStream;
var
 l_TooMuchIndex : Integer;
begin
 if (CurrentTreadID = MainThreadID) then
 begin
  f_ReadFileInfoList.IncOpenCount(aFileName, l_TooMuchIndex);
  if (l_TooMuchIndex >= 0) then
  begin
   SystemLog.ToLog('File "' + aFileName + '" too much opened');
   l_Item := f_ReadFileInfoList.Items[l_TooMuchIndex];
   if (l_Item.rStream <> nil) then
   begin
    Result := l_Item.rStream;
    // - возвращаем ранее закешированный поток
    Result.Seek(0, STREAM_SET);
    // - перематываем поток на начало (это - ВАЖНО), не зря я говорил (выше), про МНОГОПОТОЧНОСТЬ
    Exit;
    // - выходим, т.к. уже нашли нужный файл
   end;//l_Item.rStream <> nil
  end;//l_TooMuchIndex >= 0
 end;//CurrentTreadID = MainThreadID
 Result := Create(aFileName);
 if (l_TooMuchIndex >= 0) then
 begin
  l_Item := f_ReadFileInfoList.Items[l_TooMuchIndex];
  Assert(l_Item.rStream = nil, 'Убеждаемся в очевидном');
  l_Item.rStream := Result;
  // - запоминаем НАШ объект
  f_ReadFileInfoList.Items[l_TooMuchIndex] := l_Item;
  // - сохраняем его в КЕШЕ
 end;//l_TooMuchIndex >= 0
end;

...

var
 l_F : IStream;
begin
 l_F := TReadFile.Make('aFileName');
end;

-- что мы тут сделали?

А мы тут устроили кеш файлов. Т.е. не открываем их повторно, а получаем из кеша.

Повторю - пример надуманый, но "почти из жизни".

Какие есть проблемы?

А например такие, что "просто так DeleteFile уже сложно позвать".

Почему?

А потому, что он "может быть открыт".

Ну и "смежные проблемы".

Которые можно решить примерно так:

Звать не:

 DeleteFile('aFileName');

а:

...

class procedure TReadFile.DeleteFile(const aFileName: String);
begin
 f_ReadFileInfoList.Clear;
 // - отпустили ВСЕ файлы, хотя можно и ВЫБОРОЧНО отпускать, но не хочется загромождать код
 SysUtils.DeleteFile(aFileName);
end;

...

TReadFile.DeleteFile('aFileName');

-- но это уже - "тонкости", хотя и небезпроблемные.

Ну вот собственно и всё, что я хотел ещё сказать о фабриках.

Понимаю - "слом шаблона".

Но может быть - кому-нибудь понравится.

P.S. Может быть какие-то запятые или пробелы стоя не так как хотелось бы. Но я ЧЕСТНО СТАРАЛСЯ. Хотя и писал "с листа". И код - не компилировал. Хотел лишь идею проиллюстрировать.

P.P.S. Если кто-то захочет спросить про SHARE_READ или SHARE_WRITE - скажу - "это правильный ход мыслей", но тут же процитирую одного своего педагога, который на вопрос "почему мы берём тангенс бесконечно малого, а не само бесконечно малое" ответил - "давайте на глупые вопросы Саши Люлина не будем тратить время".

3 комментария:

  1. Думаю с нашими Application.ProcessMessages вот это вот

    if (CurrentTreadID = MainThreadID) then
    begin

    все равно не спасет. Я бы постремался так кэшировать..

    ОтветитьУдалить
    Ответы
    1. Ром, там же выше написано "потокобезопасность оставляем за скобками" :-)

      А по поводу - "нашего", здесь же чисто "идея". А вот "реализацию" - увидишь в CVS ;-) Она вовсю уже молотит :-)

      Ну и там конечно честная потокобезопасность. Без дураков.

      Удалить
  2. Ключевое слово скажи где искать. Моей фантазии не хватает чтоб придумать, как можно защититься от повторного получения стрима через processMessages в одном(!) (главном) потоке, с катастрофической поломкой Position.

    ОтветитьУдалить