среда, 3 сентября 2014 г.

Коротко. Ещё о фабриках

В некотором смысле по мотивам:

Коротко. О фабриках
Маниловщина. Пишем реализацию IStorage применяя TDD

О чём хочу повести речь?

У нас своя есть реализация IStorage (ну и IStream соответственно).

Она - хороша. В некотором смысле. Хотя бы потому, что достаточно стабильно работает уже лет 15-ть.

Но там есть "некоторые проблемы".

И я с этими проблемами сейчас скурпулёзно разбираюсь.

Ну там "проблемы" сложного порядка в гетерогенной сетевой среде.

Я не о них.

Но есть и "локальные проблемы".

Например то, что там всё построено на "бинарной сериализации".

Т.е. сделано примерно так:

type
 TStoreHeader = record
  rNextPosition : Int64;
  rRealSize : Int64;
  ...
 end;//TStoreHeader

...

procedure SomeReadCode;
var
 l_H : TStoreHeader;
begin
 ...
 Stream.Read(l_H, SizeOf(l_H);
 ...
end;

...

procedure SomeWriteCode;
var
 l_H : TStoreHeader;
begin
 ...
 Stream.Write(l_H, SizeOf(l_H);
 ...
end;

"Реальный код" можно посмотреть тут.

В чём проблема? А в том, что "формат" TStoreHeader "просто так" не изменишь. Потому, что "всё поедет".

Что делать?

Для начала вот примерно так:

type
 TStoreHeaderRec = record
  rNextPosition : Int64;
  rRealSize : Int64;
  ...
 end;//TStoreHeaderRec

 TStoreHeader = class
  private
   Data : TStoreHeaderRec;
  public
   procedure Load(aStream: TStream);
   procedure Save(aStream: TStream);
 end;//TStoreHeader

...

procedure TStoreHeader.Load(aStream: TStream);
begin
 aStream.ReadBuffer(Data, SizeOf(Data);
end;

procedure TStoreHeader.Save(aStream: TStream);
begin
 aStream.WriteBuffer(Data, SizeOf(Data);
end;

...

procedure SomeReadCode;
var
 l_H : TStoreHeader;
begin
 ...
 l_H := TStoreHeader.Create;
 ...
 l_H.Load(Stream);
 ...
end;

...

procedure SomeWriteCode;
var
 l_H : TStoreHeader;
begin
 ...
 l_H := TStoreHeader.Create;
 ...
 l_H.Save(Stream);
 ...
end;

Какой следующий шаг?

Вот как-то так:

type
 TStoreHeaderAbstract = class
  public
   procedure Load(aStream: TStream); virtual; abstract;
   procedure Save(aStream: TStream); virtual; abstract;
 end;// TStoreHeaderAbstract

...

 TStoreHeaderRec = record
  rNextPosition : Int64;
  rRealSize : Int64;
  ...
 end;//TStoreHeaderRec

 TStoreHeader = class(TStoreHeaderAbstract)
  private
   Data : TStoreHeaderRec;
  public
   procedure Load(aStream: TStream); override;
   procedure Save(aStream: TStream); override;
 end;//TStoreHeader

 TStoreHeaderFactory = class
  public
   class function Make: TStoreHeaderAbstract;
 end;//TStoreHeaderFactory

...

class function TStoreHeaderFactory.Make: TStoreHeaderAbstract;
begin
 Result := TStoreHeader.Create;
end;

procedure TStoreHeader.Load(aStream: TStream);
begin
 aStream.ReadBuffer(Data, SizeOf(Data);
end;

procedure TStoreHeader.Save(aStream: TStream);
begin
 aStream.WriteBuffer(Data, SizeOf(Data);
end;

...

procedure SomeReadCode;
var
 l_H : TStoreHeaderAbstract;
begin
 ...
 l_H := TStoreHeaderFactory.Make;
 ...
 l_H.Load(Stream);
 ...
end;

...

procedure SomeWriteCode;
var
 l_H : TStoreHeaderAbstract;
begin
 ...
 l_H := TStoreHeaderFactory.Make;
 ...
 l_H.Save(Stream);
 ...
end;

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

Мы ввели базовый абстрактный класс - TStoreHeaderAbstract и фабрику -TStoreHeaderFactory.

Теперь как нам поменять формат данных?

А вот примерно так:

type
 TStoreHeaderAbstract = class
  public
   procedure Load(aStream: TStream); virtual; abstract;
   procedure Save(aStream: TStream); virtual; abstract;
 end;// TStoreHeaderAbstract

...

 TStoreHeaderRec = record
  rNextPosition : Int64;
  rRealSize : Int64;
  ...
 end;//TStoreHeaderRec

 TStoreHeader = class(TStoreHeaderAbstract)
  private
   Data : TStoreHeaderRec;
  public
   procedure Load(aStream: TStream); override;
   procedure Save(aStream: TStream); override;
 end;//TStoreHeader

 TStoreHeaderRecNew = record
  rNextPosition : Int64;
  rRealSize : Int64;
  rSomeOtherData : SomeOtherType;
  ...
 end;//TStoreHeaderRecNew

 TStoreHeaderNew = class(TStoreHeaderAbstract)
  private
   Data : TStoreHeaderRecNew;
  public
   procedure Load(aStream: TStream); override;
   procedure Save(aStream: TStream); override;
 end;//TStoreHeaderNew

 TStoreHeaderFactory = class
  public
   class function Make(aVersion : TGUID): TStoreHeaderAbstract;
 end;//TStoreHeaderFactory

...

class function TStoreHeaderFactory.Make(aVersion : TGUID): TStoreHeaderAbstract;
begin
 if EqualGUID(aVersion, OldFormatGUID) then
  Result := TStoreHeader.Create
 else
 if EqualGUID(aVersion, NewFormatGUID) then
  Result := TStoreHeaderNew.Create
 else
  Assert(false, 'Неверный заголовок');
end;

procedure TStoreHeader.Load(aStream: TStream);
begin
 aStream.ReadBuffer(Data, SizeOf(Data);
end;

procedure TStoreHeader.Save(aStream: TStream);
begin
 aStream.WriteBuffer(Data, SizeOf(Data);
end;

...

procedure TStoreHeaderNew.Load(aStream: TStream);
begin
 aStream.ReadBuffer(Data, SizeOf(Data);
end;

procedure TStoreHeaderNew.Save(aStream: TStream);
begin
 aStream.WriteBuffer(Data, SizeOf(Data);
end;

...

procedure SomeReadCode;
var
 l_H : TStoreHeaderAbstract;
begin
 ...
 l_H := TStoreHeaderFactory.Make(GetVersionGUID);
 ...
 l_H.Load(Stream);
 ...
end;

...

procedure SomeWriteCode;
var
 l_H : TStoreHeaderAbstract;
begin
 ...
 l_H := TStoreHeaderFactory.Make(GetVersionGUID);
 ...
 l_H.Save(Stream);
 ...
end;

Более того можно тогда написать и так:

...
procedure TStoreHeaderNew.Load(aStream: TStream);
begin
 aStream.ReadBuffer(Data.rRealSize, SizeOf(Data.rRealSize);
 aStream.ReadBuffer(Data.rNextPosition, SizeOf(Data.rNextPosition);
 aStream.ReadBuffer(Data.rSomeOtherData, SizeOf(Data.rSomeOtherData);
end;

procedure TStoreHeaderNew.Save(aStream: TStream);
begin
 aStream.WriteBuffer(Data.rRealSize, SizeOf(Data.rRealSize);
 aStream.WriteBuffer(Data.rNextPosition, SizeOf(Data.rNextPosition);
 aStream.WriteBuffer(Data.rSomeOtherData, SizeOf(Data.rSomeOtherData);
end;

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

Мы во-первых - разбили "бинарную сериализацию" записи на несколько "бинарных сериализаций" отдельных полей.

А во-вторых - мы поменяли часть полей местами, чтобы продемонстрировать "суть подхода".

За рамками повествования конечно осталось много вопросов.

Например - "откуда берётся GetVersionGUID"?

Или - "что делать, если запись версии изначально не предусмотрены"?

Это важные вопросы. Но они "не влезают в рамки" данного поста. Да и вообще говоря - они важные, но "достаточно технические". Если будет интерес - я и их подробнее разберу.

Но пока - оставлю их "за рамками повествования".

Что в итоге?

В  итоге - по-моему - было показано как фабрики являются весомым дополнением к инкапсуляции и полиморфизму.

Мы сначала воспользовались полиморфизмом - введя тип TStoreHeaderAbstract.
А потом воспользовались инкапсуляцией - разделив TStoreHeader.Data и TStoreHeaderNew.Data.

Ну и за счёт полиморфиза и инкапсуляции мы в некотором роде ушли от "бинарной сериализации".

Почему?

Потому, что следующий шаг может быть таким:

...
procedure TStoreHeaderNew.Load(aStream: TStream);
begin
 aStream.ReadBuffer(Data.rRealSize, SizeOf(Data.rRealSize);
 aStream.ReadBuffer(Data.rNextPosition, SizeOf(Data.rNextPosition);
 Data.rSomeOtherData.Load(aStream);
end;

procedure TStoreHeaderNew.Save(aStream: TStream);
begin
 aStream.WriteBuffer(Data.rRealSize, SizeOf(Data.rRealSize);
 aStream.WriteBuffer(Data.rNextPosition, SizeOf(Data.rNextPosition);
 Data.rSomeOtherData.Save(aStream);
end;

- т.е. тут уже пишем/читаем не "бинарно", а так как написано в SomeOtherDataType.Load/SomeOtherDataType.Save.

Итак.

Что я хотел показать?

Я повторю.

Я хотел показать, что фабрики являются весомым дополнением к инкапсуляции и полиморфизму.

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

Как уж это у меня получилось - судить вам.

Думаю - "Америку не открыл", но надеюсь, что написал что-то полезное.

Offtopic. О "текущем международном положении"

О Ливии.. Ираке... Египте... Сирии... И много о чём....


Сатановский предрекает "развал ИГИЛ" мотивируя тем что "они между собой не договорятся".. И предрекает "тяжёлую гражданскую войну"... И мне кажется, что СТОИТ с ним согласиться... Почему Приведу простой пример - "попробуйте поставить домофон в подъезде и собрать деньги СО ВСЕХ"... Просто попробуйте..

Так что - "держитесь люди"... :-(

Хафтар, Халифа

Offtopic.О репутации

Я сегодня покупал машину.. Через мастера (точнее хозяина автосервиса).. Я у него лет 5-ть уже чинюсь.. Я ему честно сказал - "Юра, подкупает покупать машину через вас... потому что я всё равно приеду к вам чиниться... Посему - хотя никому нельзя верить, но я подставы от вас не жду"... И потом добавил - "хотя вряд ли я вас чем-то прижму".. Знаете что он мне сказал? "А моя репутация?"...
Ещё раз... "А моя репутация?"
Знаете - ОТРАДНО было это слышать.. есть люди, которые думают о РЕПУТАЦИИ...
О репутации...
Как будет дальше - "история покажет".. Я напишу - "если вдруг разочаруюсь"... Но пока - я удивлён...

Повторю - есть люди,которые говорят "в глаза" - "а как же моя репутация"...

Ссылка. Опять Тепляков. Open/Closed Principle. ФП vs. ООП

http://sergeyteplyakov.blogspot.ru/2014/09/openclosed-principle-fp-vs-oop.html

Если я ПРАВИЛЬНО понял, то там речь и о "анемичной модели" - ТОЖЕ.

Ссылка. RAD Studio XE7, Delphi XE7 и C++Builder XE7

Ссылка. Пара слов о Delphi XE7.

вторник, 2 сентября 2014 г.

Коротко. О рефакторинге. "С колёс"

Совсем не стремлюсь открыть Америку и тем более чему-то "научить".

Скажем так - "меня попросили сделать review кода".

Поэтому просто дам "пример переписывания кода".

Был код:

procedure TraverseTreeItems(const aTree : TTreeViewItem; var ResultList : TList<TTreeViewItem>);
var
 i : Integer;
begin
 for i := 0 to Pred(aTree.Count)  do
 begin
  if aTree.Items[i].IsChecked
   then ResultList.Add(aTree.Items[i]);

  TraverseTreeItems(aTree.Items[i], ResultList);
 end;
end;

procedure TraverseTree(const aTree: TTreeView; var ResultList : TList<TTreeViewItem>);
var
 i : integer;
begin
 for i := 0 to Pred(aTree.Count) do
 begin
  if aTree.Items[i].IsChecked then
   ResultList.Add(aTree.Items[i]);

  TraverseTreeItems(aTree.Items[i], ResultList);
 end;
end;

Вообще говоря, можно написать так:

type
 TTreeViewItemList = TList<TTtreeViewItem>;

...

procedure TraverseTreeItem(anItem : TTreeViewItem; ResultList : TTreeViewItemList);
var
 i : Integer;
begin
 if anItem.IsChecked then
  ResultList.Add(anItem);
 
 for i := 0 to Pred(anItem.Count)  do
  TraverseTreeItems(anItem.Items[i], ResultList);
end;

procedure TraverseTree(aTree: TTreeView; ResultList : TTreeViewItemList);
var
 i : integer;
begin
 for i := 0 to Pred(aTree.Count) do
  TraverseTreeItem(aTree.Items[i], ResultList);
end;

-- и сделать тест, что оба алгоритма - дают одинаковый результат :-) Рандомный. Как мы когда-то писали. (Про тесты я напишу позже, если конечно интересно)

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

Мы перенесли "ответственность" по проверке aTree.Items[i].IsChecked в anItem.IsChecked (из одного метода в другой).

Т.е. спустили часть одного метода в другой.

Ну и ввели "алиас" - TTreeViewItemList.

Ну и убрали лишние const и var. (Кстати в Delphi не хватает всё же спецификаторов параметров типа constref и varref или constobject и varobject и спецификатора const на метод, особенно getter, ну по аналогии с C++, но правда тогда и mutable может понадобиться)

Вот собственно и всё. Ничего "космического".

Мелочь? Да - мелочь! Но из таких "мелочей" и складывается читабельный, сопровождаемый и тестируемый код.

P.S. Можно ещё generic'и применить. Но надо ли? Для одной строчки.

Update.

P.P.S. кстати к TTreeView и TTreeViewItem можно enumerator'ы привесить через helper'ы
попробуете?

http://programmingmindstream.blogspot.ru/2014/08/for-in.html

Или:

for anItem in TTreeViewEnumerator.Get(aTree) do ...
for anItem in TTreeViewEnumerator.Get(aTree) do ...

Где - TTreeViewEnumerator.Get это class function: TEnumerator с overload

мысль понятна?

Тогда можно будет написать так:

type
 TTreeViewItemList = TList<TTtreeViewItem>;

...

procedure TraverseTreeItem(anItem : TTreeViewItem; ResultList : TTreeViewItemList);
var
 l_Item : TTreeViewItem;
begin
 if anItem.IsChecked then
  ResultList.Add(anItem);
 
 for l_Item in TTreeViewEnumerator.Get(anItem)  do
  TraverseTreeItems(anItem.Items[i], ResultList);
end;

procedure TraverseTree(aTree: TTreeView; ResultList : TTreeViewItemList);
var
 l_Item : TTreeViewItem;
begin
 for l_Item in TTreeViewEnumerator.Get(aTree) do
  TraverseTreeItem(l_Item, ResultList);
end;

- мысль понятна?