понедельник, 4 ноября 2013 г.

Коротко. О GUI-тестировании "по-русски"

Про реальную разработку написано тут:
http://18delphi.blogspot.ru/2013/10/blog-post_7652.html
http://18delphi.blogspot.ru/2013/10/blog-post_7099.html

Теперь попробую описать собственно идею.

Пусть у нас есть форма:
TSomeForm = class(TForm)
 SomeButton : TButton;
end;//TSomeForm

Пусть мы хотим иметь возможность нажимать на кнопку SomeButton на форме SomeForm.
Из наших тестов.

Этому нам помогут следующие слова тестовой машины:

interface

TkwFindForm = class(TscriptKeyWord)
protected
 procedure DoIt(aContext : TscriptContext); override;
end;//TkwFindForm

TkwFindComponent = class(TscriptKeyWord)
protected
 procedure DoIt(aContext : TscriptContext); override;
end;//TkwFindComponent

TkwButtonClick = class(TscriptKeyWord)
protected
 procedure DoIt(aContext : TscriptContext); override;
end;//TkwButtonClick

И реализация:

implementation

procedure TkwFindForm.DoIt(aContext : TscriptContext);
var
 l_Name : String;
 l_Form : TForm;
begin
 l_Name := aContext.PopString;
 Assert(l_Name <> '');
 l_Form := Screen.FindFormByName(l_Name); // - такого метода нет, но его легко можно написать
 Assert(l_Form <> nil);
 aContext.PushObject(l_Form);
end;

procedure TkwFindComponent.DoIt(aContext : TscriptContext);
var
 l_Name : String;
 l_Form : TForm;
 l_Component : TComponent;
begin
 l_Name := aContext.PopString;
 l_Form := aContext.PopObject As TForm;
 Assert(l_Name <> '');
 Assert(l_Form <> nil);
 l_Component := l_Form.FindComponent(l_Name);
 Assert(l_Component <> nil);
 aContext.PushObject(l_Component);
end;

procedure TkwButtonClick.DoIt(aContext : TscriptContext);
var
 l_Component : TComponent;
begin
 l_Component := aContext.PopObject As TComponent;
 Assert(l_Component Is TButton);
 (l_Component As TButton).Click;
end;

Регистрируем описанные слова в тестовой машине:

initialization
 ScriptEngine.RegisterWord(TkwFindForm, 'FindForm');
 ScriptEngine.RegisterWord(TkwFindComponent, 'FindComponent');
 ScriptEngine.RegisterWord(TkwButtonClick, 'ButtonCick');


Понятное, дело, что идентификаторы слов можно "придумывать" из ClassName, но это - не так важно.

Теперь мы можем написать первый тест:

VAR l_Form
VAR l_Button
'SomeForm' FindForm =: l_Form
l_Form 'SomeButton' FindComponent =: l_Button
l_Button ButtonClick

И он делает то, что хотелось.

(Обратная польская запись пусть вас не пугает. Пишу так - потому, что у меня так сделано. Исторически. Обратная польская запись или инфиксная - не суть важно.)

Но. Хотелось бы "по-русски".

Напишем "вспомогательные" слова. Уже на скриптовом языке тестовой машины.

Пусть мы знаем, что SomeForm, это форма для регистрации пользователя, а SomeButton это кнопка для сохранения.
Ну из бизнес-логики такое следует.

Напишем:

CONST "Форма регистрации" 'SomeForm'
CONST "Сохранить" 'SomeButton'

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

VAR l_Form
VAR l_Button
"Форма регистрации" FindForm =: l_Form
l_Form "Сохранить" FindComponent =: l_Button
l_Button ButtonClick

Ну отчасти "по-русски". Но - не совсем.

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

Напишем:

STRING FUNCTION "Найти форму" STRING IN aFormName
 aFormName FindForm =: Result
END //"Найти форму"

STRING FUNCTION "Найти найти компонент на форме" STRING IN aComponentName OBJECT IN aForm
 aForm aComponentName FindComponent =: Result
END //"Найти найти компонент на форме""

PROCEDURE "Нажать кнопку" OBJECT IN aButton
 aButton ButtonClick
END //"Нажать кнопку"

WORDALIAS VAR Переменная

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

Переменная "Найденная форма"
Переменная "Найденная кнопка"
"Найти форму {("Форма регистрации")}" =: "Найденная форма"
"Найти найти компонент {("Сохранить")} на форме {("Найденная форма")}" =: "Найденная кнопка"
"Нажать кнопку {("Найденная кнопка")}"

Ну и последний штрих.
Вносим написанное слово в словарь:

PROCEDURE "Нажать кнопку Сохранить на форме регистрации"
 Переменная "Найденная форма"
 Переменная "Найденная кнопка"
 "Найти форму {("Форма регистрации")}" =: "Найденная форма"
 "Найти найти компонент {("Сохранить")} на форме {("Найденная форма")}" =: "Найденная кнопка"
 "Нажать кнопку {("Найденная кнопка")}"
END //"Нажать кнопку Сохранить на форме регистрации"

Теперь тест приобретает вид:
 "Нажать кнопку Сохранить на форме регистрации"

- все служебные конструкции - "упрятаны" внутрь "слова высокого уровня" - "Нажать кнопку Сохранить на форме регистрации". 

И это уже не совсем "слово", а по сути - целое "предложение.

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

Зачем это нужно?

Ну конечно не для того, чтобы "писать по-русски". Это конечно - утопия.

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

Тест приобретает вид TestCase'а.

И его может выполнять не только скриптовая машина, но и человек.

Ну и - "самодокументируемость кода".

Таким образом выводя "нужные ручки" со стороны Delphi (не обязательно через RTTI) и описывая "высокоуровневые" слова в промежуточных словарях тестовой машины - можно быстро "набрать массу", необходимую для написания тестов в виде TestCase'ов.

Немножко о том "как всё это устроено внутри" читайте тут - http://18delphi.blogspot.ru/2013/11/gui_5.html

Что ещё может "наша модель"

Я уже приводил ссылку на чужую статью:
http://www.rsdn.ru/article/patterns/patterns.xml

Там описаны формы, модули и операции.

Также я писал про "логику форм и прецеденты".

Вот тут:
http://18delphi.blogspot.ru/2013/08/mvc.html
http://18delphi.blogspot.ru/2013/08/mvc_8.html

У нас есть ещё одно понятие - "реализация прецедента". Это программная сущность, которая выполняет собой роль контроллера форм входящих в неё. И, как следует из названия - эта сущность нужна для реализации логики пользовательского прецедента, выделенного в требованиях.

Исторически мы их назвали "фабриками сборок форм". Это был "период исканий". И тогда мы ещё не понимали, что это ИМЕННО - "реализации прецедентов". Поэтому в коде много упоминаний FormSetFactory. В коде реализации прецедентов (FormSetFactory) описываются так:

unit fsSituationSearch;

{$Include nsDefine.inc}

interface

uses
  Classes,
  vcmInterfaces,
  
  vcmFormSetFactory,
  
  vcmUserControls,

  QueryCardInterfaces,
  l3StringIDEx,
  PrimSaveLoadUserTypes_slqtKW_UserType,
  PrimAttributeSelect_utSingleSearch_UserType,
  PrimTreeAttributeSelect_astNone_UserType,
  PrimTreeAttributeFirstLevel_flSituation_UserType,
  FiltersUserTypes_utFilters_UserType,
  Common_FormDefinitions_Controls,
  Search_FormDefinitions_Controls,
  PrimSelectedAttributes_utSelectedAttributes_UserType,
  SearchLite_FormDefinitions_Controls,
  vcmFormSetFormsCollectionItemPrim,
  SimpleListInterfaces {a},
  SearchInterfaces {a},
  l3TreeInterfaces,
  vcmFormSetFactoryPrim
  ;

type
  Tfs_SituationSearch = {final fsf} class(TvcmFormSetFactory)
   {* ППС 6.х }
  protected
  // overridden protected methods
   procedure InitFields; override;
   class function GetInstance: TvcmFormSetFactoryPrim; override;
  public
  // public methods
   function SaveLoadParentSlqtKWNeedMakeForm(const aDataSource: IvcmFormSetDataSource;
      out aNew: IvcmFormDataSource;
      aSubUserType: TvcmUserType): Boolean;
     {* Обработчик OnNeedMakeForm для SaveLoad }
   function AttributeSelectParentUtSingleSearchNeedMakeForm(const aDataSource: IvcmFormSetDataSource;
      out aNew: IvcmFormDataSource;
      aSubUserType: TvcmUserType): Boolean;
     {* Обработчик OnNeedMakeForm для AttributeSelect }
   function TreeAttributeSelectParentAstNoneNeedMakeForm(const aDataSource: IvcmFormSetDataSource;
      out aNew: IvcmFormDataSource;
      aSubUserType: TvcmUserType): Boolean;
     {* Обработчик OnNeedMakeForm для TreeAttributeSelect }
   function SelectedAttributesChildUtSelectedAttributesNeedMakeForm(const aDataSource: IvcmFormSetDataSource;
      out aNew: IvcmFormDataSource;
      aSubUserType: TvcmUserType): Boolean;
     {* Обработчик OnNeedMakeForm для SelectedAttributes }
   function FiltersNavigatorUtFiltersNeedMakeForm(const aDataSource: IvcmFormSetDataSource;
      out aNew: IvcmFormDataSource;
      aSubUserType: TvcmUserType): Boolean;
     {* Обработчик OnNeedMakeForm для Filters }
   function TreeAttributeFirstLevelNavigatorFlSituationNeedMakeForm(const aDataSource: IvcmFormSetDataSource;
      out aNew: IvcmFormDataSource;
      aSubUserType: TvcmUserType): Boolean;
     {* Обработчик OnNeedMakeForm для TreeAttributeFirstLevel }
  public
  // singleton factory method
    class function Instance: Tfs_SituationSearch;
     {- возвращает экземпляр синглетона. }
  end;//Tfs_SituationSearch

implementation

uses
  l3Base {a},
  l3MessageID,
  SysUtils {a}
  ;

// start class Tfs_SituationSearch

var g_Tfs_SituationSearch : Tfs_SituationSearch = nil;

procedure Tfs_SituationSearchFree;
begin
 FreeAndNil(g_Tfs_SituationSearch);
end;

class function Tfs_SituationSearch.Instance: Tfs_SituationSearch;
begin
 if (g_Tfs_SituationSearch = nil) then
 begin
  l3System.AddExitProc(Tfs_SituationSearchFree);
  g_Tfs_SituationSearch := Create;
 end;
 Result := g_Tfs_SituationSearch;
end;

var
    { Локализуемые строки SituationSearchCaptionLocalConstants }
   str_fsSituationSearchCaption : Tl3StringIDEx = (rS : -1; rLocalized : false; rKey : 'fsSituationSearchCaption'; rValue : 'ППС 6.х');
    { Заголовок фабрики сборки форм "SituationSearch" }

// start class Tfs_SituationSearch

function Tfs_SituationSearch.SaveLoadParentSlqtKWNeedMakeForm(const aDataSource: IvcmFormSetDataSource;
  out aNew: IvcmFormDataSource;
  aSubUserType: TvcmUserType): Boolean;
var
 l_UseCase : IsdsSituation;
begin
 if Supports(aDataSource, IsdsSituation, l_UseCase) then
  try
   //#UC START# *4D80CC1002D6NeedMake_impl*
   aNew := l_UseCase.dsSaveLoad;
   //#UC END# *4D80CC1002D6NeedMake_impl*
  finally
   l_UseCase := nil;
  end;//try..finally
 Result := (aNew <> nil);
end;//Tfs_SituationSearch.SaveLoadParentSlqtKWNeedMakeForm

function Tfs_SituationSearch.AttributeSelectParentUtSingleSearchNeedMakeForm(const aDataSource: IvcmFormSetDataSource;
  out aNew: IvcmFormDataSource;
  aSubUserType: TvcmUserType): Boolean;
var
 l_UseCase : IsdsSituation;
begin
 if Supports(aDataSource, IsdsSituation, l_UseCase) then
  try
   //#UC START# *4D80CC820003NeedMake_impl*
   aNew := l_UseCase.dsAttributeSelect;
   //#UC END# *4D80CC820003NeedMake_impl*
  finally
   l_UseCase := nil;
  end;//try..finally
 Result := (aNew <> nil);
end;//Tfs_SituationSearch.AttributeSelectParentUtSingleSearchNeedMakeForm

function Tfs_SituationSearch.TreeAttributeSelectParentAstNoneNeedMakeForm(const aDataSource: IvcmFormSetDataSource;
  out aNew: IvcmFormDataSource;
  aSubUserType: TvcmUserType): Boolean;
var
 l_UseCase : IsdsSituation;
begin
 if Supports(aDataSource, IsdsSituation, l_UseCase) then
  try
   //#UC START# *4D80CCDD0117NeedMake_impl*
   aNew := l_UseCase.dsTreeAttributeSelect;
   //#UC END# *4D80CCDD0117NeedMake_impl*
  finally
   l_UseCase := nil;
  end;//try..finally
 Result := (aNew <> nil);
end;//Tfs_SituationSearch.TreeAttributeSelectParentAstNoneNeedMakeForm

function Tfs_SituationSearch.SelectedAttributesChildUtSelectedAttributesNeedMakeForm(const aDataSource: IvcmFormSetDataSource;
  out aNew: IvcmFormDataSource;
  aSubUserType: TvcmUserType): Boolean;
var
 l_UseCase : IsdsSituation;
begin
 if Supports(aDataSource, IsdsSituation, l_UseCase) then
  try
   //#UC START# *4D80CCF40025NeedMake_impl*
   aNew := l_UseCase.dsSelectedAttributes;
   //#UC END# *4D80CCF40025NeedMake_impl*
  finally
   l_UseCase := nil;
  end;//try..finally
 Result := (aNew <> nil);
end;//Tfs_SituationSearch.SelectedAttributesChildUtSelectedAttributesNeedMakeForm

function Tfs_SituationSearch.FiltersNavigatorUtFiltersNeedMakeForm(const aDataSource: IvcmFormSetDataSource;
  out aNew: IvcmFormDataSource;
  aSubUserType: TvcmUserType): Boolean;
var
 l_UseCase : IsdsSituation;
begin
 if Supports(aDataSource, IsdsSituation, l_UseCase) then
  try
   //#UC START# *4D80CC20008BNeedMake_impl*
   aNew := l_UseCase.dsFilters;
   //#UC END# *4D80CC20008BNeedMake_impl*
  finally
   l_UseCase := nil;
  end;//try..finally
 Result := (aNew <> nil);
end;//Tfs_SituationSearch.FiltersNavigatorUtFiltersNeedMakeForm

function Tfs_SituationSearch.TreeAttributeFirstLevelNavigatorFlSituationNeedMakeForm(const aDataSource: IvcmFormSetDataSource;
  out aNew: IvcmFormDataSource;
  aSubUserType: TvcmUserType): Boolean;
var
 l_UseCase : IsdsSituation;
begin
 if Supports(aDataSource, IsdsSituation, l_UseCase) then
  try
   //#UC START# *4D80CC2E03BCNeedMake_impl*
   aNew := l_UseCase.dsTreeAttributeFirstLevel;
   //#UC END# *4D80CC2E03BCNeedMake_impl*
  finally
   l_UseCase := nil;
  end;//try..finally
 Result := (aNew <> nil);
end;//Tfs_SituationSearch.TreeAttributeFirstLevelNavigatorFlSituationNeedMakeForm

procedure Tfs_SituationSearch.InitFields;
 {-}
begin
 inherited;
 with AddZone(vcm_ztParent, fm_cfSaveLoad) do
 begin
  UserType := slqtKW;
  with AddZone(vcm_ztParent, fm_cfAttributeSelect) do
  begin
   UserType := utSingleSearch;
   with AddZone(vcm_ztParent, fm_efTreeAttributeSelect) do
   begin
    UserType := astNone;
    OnNeedMakeForm := TreeAttributeSelectParentAstNoneNeedMakeForm;
   end;
   with AddZone(vcm_ztChild, fm_enSelectedAttributes) do
   begin
    UserType := utSelectedAttributes;
    OnNeedMakeForm := SelectedAttributesChildUtSelectedAttributesNeedMakeForm;
   end;
   OnNeedMakeForm := AttributeSelectParentUtSingleSearchNeedMakeForm;
  end;
  OnNeedMakeForm := SaveLoadParentSlqtKWNeedMakeForm;
 end;
 with AddZone(vcm_ztNavigator, fm_enFilters) do
 begin
  UserType := utFilters;
  OnNeedMakeForm := FiltersNavigatorUtFiltersNeedMakeForm;
 end;
 with AddZone(vcm_ztNavigator, fm_efTreeAttributeFirstLevel) do
 begin
  UserType := flSituation;
  ActivateIfUpdate := wafAlways;
  OnNeedMakeForm := TreeAttributeFirstLevelNavigatorFlSituationNeedMakeForm;
 end;
 Caption := str_fsSituationSearchCaption.AsCStr;
end;//Tfs_SituationSearch.InitFields

class function Tfs_SituationSearch.GetInstance: TvcmFormSetFactoryPrim;
 {-}
begin
 Result := Self.Instance;
end;//Tfs_SituationSearch.GetInstance

initialization
 str_fsSituationSearchCaption.Init;

end.

Методы вида XXXNeedMakeForm служат для определения необходимости создания формы представления и передачи этому представлению его бизнес-объекта (контроллера).

Сразу оговорюсь, что использование Supports - это наследие "времён RAD", когда модель описывалась в дизайнере форм, а не на "чертежах".

Сейчас - есть чёткое понимание - как избежать данного Supports. Но поскольку этого пока не было сделано - привожу код как есть.

Запись вида:
AddZone(vcm_ztParent, fm_cfSaveLoad)
-- служит для связывания формы представления с конкретной зоной главного окна, в которое будет встраиваться реализация прецедента.

Регистрируются в конечном приложении они примерно так:

procedure TAppRes.RegisterFormSetFactories;
begin
 inherited;
 RegisterFormSetFactory(Tfs_CompareEditions);
 RegisterFormSetFactory(Tfs_InternetAgent);
 RegisterFormSetFactory(Tfs_Folders);
 RegisterFormSetFactory(Tfs_Autoreferat);
 RegisterFormSetFactory(Tfs_AutoreferatAfterSearch);
 RegisterFormSetFactory(Tfs_Document);
 RegisterFormSetFactory(Tfs_DocumentWithFlash);
 RegisterFormSetFactory(Tfs_List);
 RegisterFormSetFactory(Tfs_Diction);
 RegisterFormSetFactory(Tfs_Tips);
 RegisterFormSetFactory(Tfs_MedicDiction);
 RegisterFormSetFactory(Tfs_MedicFirmDocument);
 RegisterFormSetFactory(Tfs_DrugDocument);
 RegisterFormSetFactory(Tfs_DrugList);
 RegisterFormSetFactory(Tfs_MedicFirmList);
 RegisterFormSetFactory(Tfs_SendConsultation);
 RegisterFormSetFactory(Tfs_Consultation);
 RegisterFormSetFactory(Tfs_ViewChangedFragments);
 RegisterFormSetFactory(Tfs_SituationSearch);
 RegisterFormSetFactory(Tfs_SituationFilter);
 RegisterFormSetFactory(Tfs_AACContents);
 RegisterFormSetFactory(Tfs_AAC);
end;

-- понятное дело, что правильнее было сделать внедрение зависимостей (http://ru.wikipedia.org/wiki/%D0%92%D0%BD%D0%B5%D0%B4%D1%80%D0%B5%D0%BD%D0%B8%D0%B5_%D0%B7%D0%B0%D0%B2%D0%B8%D1%81%D0%B8%D0%BC%D0%BE%D1%81%D1%82%D0%B8). Но оно - пока сделано не было.

 Создаётся же экземпляр реализации прецедента таким образом:

class procedure TSearchModule.OpenSituationCard(const aQuery: IQuery);
var
 __WasEnter : Boolean;
//#UC START# *4F27EA7D0011_4AA641A3036C_var*
//#UC END# *4F27EA7D0011_4AA641A3036C_var*
begin
 __WasEnter := vcmEnterFactory;
 try
  //#UC START# *4F27EA7D0011_4AA641A3036C_impl*
   Tfs_SituationSearch.Make(TsdsSituation.Make(TdeSearch.Make(nsCStr(AT_KW), aQuery)));
  //#UC END# *4F27EA7D0011_4AA641A3036C_impl*
 finally
  if __WasEnter then
   vcmLeaveFactory;
 end;//try..finally
end;//TSearchModule.OpenSituationCard

Tfs_SituationSearch.Make - это фабричный метод, который создаёт экземпляр "реализации прецедента" и встраивает его в главную форму приложения, заменяя им предыдущую активную "реализацию прецедента".

При этом сохранение состояния всех составляющих предыдущей реализации прецедента в историю Back/Forward осуществляется "автоматически".

TsdsSituation.Make - фабричный метод, создающий бизнес-объект логики прецедента. Который является контроллером для бизнес-объектов форм.

aQuery - это источник данных из предметной области для инициализации данного прецедента. В данном случае это IQuery - т.е. пользовательский запрос. Например загруженный из папок.

В конечном итоге метод OpenSituationCard зовётся примерно так:
class procedure TAppRes.OpenQuery(aQueryType: TlgQueryType;
  const aQuery: IQuery);
//#UC START# *4AC4A69D03B7_4A925AFF01BA_var*
//#UC END# *4AC4A69D03B7_4A925AFF01BA_var*
begin
//#UC START# *4AC4A69D03B7_4A925AFF01BA_impl*
 case aQueryType of
  lg_qtKeyWord:
   TdmStdRes.OpenSituationCard(aQuery);
  lg_qtAttribute:
   TdmStdRes.AttributeSearch(aQuery, nil);
  lg_qtPublishedSource:
   TdmStdRes.PublishSourceSearch(aQuery, nil);
  lg_qtLegislationReview:
   TdmStdRes.OpenLegislationReview(aQuery);
  lg_qtSendConsultation:
   vcmDispatcher.ModuleOperation(TdmStdRes.mod_opcode_Search_OpenConsult);
  lg_qtBaseSearch:
   TdmStdRes.OpenBaseSearch(ns_bsokGlobal,
                            aQuery);
  lg_qtInpharmSearch:
   TdmStdRes.InpharmSearch(aQuery, nil);
  else
   inherited;   
 end;//case aQueryType
//#UC END# *4AC4A69D03B7_4A925AFF01BA_impl*
end;//TPrimNemesisRes.OpenQuery

А уж OpenQuery, зовётся из "клиентского кода" так:

procedure TPrimMainMenuNewForm.SearchClick(aSender: TObject);
//#UC START# *4ACB8B7B0192_4958E1F700C0_var*
//#UC END# *4ACB8B7B0192_4958E1F700C0_var*
begin
//#UC START# *4ACB8B7B0192_4958E1F700C0_impl*
 if (aSender = flAttributeSearch) then
  TdmStdRes.OpenQuery(lg_qtAttribute, nil)
 else
 if (aSender = flSitiationSearch) then
  TdmStdRes.OpenQuery(lg_qtKeyWord, nil)
 else
 if (aSender = flPublishedSourceSearch) then
  TdmStdRes.OpenQuery(lg_qtPublishedSource, nil)
 else
 if (aSender = flDictionSearch) then
  TdmStdRes.OpenDictionary(nil, NativeMainForm)
 else
  Assert(false);
//#UC END# *4ACB8B7B0192_4958E1F700C0_impl*
end;//TPrimMainMenuNewForm.SearchClick

-- из обработчика OnClick различных контролов.

Или так:

procedure TPrimWorkJournalForm.SavedQuery_Execute_Execute(const aParams: IvcmExecuteParamsPrim);
//#UC START# *4C3F342E02AF_4BD6D6EA0075exec_var*
var
 l_AdapterNode : INodeBase;
 l_BaseEntity : IUnknown;
//#UC END# *4C3F342E02AF_4BD6D6EA0075exec_var*
begin
//#UC START# *4C3F342E02AF_4BD6D6EA0075exec_impl*
 if Supports(JournalTree.TreeView.CurrentNode, INodeBase, l_AdapterNode) then
  try
   try
    l_AdapterNode.GetEntity(l_BaseEntity);
   except
    on ECanNotFindData do
     Exit; //TODO: нода "пропала" что делать?
   end;
   try
    TdmStdRes.OpenQuery(l_BaseEntity As IQuery);
   finally
    l_BaseEntity := nil;
   end;//try..finally
  finally
   l_AdapterNode := nil;
  end;//try..finally
//#UC END# *4C3F342E02AF_4BD6D6EA0075exec_impl*
end;//TPrimWorkJournalForm.SavedQuery_ExecuteQuery_Execute

-- из обработчика "операции" SavedQuery.Execute на одной из форм представления другого прецедента.

Не скрою. Выглядит вся эта "конструкция" - тяжеловесно. Особенно на ПЕРВЫЙ ВЗГЛЯД.

Но во-первых - всё это рождалось годами. В процессе множества дискуссий и недопониманий.

Во-вторых - очень долго нам мешало то, что мы пытались всё это делать "через призму RAD" и дизайнеров форм и компонентов. Пока не пришли к пониманию того, что мета-модель и "чертежи" - годятся для этого гораздо лучше.

В-третьих - было допущено множество архитектурных ошибок. Опять же продиктованных "периодом исканий". Какие-то ошибки были исправлены, какие-то - до сих пор существуют. Но зато есть понимание - как их исправлять. И какой должна быть "идеальная архитектура.

Зато ТЕПЕРЬ, при наличии мета-модели, "чертежей" и кодогенерации - мы реально можем можем собирать реализации конкретных пользовательских прецедентов и их логики из УЖЕ существующих "кирпичиков" на модели и "номенклатуры микросхем".

Процесс создания НОВЫХ прецедентов выглядит реально как "отвёрточная сборка". Разумеется до тех пор, пока готовые "кирпичики" удовлетворяют текущим потребностям. Новые "кирпичики" делать сложнее, но тоже - не "космически" сложно. И для этого тоже существует поддержка со стороны мета-модели.

Николай Зверев возражает/добавляет насчёт "FM и использование памяти"

"Это к посту про FM:

Я тоже может сейчас не в тему напишу. Но всё зависит от решаемых задач.

В наших проектах есть такая вот вещь - свои датасеты. В обычном режиме там память под каждую запись (row) выделяется в общей куче. Но есть режим пула - когда в один момент времени выделяется память... скажем для 10000 записей. А дальше записи берутся из пула (и возвращаются обратно).
И это даёт прирост в скорости. Но только в старых версиях Delphi со старым менеджером памяти. С FastMM прирост не вооружённым глазом не заметен даже на очень большом кол-ве записей (моё любимое кол-во записей для тестов - 6 миллионов).
Или вот ещё пример - VirtualTreeView. Там тоже есть режим пула для выделяемых нод. Картинка та же самая.
И даже если сложить выигранное время там плюс тут - то с секундомером пользователь может и заметит разницу, но с таким объёмом данных никто локально не работает.

Ну и ещё я как-то делал оптимизации динамических массивов - типа выделяем сразу большое кол-во элементов (типа Capacity) и отдельно храним кол-во актуальных элементов. По скорости - оно конечно быстрее, чем постоянно делать SetLength(array, Length(array) + 1), но на такой маленький процент... в общем я решил, что пусть код будет проще, чище и понятнее.

Другое дело, элементы графики, с которыми активно взаимодействует пользователь, тут оптимизация конечно нужна.

А если вспомнить Ваш текстовый процессор, то там наверняка одно слово - это отдельный объект. Слов в документе тысячи, документов тоже не мало, понятно, что тут игра тоже стоит свеч.

Ну и про FM моё мнение такое. Задумка отличная, архитектура продуманная, оптимизация хромает. Но на всё нужны ресурсы. Пусть пилят, мы подождём."

-- по делу всё написано. Мне так кажется.

Что такое QueryInterface и Supports с моей точки зрения

Что такое QueryInterface и Supports с моей точки зрения?

Это "своего рода" - "типизированный Duck-Typing". Когда от одного объекта - "неожиданно" рождается "другой".

Именно "рождается". Семантика метода - это предполагает.

Вещь - БЕЗУСЛОВНО полезная и гибкая.

И решает множество задач.

Но! Ставить во главу архитектурных решений подобный подход - я считаю ошибкой.

Кроме потери эффективности, на которую может быть и можно закрыть глаза, это ведёт к "косвенным зависимостям". Описанным "только" в документации.

Метод "секретного тут-тук".

Практические примеры - есть у меня перед глазами.

Далеко ходить не надо:

Supports(BorlandIDEServices, IOTAModuleServices, Result);

  IBorlandIDEServices = interface(IUnknown)
    ['{7FD1CE92-E053-11D1-AB0B-00C04FB16FB3}']
  end;

 (* The BorlandIDEServices global variable is initialized by the Delphi or
    C++Builder IDE.  From this interface all of the IxxxxServices interfaces
    may be queried for.  For example, in order to obtain the IOTAModuleServices
    interface, simply call the QueryInterface method with the interface
    identifier or the GUID for the IOTAModuleServices interface.  In Delphi, you
    could also use the "as" operator, however not all versions of the IDEs will
    support all the "services" interfaces.  IOTATodoServices is only supported
    in the Professional and Enterprise versions of the products.

    In Delphi;
      var
        ModuleServices: IOTAModuleServices;

      ...

      if Supports(BorlandIDEServices, IOTAModuleServices, ModuleServices) then
      begin
        ...
      end;

    or in C++Builder;

      IOTAModuleServices *ModuleServices;
      if (Supports(BorlandIDEServices, __uuidof(IOTAModuleServices), &ModuleServices))
      {
        ...
      }
  *)
-- ну НЕ дружелюбен такой подход к использующим его.

Почему нельзя ЯВНО возвращать те или иные интерфейсы, через ЯВНЫЕ методы AsIxxxxServices - лично я - НЕ ПОНИМАЮ.

И к сожалению примеров ТАКОЙ архитектуры - масса.

И главное, что разработчики "срисовывают" друг у друга подобные подходы. Зачем-то.

Я сам - "срисовывал". Пока не убедился в "порочности" подобной архитектуры.

Пример из Борланда приведён не потому, что у меня "претензии" именно к Борланду, а потому, что он реален и общеизвестен. Чтобы меня не упрекнули в "очередном велосипеде".

И как раз-таки "почему так Борланд" сделал - я "понимаю".

Комментариев и мнений - не жду. Я написал СВОЁ МНЕНИЕ. Подтверждённое практикой.

"Теория Маркса всесильна потому, что она верна".

воскресенье, 3 ноября 2013 г.

Кодогенерация из Visual Paradigm в Delphi

Диаграмма:

Дала код:
Unit untitled;
 Interface
  Uses
   SysUtils;
  Type
   Class = Class;
   Class2 = Class;
   Class3 = Class;

   Class = Class(TObject)
   End;

   Class2 = Class(untitled.Class)
   End;

   Class3 = Class(untitled.Class2)
   End;

 Implementation



End.
Ну в общем - "на вкус и цвет". Первое впечатление - "всё не очень промышленно". Не для КОНВЕЙЕРА. И не для постоянной кодогенерации.

Шаблоны кодогенерации в Visual Paradigm

Кодогенерация класса выглядит как-то так:
$class.t_prepare($args.get("property"))##
## ===== Global =====
#if( $indentationLevel == "0" )
 #set( $indentationLevel = 0 )
#end
#set( $baseIndentation = $utilities.getIndentation($indentation, $indentationLevel) )
## ===== Output =====
#if( $clsMode == "INTERFACE" )
 $class.t_getDocumentation($baseIndentation)##
 ${baseIndentation}$class.getName() = $class.t_getInterface()##
 #if (!$class.isInterface())
  ($class.t_getGeneralizationRealization())
 #end
 #if( $class.containmentClassCount() > 0 )
  ${baseIndentation}${indentation}Type
  #foreach( $class in $class.containmentClassIterator() )
   #set( $indentationLevel = $indentationLevel + 2 )
   #parse("$template-dir/DelphiClass.vm")

   #set( $indentationLevel = $indentationLevel - 2 )
  #end
 #end
 #set( $baseIndentation = $utilities.getIndentation($indentation, $indentationLevel) )
 #if( $class.attributeCount() > 0 )
  #foreach( $attribute in $class.attributeIterator() )
   #if( $class.isInterface() == false || $attribute.t_getScope().equals("static ") )
    #parse("$template-dir/DelphiAttribute.vm")

   #end
  #end
 #end
#else
 #set( $baseIndentation = "$indentation" )
 #if( $class.containmentClassCount() > 0 )
  #foreach( $class in $class.containmentClassIterator() )
   #parse("$template-dir/DelphiClass.vm")

  #end
 #end
#end
#if( $class.operationCount() > 0 )
 #foreach( $operation in $class.operationIterator() )
  #if( $clsMode == "INTERFACE" || !$operation.isAbstract() )
   #parse("$template-dir/DelphiOperation.vm")
  #end
 #end
#end
#if( $clsMode == "INTERFACE" )
 ${baseIndentation}End;
#end

Типа - "не криптованно". Но что-то по первоначалу - не впечатляет.

Профилировщик, которым я пользуюсь

суббота, 2 ноября 2013 г.

Написал бы я, что мне в этом коде не нравится, но ведь всё равно дураком назовут

procedure TStyledWindowBorder.MouseMove(Shift: TShiftState; X, Y: Single);
var
  P: TPointF;
  Obj: IControl;
  SG: ISizeGrip;
  NewCursor: TCursor;
  CursorService: IFMXCursorService;
begin
  NewCursor := crDefault;
  TPlatformServices.Current.SupportsPlatformService(IFMXCursorService, IInterface(CursorService));
  FMousePos := PointF(X, Y);
  if Assigned(FCaptured) then
  begin
    if Assigned(CursorService) then
    begin
      if ((FCaptured.QueryInterface(ISizeGrip, SG) = 0) and Assigned(SG)) then
        CursorService.SetCursor(crSizeNWSE)
      else
        CursorService.SetCursor(FCaptured.Cursor);
    end;
    P := FCaptured.ScreenToLocal(PointF(FMousePos.X, FMousePos.Y));
    FCaptured.MouseMove(Shift, P.X, P.Y);
    Exit;
  end;
 
  Obj := ObjectAtPoint(FMousePos);
  if Assigned(Obj) then
  begin
    SetHovered(Obj);
    P := Obj.ScreenToLocal(PointF(FMousePos.X, FMousePos.Y));
    Obj.MouseMove(Shift, P.X, P.Y);
    if ((Obj.QueryInterface(ISizeGrip, SG) = 0) and Assigned(SG)) then
      NewCursor := crSizeNWSE
    else
      NewCursor := Obj.Cursor;
  end
  else
    SetHovered(nil);
  // set cursor
  if Assigned(CursorService) then
    CursorService.SetCursor(NewCursor);
  FDownPos := FMousePos;
end;
....
Посему - предлагаю просто получить эстетическое удовольствие.

Ещё про QueryInterface

Про меня - все всё поняли.

http://18delphi.blogspot.ru/2013/11/supports.html?showComment=1383353862701#c88813335274335154
"Странные вещи пишите Александр... Очень странные...
<-- Документация System.SysUtils.Supports
Indicates whether a given object or interface supports a specified interface. 

Call Supports to determine whether the object or interface specified by Instance, or the class specified by AClass, supports the interface identified by the IID parameter. If Instance supports the interface, Supports returns the interface as the Intf parameter and returns True. If AClass supports the interface, Supports does not return an interface, but still returns True. If the interface specified by IID is not supported, Supports returns False. 
--> 
Обратите внимание на упоминание в документации слов objects, class, instance.
Из документации явно следует, что SysUtils.Supports предназначена (задумывалась авторами) для проверки, поддерживает ли данный класс или объект (экземпляр класса) данный интерфейс. Иными словами: умеет ли *данный объект* «делать это»? Обратите внимание — объект, а не кто-то, кто как-то связан с этим объектом или кого можно получить, зная объект."

Ну меня дурака - положим "умный дядя научил".

Но вот незадача.

Других-то - не научил:

function TCustomForm.QueryInterface(const IID: TGUID; out Obj): HResult;
begin
  // Route the QueryInterface throught the DesignerHook first
  if (DesignerHook = nil) or (DesignerHook.QueryInterface(IID, Obj) <> 0) then
    Result := inherited QueryInterface(IID, Obj)
  else
    Result := 0;
end;
...
function TCorbaImplementation.QueryInterface(const IID: TGUID;
  out Obj): HResult;
begin
  if Assigned(FController) then
    Result := IObject(FController).QueryInterface(IID, Obj) else
    Result := ObjQueryInterface(IID, Obj);
end;
...
function TComponent.QueryInterface(const IID: TGUID; out Obj): HResult;
begin

  if FVCLComObject = nil then
  begin

    if GetInterface(IID, Obj) then Result := S_OK
    else Result := E_NOINTERFACE

  end
  else
    Result := IVCLComObject(FVCLComObject).QueryInterface(IID, Obj);

end;
....
function TCustomWebAppDataModule.QueryInterface(const IID: TGUID;
  out Obj): HResult;
begin
  if IsEqualGuid(IID, IGetWebAppComponents) then
    if Supports(AppServices, IGetWebAppComponents, Obj) then
    begin
      Result := S_OK;
      exit;
    end;
  Result := inherited QueryInterface(IID, Obj);
end;
...
function TCustomWebAppPageModule.QueryInterface(const IID: TGUID; out Obj): HResult;
begin
  if IsEqualGuid(IID, IGetWebAppComponents) then
    if Supports(AppServices, IGetWebAppComponents, Obj) then
    begin
      Result := S_OK;
      exit;
    end;
  Result := inherited QueryInterface(IID, Obj);
end;
...
function TXPInterfacedObject.QueryInterface(const IID: TGUID; out Obj): HResult;
begin

  if (FDelegator = nil) or FIntrospective then
    Result := inherited QueryInterface(IID, Obj)
  else
    Result := IInterface(FDelegator).QueryInterface(IID, Obj);

end;
....
function TNestedScope.QueryInterface(const IID: TGUID; out Obj): HResult;
begin
  Result := E_NOINTERFACE;
  if GetInterface(IID, Obj) then
    Result := S_OK
  else
    if Supports(Inner, IID, Obj) or Supports(Outer, IID, Obj) then
      Result := S_OK;
end;
....
function TVirtualObjectMemberInstance.QueryInterface(const IID: TGUID;
  out Obj): HResult;
begin
  // depending on the custom wrapper type, it returns the appropriate interfaces
  // that the virtual wrapper is supposed to support
  if ((IID = IValue) or (IID = IInvokable) or (IID = IArguments)) and
     (not Supports(GetCustomWrapper, IID)) then
    Result := E_NOINTERFACE
  else
    Result := inherited QueryInterface(IID, Obj);
end;
....
procedure TStyledWindowBorder.MouseMove(Shift: TShiftState; X, Y: Single);
var
  P: TPointF;
  Obj: IControl;
  SG: ISizeGrip;
  NewCursor: TCursor;
  CursorService: IFMXCursorService;
begin
  NewCursor := crDefault;
  TPlatformServices.Current.SupportsPlatformService(IFMXCursorService, IInterface(CursorService));
  FMousePos := PointF(X, Y);
  if Assigned(FCaptured) then
  begin
    if Assigned(CursorService) then
    begin
      if ((FCaptured.QueryInterface(ISizeGrip, SG) = 0) and Assigned(SG)) then
        CursorService.SetCursor(crSizeNWSE)
      else
        CursorService.SetCursor(FCaptured.Cursor);
    end;
    P := FCaptured.ScreenToLocal(PointF(FMousePos.X, FMousePos.Y));
    FCaptured.MouseMove(Shift, P.X, P.Y);
    Exit;
  end;

  Obj := ObjectAtPoint(FMousePos);
  if Assigned(Obj) then
  begin
    SetHovered(Obj);
    P := Obj.ScreenToLocal(PointF(FMousePos.X, FMousePos.Y));
    Obj.MouseMove(Shift, P.X, P.Y);
    if ((Obj.QueryInterface(ISizeGrip, SG) = 0) and Assigned(SG)) then
      NewCursor := crSizeNWSE
    else
      NewCursor := Obj.Cursor;
  end
  else
    SetHovered(nil);
  // set cursor
  if Assigned(CursorService) then
    CursorService.SetCursor(NewCursor);
  FDownPos := FMousePos;
end;
....
function TCommonCustomForm.QueryInterface(const IID: TGUID; out Obj): HResult;
begin
  // Route the QueryInterface through the Designer first
  if not Assigned(Designer) or (Designer.QueryInterface(IID, Obj) <> 0) then
    Result := inherited QueryInterface(IID, Obj)
  else
    Result := 0;
end;
....
function TServerEventDispatch.QueryInterface(const IID: TGUID; out Obj): HResult;
begin
  if GetInterface(IID, Obj) then
  begin
    Result := S_OK;
    Exit;
  end;
  if IsEqualIID(IID, FServer.FServerData^.EventIID) then
  begin
    GetInterface(IDispatch, Obj);
    Result := S_OK;
    Exit;
  end;
  Result := E_NOINTERFACE;
end;
....

У меня наверное с русским языком что-то не то...

http://18delphi.blogspot.ru/2013/11/blog-post_4367.html?showComment=1383369874239#c5819572482489216617

"Преждевременные оптимизации здесь - зло."

ПРЕЖДЕВРЕМЕННЫЕ оптимизации - ВООБЩЕ ЗЛО.

Зло - АБСОЛЮТНОЕ.

Но разве я об этом спрашивал?