Предыдущая серия была тут - http://18delphi.blogspot.ru/2013/11/5.html
И тут - http://18delphi.blogspot.ru/2013/11/gui-back-to-basics.html
И вот тут - http://18delphi.blogspot.ru/2013/11/gui.html
И вот тут - http://18delphi.blogspot.ru/2013/11/gui.html
Мне тут написали, что мол "вы пишете в формате лекции, а хотелось бы в формате семинара".
Попробую в формате семинара.
Итак.
Тезисы:
1. тест должен быть линейным
2. похожим на тест-кейс
3. читаться человеком
4. оперировать терминами предметной области
(к этим тезисам мы ещё не раз будем возвращаться)
Частично эти тезисы затронуты вот тут - http://18delphi.blogspot.ru/2013/11/blog-post_19.html
Попробую теперь сделать законченный пример в "классическом RAD-стиле".
Ну и в некотором смысле "в стиле XP" - http://18delphi.blogspot.ru/2013/04/blog-post.html
Там где что-то непонятно, что делать - буду писать Assert.
Пример доступен тут - https://sourceforge.net/p/rumtmarc/code-0/HEAD/tree/trunk/Blogger/GUITests/Chapter0/
Сделаем форму TForm1 с кнопками Button1, Button2, Button3 и строкой ввода Edit1.
Сделаем это для пущей "рекламы" и "новизны подхода" на FM.
Код формы приведён тут - http://18delphi.blogspot.ru/2013/11/gui-back-to-basics.html
Повторять его - не вижу смысла.
Весь код примера доступен тут - https://sourceforge.net/p/rumtmarc/code-0/HEAD/tree/trunk/Blogger/GUITests/Chapter1/
Также по-максимуму постараемся использовать обобщения (generics) и интерфейсы, а также TInterfacedObject.
У меня есть свои контейнеры вот - http://18delphi.blogspot.ru/2013/07/blog-post_3683.html и вот - http://18delphi.blogspot.ru/2013/07/2.html и вот - http://18delphi.blogspot.ru/2013/07/blog-post_8789.html
А также у меня есть своё виденье на "подсчёт ссылок" - вот - http://18delphi.blogspot.ru/2013/04/iunknown.html и вот - http://18delphi.blogspot.ru/2013/09/arc.html.
Но я оставлю эти темы за рамками данной статьи, дабы на "расплываться мыслью по древу". Пытливые читатели - могут сделать выводы из приведённых ссылок - самостоятельно.
Я же, повторюсь - постараюсь обойтись стандартными средствами языка и библиотек.
А теперь "поучимся" нажимать кнопки на этой форме.
Но не просто "нажимать", а "средствами скриптовой машины".
Введём понятия:
1. Тестовый сценарий (script, скрипт) - это код на "псевдо-языке", который нацелен на то, чтобы описывать действия "максимально приближенные к действиям пользователя". Такие как "нажатия на кнопки", "ввод с клавиатуры" и "управление мышью".
2. Скриптовая машина (TscriptEngine) - это программная сущность (класс), которая умеет исполнять скрипты.
3. Контекст выполнения (TscriptContext) - это программная сущность, которая обеспечивает контекст (опять не обошлось без тавтологии) выполнения скриптов и их составляющих. Это некий аналог стека значений.
4. Слово скрипта (IscriptWord) - это "кирпичик", составляющий тело скрипта. Обладает среди прочего одним из самых важных методов - DoIt. Это собственно метод, выполняющий код слова. (Слова бывают "периода компиляции" и "периода исполнения", тут я забежал вперёд, если интересно читайте тут - http://ru.wikipedia.org/wiki/Forth).
5. Код скрипта (TscriptCode) - скомпилированный код скрипта состоящий из IscriptWord. Который последовательно вызывает методы IscriptWord.DoIt.
6. Словарь скриптовой машины (TscriptDictionary) - это программная сущность в которой регистрируются слова скрипта (IscriptWord). Словарь это важный момент в компиляции скрипта все "токены" входного потока (о которых было написано тут - http://18delphi.blogspot.ru/2013/11/5.html) сопоставляются со словами в словаре. (Строить этот класс мы будем на "родном" generic-классе TDictionary от Embarcadero, хотя у меня к нему и есть вопросы и "настоящая" скриптовая машина построена на самописных абстрактных котейнерах)
7. Аксиоматика скриптовой машины (TscriptAxiomatiсs) - разновидность словаря скриптовой машины (TscriptDictionary). Это "базовый словарь" в котором определены "базовые термины" скриптовой машины (аксиоматика) на стороне Delphi. Если кому интересно - аксиоматика представляется singleton'ом (http://ru.wikipedia.org/wiki/Singleton). Все остальные словари - вложенные и строятся по мере компиляции кода скрипта (но это я опять забегаю вперёд).
8. Лог выполнения (IscriptLog) - это "нечто", скажем так "консоль", куда и скриптовая машина и слова скриптовой машины - могут выводить всё что они думают о процессе компиляции и выполнения. Лог выполнения служит для целей отладки.
9. На самом деле есть два лога - IscriptCompileLog и IscriptRunLog. Это лог компиляции и лог выполнения. По "ходу пьесы" мы увидим их оба.
10. Парсер входного потока (TscriptParser) - машина для разбора входного потока скрипта с целью дробления этого входного потока на токены. О ней вкратце было написано тут - http://18delphi.blogspot.ru/2013/11/5.html.
11. Стек значений. Это основной механизм передачи значений в нашей скриптовой машине. Он содержит значения TscriptValue. И связан с контекстами компиляции и выполнения. В любой момент слово скриптовой машины может положить в стек какое-то значение или прочитать его с верхушки стека.
7. Аксиоматика скриптовой машины (TscriptAxiomatiсs) - разновидность словаря скриптовой машины (TscriptDictionary). Это "базовый словарь" в котором определены "базовые термины" скриптовой машины (аксиоматика) на стороне Delphi. Если кому интересно - аксиоматика представляется singleton'ом (http://ru.wikipedia.org/wiki/Singleton). Все остальные словари - вложенные и строятся по мере компиляции кода скрипта (но это я опять забегаю вперёд).
8. Лог выполнения (IscriptLog) - это "нечто", скажем так "консоль", куда и скриптовая машина и слова скриптовой машины - могут выводить всё что они думают о процессе компиляции и выполнения. Лог выполнения служит для целей отладки.
9. На самом деле есть два лога - IscriptCompileLog и IscriptRunLog. Это лог компиляции и лог выполнения. По "ходу пьесы" мы увидим их оба.
10. Парсер входного потока (TscriptParser) - машина для разбора входного потока скрипта с целью дробления этого входного потока на токены. О ней вкратце было написано тут - http://18delphi.blogspot.ru/2013/11/5.html.
11. Стек значений. Это основной механизм передачи значений в нашей скриптовой машине. Он содержит значения TscriptValue. И связан с контекстами компиляции и выполнения. В любой момент слово скриптовой машины может положить в стек какое-то значение или прочитать его с верхушки стека.
Отмечу тот факт, что наша скриптовая машина работает как "компилятор". Сначала она строит код скрипта, проверяя его на валидность, а потом только - выполняет.
Код скомпилированный единожды - может исполняться сколько угодно раз.
Давайте для НАЧАЛА опишем фейковую скриптовую машину, которая просто из токенов входного потока скомпилирует код, который будет выводить в лог названия этих токенов.
Давайте для НАЧАЛА опишем фейковую скриптовую машину, которая просто из токенов входного потока скомпилирует код, который будет выводить в лог названия этих токенов.
Итак код введённых выше понятий:
Для начала TscriptCode.
Он - БОЛЕЕ чем прост. Благодаря наличию обобщений (generics):
Далее:
Код скриптовой машины:
Код базового слова:
И код "фейкового" слова:
Вуаля! Мы получили код скрипта, который компилируется и выполняется. И где каждый токен выводит своё имя в лог.
Далее мы продолжим с "нажатиями на кнопки".
(Пока писал всё это - задумался - сразу ли писать про внедрение зависимостей (http://ru.wikipedia.org/wiki/Dependency_Injection) и фабрики интерфейсов (http://ru.wikipedia.org/wiki/%D0%90%D0%B1%D1%81%D1%82%D1%80%D0%B0%D0%BA%D1%82%D0%BD%D0%B0%D1%8F_%D1%84%D0%B0%D0%B1%D1%80%D0%B8%D0%BA%D0%B0_(%D1%88%D0%B0%D0%B1%D0%BB%D0%BE%D0%BD_%D0%BF%D1%80%D0%BE%D0%B5%D0%BA%D1%82%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D1%8F))? Эту технику можно применить уже в нескольких местах. Или оставить "для следующих статей"? Подумаю ещё пока буду писать.)
Теперь давайте разделим компиляцию кода и его запуск.
А также разделим лог компиляции и лог запуска:
Введём новые интерфейсы - IscriptCompileLog и IscriptRunLog:
Введём новый класс - TscriptRunContext:
И видоизменим скриптовую машину:
Видно, что процесс компиляции "стоит отдельно", а "процесс выполнения" - отдельно.
При этом компиляция логируется отдельно, а выполнение логируется отдельно.
Следующим образом:
- тут видно, что форма TForm1 стала реализовывать два интерфейса - IscriptCompileLog и IscriptRunLog.
Причём - разными методами.
И лог компиляции выводится в компонент CompileLog, а лог запуска в компонент RunLog.
Теперь давайте реально разделим компиляцию и запуск.
Выделим из метода RunScript метод CompileScript, который будет возвращать скомпилированный код, а также интерфейс IscriptCode, который собственно и представляет скомпилированный код.
А также введём интерфейс IscriptCompiler - компилятор кода скрипта.
(Ещё ремарка - во всех интерфейсах, что я описал - нету GUID. Это сделано преднамеренно. Причины описаны вот тут - http://18delphi.blogspot.ru/2013/11/supports.html и тут - http://18delphi.blogspot.ru/2013/11/queryinterface-supports.html и тут - http://18delphi.blogspot.ru/2013/10/supports.html
Когда нам РЕАЛЬНО понадобятся GUID'ы - мы их конечно же введём)
Вот что получается:
(Самые пытливые читатели могут посмотреть лог коммитов в SVN. Он сам по себе - интересен. Я ОБЫЧНО смотрю чужие логи, чтобы "следить за мыслью автора")
Интерфейс IscriptCode и IscriptCompiler тут:
(Ещё одна ремарка - "смешение объектов и интерфейсов" - не должно пугать вас. Это - "не страшно". Ибо "интерфейсы" - внутренние. Они не пересекают "границы сред. Они служат лишь одному - подсчёту ссылок. Это можно было бы сделать и по-другому вот пример - http://18delphi.blogspot.ru/2013/04/iunknown.html и вот - http://18delphi.blogspot.ru/2013/03/blog-post_4606.html. Если пытливый читатель хочет "подискутировать" насчёт "объектов и интерфейсов" и "чистоты арийской расы" - пусть изучит приведённый ссылки и потом только "пишет письма" . У меня был ХОРОШИЙ преподаватель - Евсеев Олег (отчество - забыл) - он обычно говорил - "Хороший вопрос" Вы Люлин останьтесь - после семинара - обсудим")
Реализация интерфейсов IscriptCode и IscriptCompiler тут:
(ох как чешутся руки про внедрение зависимостей рассказать...)
Теперь использование интерфейсов IscriptCode и IscriptCompiler и опять видоизменённая скриптовая машина:
Вот как-то так пока.
(И главное - мы совсем забыли про тесты описанные тут - http://18delphi.blogspot.com/2013/11/5.html. Они ВСЕГДА проходят. TDD (http://ru.wikipedia.org/wiki/%D0%A0%D0%B0%D0%B7%D1%80%D0%B0%D0%B1%D0%BE%D1%82%D0%BA%D0%B0_%D1%87%D0%B5%D1%80%D0%B5%D0%B7_%D1%82%D0%B5%D1%81%D1%82%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5) - рулит)
(Семинар не очень длинный получается? Не на "две пары"?)
Теперь давайте отдельно скомпилируем токены (script_ttToken) и строки (script_ttString).
Появляется новый класс - TscriptUnknownToken. Он похож на TscriptStringWord, но я специально не обобщаю их код:
И вот так видоизменяется метод TscriptEngine.CompileScript:
Теперь подумаем - "а какие же токены", не являются Unknown?
Разберём регистрацию слов в словаре.
Посмотрим на реализацию класса словаря - TscriptDictionary. Он опять же - более, чем прост.
Благодаря всё тем же обобщениям:
Теперь посмотрим на "аксиоматику" - TscriptAxiomatiсs.
На данный момент унаследуем этот класс от TscriptDictionary и сделаем "одиночкой" (http://ru.wikipedia.org/wiki/Singleton).
Вот наш новый класс:
Эта реализация небезупречна и не потокобезопасна, но цель статьи не в том, чтобы рассказать о том как правильно делать "одиночек.
Код скриптовой машины видоизменяется так:
Теперь используем этот класс (TscriptAxiomatics) для регистрации хотя бы одного слова аксиоматики:
На самом деле мы зарегистрировали два слова DoNothing и DoNothing2, связав их с классами TscriptWordExample1 и TscriptWordExample2 соответственно.
Запускаем наш пример и видим, что вывод в лог - изменился.
Теперь отвлечёмся от "наращивания функционала" и немного порефакторим.
Выделим из метода TscriptEngine.CompileScript - фабричный метод (http://18delphi.blogspot.ru/2013/04/blog-post_7483.html) TscriptEngine.CompileToken.
Предварительно введём интерфейс IscriptParser и фабричный метод TscriptParser.Make. А также свяжем контекст компиляции TscriptCompileContext с парсером IscriptParser и компилятором IscriptCompiler.
Вот что получается.
Вводим интерфейс парсера:
Связываем парсер и компилятор с контекстом:
И вот реализация TscriptParser.Make:
- тут есть одна тонкость.
Для того, чтобы реализовать интерфейс IscriptParser - мы не стали "ломать наследование" и менять структуру класса TscriptParser, а ввели дополнительный скрытый класс TscriptParserContainer, который агрегирует в себя TscriptParser и реализует интерфейс IscriptParser, пользуясь функционалом агрегированного класса TscriptParser. Эта техника сродни шаблону адаптер (http://ru.wikipedia.org/wiki/Adapter) или шаблону фасад (http://18delphi.blogspot.ru/2013/11/gui-back-to-basics_22.html#more).
Итак. Класс TscriptEngine опять видоизменяется:
Теперь вернёмся к тому "ради чего всё делалось". Научимся "программно нажимать на кнопки".
Тут мы добрались до стека значений.
Посмотрим как он выглядит:
- тут мы опять использовали обобщения из стандартной библиотеки.
Теперь на основе всех полученных знаний введём два слова аксиоматики - TkwFindComponent и TkwButtonClick.
Первое слово ищет на текущей форме приложения компонент с заданным именем и кладёт его в стек.
Второе слово выбирает из стека значение объекта и трактует его как кнопку и пытается нажать указанную кнопку.
Вот эти слова:
Ремарка. Тип TControlAccess - служит для доступа к protected методу Click класса TControl. Это известный "трюк". Сами Borland и Embarcadero этим частенько пользуются. Позже мы рассмотрим "новый" RTTI. Там нам подобные "трюки" - не понадобятся.
Итак.
Всё готово для программного нажатия на кнопки из скрипта.
Код скрипта такой:
или такой:
Попробуйте их оба. Может быть вам понравится.
Заключение.
Итак. На достаточно простом примере "нажатия кнопки на форме" мы разобрали в общем не самые простые вещи.
1. Мы описали скриптовую машину.
2. Описали процесс компиляции скриптов.
3. Описали регистрацию слов в словаре аксиоматики.
4. Разобрали процесс запуска скриптов.
5. Описали стек значений и посмотрели на примеры его использования.
Ну и "на поверхности" мы добились поставленной цели - научились "программно нажимать кнопки приложения из скриптов".
Надеюсь, что понятна одна из основных идей - уже описанная скриптовая машина может расширяться путём регистрации новых слов в аксиоматике на стороне Delphi.
На этом данную статью я заканчиваю.
Если будет интерес у читателей - продолжу тему скриптовой машины и того "как она устроена".
Там вообще говоря можно многое обсудить:
1. Условные операторы.
2. Циклы.
3. Переменные.
4. Определение новых слов пользователем из кода скрипта.
5. Включение в код скрипта кода из других файлов.
6. И многое другое.
P.S. ДАВНО я не набивал СТОЛЬКО кода ВРУЧНУЮ.
Я давно уже "рисую квадраты на модели". Это - СИЛЬНО быстрее...
P.P.S. Повторюсь. Delphi - это всего лишь язык, на котором написано "это". Но "это" может быть написано на любом языке. Хоть Objective-C, хоть Python, хоть C++, хоть классический C, хоть классический Паскаль (в стиле Turbo Professional). Разница только в "запятых".
Главное это - суть ПОДХОДА.
P.P.P.S. Есть ещё одна "хотелка" - нарисовать GUITestRunner для DUnit для проектов на базе FM. Я постараюсь её реализовать.
P.P.P.P.S. Ну и анонс. Вот тут лежит новая версия примера - https://sourceforge.net/p/rumtmarc/code-0/HEAD/tree/trunk/Blogger/GUITests/Chapter2/ Там же доступен пример и VCL-проекта. Там же есть пример и для мобильных устройств, но я его пока запускал только под эмулятором.
Для начала TscriptCode.
Он - БОЛЕЕ чем прост. Благодаря наличию обобщений (generics):
unit Script.Code;
interface
uses
System.Generics.Collections,
Script.WordsInterfaces
;
type
TscriptCode = class(TList<IscriptWord>)
public
procedure Run(aContext : TscriptContext);
{* - выполняет компилированный код. }
procedure CompileWord(const aWord: IscriptWord);
{* - компилирует указанное слово в код. }
end;//TscriptCode
implementation
procedure TscriptCode.Run(aContext : TscriptContext);
var
l_Word : IscriptWord;
begin
for l_Word in Self do
l_Word.DoIt(aContext);
end;
procedure TscriptCode.CompileWord(const aWord: IscriptWord);
{* - компилирует указанное слово в код. }
begin
Self.Add(aWord);
end;
end.
Далее:
Код скриптовой машины:
unit Script.Engine;
interface
uses
Script.Interfaces
;
type
TscriptEngine = class
public
class procedure RunScript(const aFileName: String; const aLog: IscriptLog);
end;//TscriptEngine
implementation
uses
System.SysUtils,
Script.Parser,
Testing.Engine,
Script.Code,
Script.WordsInterfaces,
Script.StringWord
;
class procedure TscriptEngine.RunScript(const aFileName: String; const aLog: IscriptLog);
var
l_Parser : TscriptParser;
l_Context : TscriptCompileContext;
l_Code : TscriptCode;
l_StringWord : IscriptWord;
begin
TtestEngine.StartTest(aFileName);
try
l_Code := TscriptCode.Create;
try
l_Context := TscriptCompileContext.Create(aLog);
try
l_Parser := TscriptParser.Create(aFileName);
try
while not l_Parser.EOF do
begin
l_Parser.NextToken;
// if (aLog <> nil) then
// aLog.Log(l_Parser.TokenString);
l_StringWord := TscriptStringWord.Make(l_Parser.TokenString);
try
l_Code.CompileWord(l_StringWord);
finally
l_StringWord := nil;
end;//try..finally
end;//while not l_Parser.EOF
finally
FreeAndNil(l_Parser);
end;//try..finally
l_Code.Run(l_Context);
// - выполняем скомпилированный код
finally
FreeAndNil(l_Context);
end;//try..finally
finally
FreeAndNil(l_Code);
end;//try..finally
finally
TtestEngine.StopTest;
end;//try..finally
end;
end.
Код базового слова:
unit Script.Word;
interface
uses
Script.WordsInterfaces
;
type
TscriptWord = class(TinterfacedObject, IscriptWord)
protected
procedure DoIt(aContext: TscriptContext); virtual; abstract;
{* - собственно процедура для выполнения слова словаря. }
protected
procedure Cleanup; virtual;
public
class function Make: IscriptWord;
{* - фабрика }
destructor Destroy; override;
end;//TscriptWord
RscriptWord = class of TscriptWord;
implementation
class function TscriptWord.Make: IscriptWord;
{* - фабрика }
begin
Result := Create;
end;
destructor TscriptWord.Destroy;
begin
Cleanup;
inherited;
end;
procedure TscriptWord.Cleanup;
begin
// - тут ничего не делаем, потомки всё сделают
end;
end.
И код "фейкового" слова:
unit Script.StringWord; interface uses Script.WordsInterfaces, Script.Word ; type TscriptStringWord = class(TscriptWord) private f_String : String; protected procedure DoIt(aContext: TscriptContext); override; public constructor Create(const aString: String); class function Make(const aString: String): IscriptWord; end;//TscriptStringWord implementation constructor TscriptStringWord.Create(const aString: String); begin inherited Create; f_String := aString; end; class function TscriptStringWord.Make(const aString: String): IscriptWord; begin Result := Create(aString); end; procedure TscriptStringWord.DoIt(aContext: TscriptContext); begin aContext.Log(Self.f_String); end; end.
Вуаля! Мы получили код скрипта, который компилируется и выполняется. И где каждый токен выводит своё имя в лог.
Далее мы продолжим с "нажатиями на кнопки".
(Пока писал всё это - задумался - сразу ли писать про внедрение зависимостей (http://ru.wikipedia.org/wiki/Dependency_Injection) и фабрики интерфейсов (http://ru.wikipedia.org/wiki/%D0%90%D0%B1%D1%81%D1%82%D1%80%D0%B0%D0%BA%D1%82%D0%BD%D0%B0%D1%8F_%D1%84%D0%B0%D0%B1%D1%80%D0%B8%D0%BA%D0%B0_(%D1%88%D0%B0%D0%B1%D0%BB%D0%BE%D0%BD_%D0%BF%D1%80%D0%BE%D0%B5%D0%BA%D1%82%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D1%8F))? Эту технику можно применить уже в нескольких местах. Или оставить "для следующих статей"? Подумаю ещё пока буду писать.)
Теперь давайте разделим компиляцию кода и его запуск.
А также разделим лог компиляции и лог запуска:
Введём новые интерфейсы - IscriptCompileLog и IscriptRunLog:
unit Script.Interfaces; interface type IscriptLog = interface procedure Log(const aString: String); end;//IscriptLog IscriptCompileLog = interface(IscriptLog) end;//IscriptCompileLog IscriptRunLog = interface(IscriptLog) end;//IscriptRunLog implementation end.
Введём новый класс - TscriptRunContext:
unit Script.WordsInterfaces;
interface
uses
Core.Obj,
Script.Interfaces
;
type
TscriptContext = class(TCoreObject)
private
f_Log : IscriptLog;
protected
procedure Cleanup; override;
public
constructor Create(const aLog: IscriptLog);
procedure Log(const aString: String);
{* - Выводит сообщение в лог. }
end;//TscriptContext
TscriptCompileContext = class(TscriptContext)
public
constructor Create(const aLog: IscriptCompileLog);
end;//TscriptCompileContext
TscriptRunContext = class(TscriptContext)
public
constructor Create(const aLog: IscriptRunLog);
end;//TscriptRunContext
IscriptWord = interface
procedure DoIt(aContext: TscriptContext);
{* - собственно процедура для выполнения слова словаря. }
end;//IscriptWord
implementation
// TscriptContext
constructor TscriptContext.Create(const aLog: IscriptLog);
begin
inherited Create;
f_Log := aLog;
end;
procedure TscriptContext.Log(const aString: String);
{* - Выводит сообщение в лог. }
begin
if (f_Log <> nil) then
f_Log.Log(aString);
end;
procedure TscriptContext.Cleanup;
begin
f_Log := nil;
inherited;
end;
// TscriptCompileContext
constructor TscriptCompileContext.Create(const aLog: IscriptCompileLog);
begin
inherited Create(aLog);
end;
// TscriptRunContext
constructor TscriptRunContext.Create(const aLog: IscriptRunLog);
begin
inherited Create(aLog);
end;
end.
И видоизменим скриптовую машину:
unit Script.Engine;
interface
uses
Script.Interfaces
;
type
TscriptEngine = class
public
class procedure RunScript(const aFileName: String;
const aCompileLog: IscriptCompileLog;
const aRunLog : IscriptRunLog);
end;//TscriptEngine
implementation
uses
System.SysUtils,
Script.Parser,
Testing.Engine,
Script.Code,
Script.WordsInterfaces,
Script.StringWord
;
class procedure TscriptEngine.RunScript(const aFileName: String;
const aCompileLog: IscriptCompileLog;
const aRunLog : IscriptRunLog);
var
l_Parser : TscriptParser;
l_CompileContext : TscriptCompileContext;
l_RunContext : TscriptRunContext;
l_Code : TscriptCode;
l_StringWord : IscriptWord;
begin
TtestEngine.StartTest(aFileName);
try
l_Code := TscriptCode.Create;
try
l_CompileContext := TscriptCompileContext.Create(aCompileLog);
try
l_Parser := TscriptParser.Create(aFileName);
try
while not l_Parser.EOF do
begin
l_Parser.NextToken;
if (aCompileLog <> nil) then
aCompileLog.Log(l_Parser.TokenString);
l_StringWord := TscriptStringWord.Make(l_Parser.TokenString);
try
l_Code.CompileWord(l_StringWord);
finally
l_StringWord := nil;
end;//try..finally
end;//while not l_Parser.EOF
finally
FreeAndNil(l_Parser);
end;//try..finally
finally
FreeAndNil(l_CompileContext);
end;//try..finally
l_RunContext := TscriptRunContext.Create(aRunLog);
try
l_Code.Run(l_RunContext);
// - выполняем скомпилированный код
finally
FreeAndNil(l_RunContext);
end;//try..finally
finally
FreeAndNil(l_Code);
end;//try..finally
finally
TtestEngine.StopTest;
end;//try..finally
end;
end.
Видно, что процесс компиляции "стоит отдельно", а "процесс выполнения" - отдельно.
При этом компиляция логируется отдельно, а выполнение логируется отдельно.
Следующим образом:
unit Unit1;
interface
uses
System.SysUtils, System.Types, System.UITypes, System.Rtti, System.Classes,
System.Variants, FMX.Types, FMX.Controls, FMX.Forms, FMX.Dialogs,
FMX.StdCtrls, FMX.Edit, FMX.Layouts, FMX.Memo,
Script.Interfaces
;
type
TForm1 = class(TForm, IscriptCompileLog, IscriptRunLog)
Button1: TButton;
Button2: TButton;
Button3: TButton;
Edit1: TEdit;
Run: TButton;
CompileLog: TMemo;
RunLog: TMemo;
procedure Button1Click(Sender: TObject);
procedure RunClick(Sender: TObject);
private
{ Private declarations }
procedure IscriptCompileLog_Log(const aString: String);
procedure IscriptCompileLog.Log = IscriptCompileLog_Log;
procedure IscriptRunLog_Log(const aString: String);
procedure IscriptRunLog.Log = IscriptRunLog_Log;
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
uses
Script.Engine
;
{$R *.fmx}
procedure TForm1.IscriptCompileLog_Log(const aString: String);
begin
CompileLog.Lines.Add(aString);
end;
procedure TForm1.IscriptRunLog_Log(const aString: String);
begin
RunLog.Lines.Add(aString);
end;
procedure TForm1.Button1Click(Sender: TObject);
begin
Edit1.Text := (Sender As TButton).Text;
end;
procedure TForm1.RunClick(Sender: TObject);
begin
CompileLog.Lines.Clear;
RunLog.Lines.Clear;
TScriptEngine.RunScript('FirstScript.script', Self, Self);
end;
end.
- тут видно, что форма TForm1 стала реализовывать два интерфейса - IscriptCompileLog и IscriptRunLog.
Причём - разными методами.
И лог компиляции выводится в компонент CompileLog, а лог запуска в компонент RunLog.
Теперь давайте реально разделим компиляцию и запуск.
Выделим из метода RunScript метод CompileScript, который будет возвращать скомпилированный код, а также интерфейс IscriptCode, который собственно и представляет скомпилированный код.
А также введём интерфейс IscriptCompiler - компилятор кода скрипта.
(Ещё ремарка - во всех интерфейсах, что я описал - нету GUID. Это сделано преднамеренно. Причины описаны вот тут - http://18delphi.blogspot.ru/2013/11/supports.html и тут - http://18delphi.blogspot.ru/2013/11/queryinterface-supports.html и тут - http://18delphi.blogspot.ru/2013/10/supports.html
Когда нам РЕАЛЬНО понадобятся GUID'ы - мы их конечно же введём)
Вот что получается:
(Самые пытливые читатели могут посмотреть лог коммитов в SVN. Он сам по себе - интересен. Я ОБЫЧНО смотрю чужие логи, чтобы "следить за мыслью автора")
Интерфейс IscriptCode и IscriptCompiler тут:
unit Script.WordsInterfaces;
interface
uses
Core.Obj,
Script.Interfaces
;
type
TscriptContext = class(TCoreObject)
private
f_Log : IscriptLog;
protected
procedure Cleanup; override;
public
constructor Create(const aLog: IscriptLog);
procedure Log(const aString: String);
{* - Выводит сообщение в лог. }
end;//TscriptContext
TscriptCompileContext = class(TscriptContext)
public
constructor Create(const aLog: IscriptCompileLog);
end;//TscriptCompileContext
TscriptRunContext = class(TscriptContext)
public
constructor Create(const aLog: IscriptRunLog);
end;//TscriptRunContext
IscriptWord = interface
{* - слово скриптовой машины. }
procedure DoIt(aContext: TscriptContext);
{* - собственно процедура для выполнения слова словаря. }
end;//IscriptWord
IscriptCode = interface
{* - компилированный код скриптовой машины. }
procedure Run(aContext : TscriptRunContext);
{* - выполняет компилированный код. }
end;//IscriptCode
IscriptCompiler = interface
{* - компилятор кода скриптовой машины. }
procedure CompileWord(const aWord: IscriptWord);
{* - компилирует указанное слово в код. }
function CompiledCode: IscriptCode;
{* - скомпилированный код }
end;//IscriptCompiler
implementation
// TscriptContext
constructor TscriptContext.Create(const aLog: IscriptLog);
begin
inherited Create;
f_Log := aLog;
end;
procedure TscriptContext.Log(const aString: String);
{* - Выводит сообщение в лог. }
begin
if (f_Log <> nil) then
f_Log.Log(aString);
end;
procedure TscriptContext.Cleanup;
begin
f_Log := nil;
inherited;
end;
// TscriptCompileContext
constructor TscriptCompileContext.Create(const aLog: IscriptCompileLog);
begin
inherited Create(aLog);
end;
// TscriptRunContext
constructor TscriptRunContext.Create(const aLog: IscriptRunLog);
begin
inherited Create(aLog);
end;
end.
(Ещё одна ремарка - "смешение объектов и интерфейсов" - не должно пугать вас. Это - "не страшно". Ибо "интерфейсы" - внутренние. Они не пересекают "границы сред. Они служат лишь одному - подсчёту ссылок. Это можно было бы сделать и по-другому вот пример - http://18delphi.blogspot.ru/2013/04/iunknown.html и вот - http://18delphi.blogspot.ru/2013/03/blog-post_4606.html. Если пытливый читатель хочет "подискутировать" насчёт "объектов и интерфейсов" и "чистоты арийской расы" - пусть изучит приведённый ссылки и потом только "пишет письма" . У меня был ХОРОШИЙ преподаватель - Евсеев Олег (отчество - забыл) - он обычно говорил - "Хороший вопрос" Вы Люлин останьтесь - после семинара - обсудим")
Реализация интерфейсов IscriptCode и IscriptCompiler тут:
unit Script.Code;
interface
uses
Core.Obj,
System.Generics.Collections,
Script.WordsInterfaces
;
type
TscriptCodeContainer = class(TList<IscriptWord>)
end;//TscriptCodeContainer
TscriptCode = class(TCoreInterfacedObject, IscriptCode, IscriptCompiler)
private
f_Code : TscriptCodeContainer;
protected
// interfaces methods
procedure Run(aContext : TscriptRunContext);
{* - выполняет компилированный код. }
procedure CompileWord(const aWord: IscriptWord);
{* - компилирует указанное слово в код. }
function CompiledCode: IscriptCode;
{* - скомпилированный код }
protected
procedure Cleanup; override;
public
class function Make: IscriptCompiler;
{* - фабрика. }
end;//TscriptCode
TscriptCompiler = TscriptCode;
implementation
uses
System.SysUtils
;
// TscriptCode
class function TscriptCode.Make: IscriptCompiler;
{* - фабрика. }
begin
Result := Create;
end;
procedure TscriptCode.Cleanup;
begin
FreeAndNil(f_Code);
inherited;
end;
procedure TscriptCode.Run(aContext : TscriptRunContext);
var
l_Word : IscriptWord;
begin
if (Self.f_Code <> nil) then
for l_Word in Self.f_Code do
l_Word.DoIt(aContext);
end;
procedure TscriptCode.CompileWord(const aWord: IscriptWord);
{* - компилирует указанное слово в код. }
begin
if (Self.f_Code = nil) then
Self.f_Code := TscriptCodeContainer.Create;
Self.f_Code.Add(aWord);
end;
function TscriptCode.CompiledCode: IscriptCode;
{* - скомпилированный код }
begin
Result := Self;
end;
end.
(ох как чешутся руки про внедрение зависимостей рассказать...)
Теперь использование интерфейсов IscriptCode и IscriptCompiler и опять видоизменённая скриптовая машина:
unit Script.Engine;
interface
uses
Script.Interfaces,
Script.WordsInterfaces
;
type
TscriptEngine = class
public
class function CompileScript(const aFileName: String;
const aCompileLog: IscriptCompileLog): IscriptCode;
class procedure RunScript(const aFileName: String;
const aCompileLog: IscriptCompileLog;
const aRunLog : IscriptRunLog);
end;//TscriptEngine
implementation
uses
System.SysUtils,
Script.Parser,
Testing.Engine,
Script.Code,
Script.StringWord
;
class function TscriptEngine.CompileScript(const aFileName: String;
const aCompileLog: IscriptCompileLog): IscriptCode;
var
l_CodeCompiler : IscriptCompiler;
l_CompileContext : TscriptCompileContext;
l_Parser : TscriptParser;
l_StringWord : IscriptWord;
begin
TtestEngine.StartTest(aFileName);
try
l_CodeCompiler := TscriptCompiler.Make;
try
l_CompileContext := TscriptCompileContext.Create(aCompileLog);
try
l_Parser := TscriptParser.Create(aFileName);
try
while not l_Parser.EOF do
begin
l_Parser.NextToken;
if (aCompileLog <> nil) then
aCompileLog.Log(l_Parser.TokenString);
l_StringWord := TscriptStringWord.Make(l_Parser.TokenString);
try
l_CodeCompiler.CompileWord(l_StringWord);
finally
l_StringWord := nil;
end;//try..finally
end;//while not l_Parser.EOF
finally
FreeAndNil(l_Parser);
end;//try..finally
finally
FreeAndNil(l_CompileContext);
end;//try..finally
Result := l_CodeCompiler.CompiledCode;
finally
l_CodeCompiler := nil;
end;//try..finally
finally
TtestEngine.StopTest;
end;//try..finally
end;
class procedure TscriptEngine.RunScript(const aFileName: String;
const aCompileLog: IscriptCompileLog;
const aRunLog : IscriptRunLog);
var
l_RunContext : TscriptRunContext;
l_Code : IscriptCode;
begin
l_Code := Self.CompileScript(aFileName, aCompileLog);
try
l_RunContext := TscriptRunContext.Create(aRunLog);
try
l_Code.Run(l_RunContext);
// - выполняем скомпилированный код
finally
FreeAndNil(l_RunContext);
end;//try..finally
finally
l_Code := nil;
end;//try..finally
end;
end.
Вот как-то так пока.
(И главное - мы совсем забыли про тесты описанные тут - http://18delphi.blogspot.com/2013/11/5.html. Они ВСЕГДА проходят. TDD (http://ru.wikipedia.org/wiki/%D0%A0%D0%B0%D0%B7%D1%80%D0%B0%D0%B1%D0%BE%D1%82%D0%BA%D0%B0_%D1%87%D0%B5%D1%80%D0%B5%D0%B7_%D1%82%D0%B5%D1%81%D1%82%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5) - рулит)
(Семинар не очень длинный получается? Не на "две пары"?)
Теперь давайте отдельно скомпилируем токены (script_ttToken) и строки (script_ttString).
Появляется новый класс - TscriptUnknownToken. Он похож на TscriptStringWord, но я специально не обобщаю их код:
unit Script.UnknownToken;
interface
uses
Script.WordsInterfaces,
Script.Word
;
type
TscriptUnknownToken = class(TscriptWord)
private
f_String : String;
protected
procedure DoIt(aContext: TscriptContext); override;
public
constructor Create(const aString: String);
class function Make(const aString: String): IscriptWord;
end;//TscriptUnknownToken
implementation
constructor TscriptUnknownToken.Create(const aString: String);
begin
inherited Create;
f_String := aString;
end;
class function TscriptUnknownToken.Make(const aString: String): IscriptWord;
begin
Result := Create(aString);
end;
procedure TscriptUnknownToken.DoIt(aContext: TscriptContext);
begin
aContext.Log('Unknown token: ' + Self.f_String);
end;
end.
И вот так видоизменяется метод TscriptEngine.CompileScript:
class function TscriptEngine.CompileScript(const aFileName: String;
const aCompileLog: IscriptCompileLog): IscriptCode;
var
l_CodeCompiler : IscriptCompiler;
l_CompileContext : TscriptCompileContext;
l_Parser : TscriptParser;
begin
TtestEngine.StartTest(aFileName);
try
l_CodeCompiler := TscriptCompiler.Make;
try
l_CompileContext := TscriptCompileContext.Create(aCompileLog);
try
l_Parser := TscriptParser.Create(aFileName);
try
while not l_Parser.EOF do
begin
l_Parser.NextToken;
if (aCompileLog <> nil) then
aCompileLog.Log(l_Parser.TokenString);
Case l_Parser.TokenType of
script_ttEOF:
break;
script_ttToken:
l_CodeCompiler.CompileWord(TscriptUnknownToken.Make(l_Parser.TokenString));
script_ttString:
l_CodeCompiler.CompileWord(TscriptStringWord.Make(l_Parser.TokenString));
else
Assert(false, 'Неизвестный тип токена: ' + GetEnumName(TypeInfo(TscriptTokenType), Ord(l_Parser.TokenType)));
end;//Case l_Parser.TokenType
end;//while not l_Parser.EOF
finally
FreeAndNil(l_Parser);
end;//try..finally
finally
FreeAndNil(l_CompileContext);
end;//try..finally
Result := l_CodeCompiler.CompiledCode;
finally
l_CodeCompiler := nil;
end;//try..finally
finally
TtestEngine.StopTest;
end;//try..finally
end;
Теперь подумаем - "а какие же токены", не являются Unknown?
Разберём регистрацию слов в словаре.
Посмотрим на реализацию класса словаря - TscriptDictionary. Он опять же - более, чем прост.
Благодаря всё тем же обобщениям:
unit Script.Dictionary; interface uses System.Generics.Collections, Script.WordsInterfaces, Script.Word ; type TscriptDictionary = class(TDictionary<String, IscriptWord>) public procedure AddWord(const aKey: String; aWordClass : RscriptWord); end;//TscriptDictionary implementation procedure TscriptDictionary.AddWord(const aKey: String; aWordClass : RscriptWord); var l_Word : IscriptWord; begin l_Word := aWordClass.Make; try Self.Add(aKey, l_Word); finally l_Word := nil end;//try..finally end; end.
Теперь посмотрим на "аксиоматику" - TscriptAxiomatiсs.
На данный момент унаследуем этот класс от TscriptDictionary и сделаем "одиночкой" (http://ru.wikipedia.org/wiki/Singleton).
Вот наш новый класс:
unit Script.Axiomatics; interface uses Script.Dictionary ; type TscriptAxiomatics = class(TscriptDictionary) private class var f_Instance : TscriptAxiomatics; public class function Instance: TscriptAxiomatics; end;//TscriptAxiomatics implementation uses System.SysUtils ; class function TscriptAxiomatics.Instance: TscriptAxiomatics; begin if (f_Instance = nil) then f_Instance := TscriptAxiomatics.Create; Result := f_Instance; end; initialization finalization FreeAndNil(TscriptAxiomatics.f_Instance); end.
Эта реализация небезупречна и не потокобезопасна, но цель статьи не в том, чтобы рассказать о том как правильно делать "одиночек.
Код скриптовой машины видоизменяется так:
class function TscriptEngine.CompileScript(const aFileName: String;
const aCompileLog: IscriptCompileLog): IscriptCode;
var
l_CodeCompiler : IscriptCompiler;
l_CompileContext : TscriptCompileContext;
l_Parser : TscriptParser;
l_FoundWord : IscriptWord;
begin
TtestEngine.StartTest(aFileName);
try
l_CodeCompiler := TscriptCompiler.Make;
try
l_CompileContext := TscriptCompileContext.Create(aCompileLog);
try
l_Parser := TscriptParser.Create(aFileName);
try
while not l_Parser.EOF do
begin
l_Parser.NextToken;
if (aCompileLog <> nil) then
aCompileLog.Log(l_Parser.TokenString);
Case l_Parser.TokenType of
script_ttEOF:
break;
script_ttToken:
begin
if TscriptAxiomatics.Instance.TryGetValue(l_Parser.TokenString, l_FoundWord) then
// - слово зарегистрировано в аксиоматике
l_CodeCompiler.CompileWord(l_FoundWord)
// - компилируем его
else
l_CodeCompiler.CompileWord(TscriptUnknownToken.Make(l_Parser.TokenString));
// - пока компилируем заглушку
end;//script_ttToken
script_ttString:
l_CodeCompiler.CompileWord(TscriptStringWord.Make(l_Parser.TokenString));
else
Assert(false, 'Неизвестный тип токена: ' + GetEnumName(TypeInfo(TscriptTokenType), Ord(l_Parser.TokenType)));
end;//Case l_Parser.TokenType
end;//while not l_Parser.EOF
finally
FreeAndNil(l_Parser);
end;//try..finally
finally
FreeAndNil(l_CompileContext);
end;//try..finally
Result := l_CodeCompiler.CompiledCode;
finally
l_CodeCompiler := nil;
end;//try..finally
finally
TtestEngine.StopTest;
end;//try..finally
end;
Теперь используем этот класс (TscriptAxiomatics) для регистрации хотя бы одного слова аксиоматики:
unit Script.Word.Examples;
interface
uses
Script.WordsInterfaces,
Script.Word
;
type
TscriptWordExample1 = class(TscriptWord)
protected
procedure DoIt(aContext: TscriptContext); override;
end;//TscriptWordExample1
TscriptWordExample2 = class(TscriptWord)
protected
procedure DoIt(aContext: TscriptContext); override;
end;//TscriptWordExample2
implementation
uses
Script.Engine
;
// TscriptWordExample1
procedure TscriptWordExample1.DoIt(aContext: TscriptContext);
begin
aContext.Log('Example 1');
end;
// TscriptWordExample2
procedure TscriptWordExample2.DoIt(aContext: TscriptContext);
begin
aContext.Log('Example 2');
end;
initialization
TscriptEngine.RegisterKeyWord('DoNothing', TscriptWordExample1);
TscriptEngine.RegisterKeyWord('DoNothing2', TscriptWordExample2);
end.
На самом деле мы зарегистрировали два слова DoNothing и DoNothing2, связав их с классами TscriptWordExample1 и TscriptWordExample2 соответственно.
Запускаем наш пример и видим, что вывод в лог - изменился.
Теперь отвлечёмся от "наращивания функционала" и немного порефакторим.
Выделим из метода TscriptEngine.CompileScript - фабричный метод (http://18delphi.blogspot.ru/2013/04/blog-post_7483.html) TscriptEngine.CompileToken.
Предварительно введём интерфейс IscriptParser и фабричный метод TscriptParser.Make. А также свяжем контекст компиляции TscriptCompileContext с парсером IscriptParser и компилятором IscriptCompiler.
Вот что получается.
Вводим интерфейс парсера:
unit Script.Interfaces;
interface
type
IscriptLog = interface
procedure Log(const aString: String);
end;//IscriptLog
TscriptTokenType = (script_ttUnknown, script_ttToken, script_ttString, script_ttEOF);
IscriptParser = interface
function Get_TokenType: TscriptTokenType;
function Get_TokenString: String;
function EOF: Boolean;
{* - Достигнут конец входного потока. }
procedure NextToken;
{* - Выбрать следующий токен из входного потока. }
property TokenType: TscriptTokenType
read Get_TokenType;
property TokenString: String
read Get_TokenString;
end;//IscriptParser
IscriptCompileLog = interface(IscriptLog)
end;//IscriptCompileLog
IscriptRunLog = interface(IscriptLog)
end;//IscriptRunLog
implementation
end.
Связываем парсер и компилятор с контекстом:
unit Script.WordsInterfaces;
interface
uses
Core.Obj,
Script.Interfaces
;
type
TscriptContext = class(TCoreObject)
private
f_Log : IscriptLog;
protected
procedure Cleanup; override;
public
constructor Create(const aLog: IscriptLog);
procedure Log(const aString: String);
{* - Выводит сообщение в лог. }
end;//TscriptContext
IscriptCompiler = interface;
TscriptCompileContext = class(TscriptContext)
private
f_Parser : IscriptParser;
f_Compiler : IscriptCompiler;
protected
procedure Cleanup; override;
public
constructor Create(const aLog : IscriptCompileLog;
const aParser : IscriptParser;
const aCompiler : IscriptCompiler);
property Parser: IscriptParser
read f_Parser;
property Compiler: IscriptCompiler
read f_Compiler;
end;//TscriptCompileContext
TscriptRunContext = class(TscriptContext)
public
constructor Create(const aLog: IscriptRunLog);
end;//TscriptRunContext
IscriptWord = interface
{* - слово скриптовой машины. }
procedure DoIt(aContext: TscriptContext);
{* - собственно процедура для выполнения слова словаря. }
end;//IscriptWord
IscriptCode = interface
{* - компилированный код скриптовой машины. }
procedure Run(aContext : TscriptRunContext);
{* - выполняет компилированный код. }
end;//IscriptCode
IscriptCompiler = interface
{* - компилятор кода скриптовой машины. }
procedure CompileWord(const aWord: IscriptWord);
{* - компилирует указанное слово в код. }
function CompiledCode: IscriptCode;
{* - скомпилированный код }
end;//IscriptCompiler
implementation
// TscriptContext
constructor TscriptContext.Create(const aLog: IscriptLog);
begin
inherited Create;
f_Log := aLog;
end;
procedure TscriptContext.Log(const aString: String);
{* - Выводит сообщение в лог. }
begin
if (f_Log <> nil) then
f_Log.Log(aString);
end;
procedure TscriptContext.Cleanup;
begin
f_Log := nil;
inherited;
end;
// TscriptCompileContext
constructor TscriptCompileContext.Create(const aLog : IscriptCompileLog;
const aParser : IscriptParser;
const aCompiler : IscriptCompiler);
begin
Assert(aParser <> nil);
Assert(aCompiler <> nil);
inherited Create(aLog);
f_Parser := aParser;
f_Compiler := aCompiler;
end;
procedure TscriptCompileContext.Cleanup;
begin
f_Parser := nil;
f_Compiler := nil;
inherited;
end;
// TscriptRunContext
constructor TscriptRunContext.Create(const aLog: IscriptRunLog);
begin
inherited Create(aLog);
end;
end.
И вот реализация TscriptParser.Make:
unit Script.Parser;
interface
uses
Classes,
Core.Obj,
Script.Interfaces
;
{$IfNDef NoTesting}
{$Define TestParser}
{$EndIf NoTesting}
type
TscriptParser = class(TCoreObject)
private
f_Stream : TStream;
f_EOF : Boolean;
f_CurrentLine : String;
f_PosInCurrentLine : Integer;
f_Token : String;
f_TokenType : TscriptTokenType;
protected
procedure Cleanup; override;
function ReadLn: String;
protected
function GetChar(out aChar: AnsiChar): Boolean;
public
constructor Create(const aStream : TStream); overload;
constructor Create(const aFileName : String); overload;
class function Make(const aFileName : String): IscriptParser;
{* - Фабрика интерфейса IscriptParser. }
function EOF: Boolean;
{* - Достигнут конец входного потока. }
procedure NextToken;
{* - Выбрать следующий токен из входного потока. }
public
property TokenString: String
read f_Token;
{* - текущий токен. }
property TokenType: TscriptTokenType
read f_TokenType;
{* - тип текущего токена. }
end;//TscriptParser
implementation
uses
System.SysUtils
{$IfDef TestParser}
,
Testing.Engine
{$EndIf TestParser}
;
type
TscriptParserContainer = class(TCoreInterfacedObject, IscriptParser)
private
f_Parser : TscriptParser;
private
function Get_TokenType: TscriptTokenType;
function Get_TokenString: String;
function EOF: Boolean;
{* - Достигнут конец входного потока. }
procedure NextToken;
{* - Выбрать следующий токен из входного потока. }
protected
procedure Cleanup; override;
public
constructor Create(aParser: TscriptParser);
class function Make(aParser: TscriptParser): IscriptParser;
end;//TscriptParserContainer
constructor TscriptParserContainer.Create(aParser: TscriptParser);
begin
Assert(aParser <> nil);
inherited Create;
f_Parser := aParser;
end;
class function TscriptParserContainer.Make(aParser: TscriptParser): IscriptParser;
begin
Result := TscriptParserContainer.Create(aParser);
end;
procedure TscriptParserContainer.Cleanup;
begin
FreeAndNil(f_Parser);
inherited;
end;
function TscriptParserContainer.Get_TokenType: TscriptTokenType;
begin
Result := f_Parser.TokenType;
end;
function TscriptParserContainer.Get_TokenString: String;
begin
Result := f_Parser.TokenString;
end;
function TscriptParserContainer.EOF: Boolean;
{* - Достигнут конец входного потока. }
begin
Result := f_Parser.EOF;
end;
procedure TscriptParserContainer.NextToken;
{* - Выбрать следующий токен из входного потока. }
begin
f_Parser.NextToken;
end;
// TscriptParser
constructor TscriptParser.Create(const aStream : TStream);
begin
inherited Create;
f_PosInCurrentLine := 1;
f_EOF := false;
f_Stream := aStream;
end;
constructor TscriptParser.Create(const aFileName : String);
var
l_FileName : String;
begin
l_FileName := ExtractFilePath(ParamStr(0)) + '\' + aFileName;
Create(TFileStream.Create(l_FileName, fmOpenRead));
end;
class function TscriptParser.Make(const aFileName : String): IscriptParser;
{* - Фабрика интерфейса IscriptParser. }
begin
Result := TscriptParserContainer.Make(Self.Create(aFileName));
end;
...
end.
- тут есть одна тонкость.
Для того, чтобы реализовать интерфейс IscriptParser - мы не стали "ломать наследование" и менять структуру класса TscriptParser, а ввели дополнительный скрытый класс TscriptParserContainer, который агрегирует в себя TscriptParser и реализует интерфейс IscriptParser, пользуясь функционалом агрегированного класса TscriptParser. Эта техника сродни шаблону адаптер (http://ru.wikipedia.org/wiki/Adapter) или шаблону фасад (http://18delphi.blogspot.ru/2013/11/gui-back-to-basics_22.html#more).
Итак. Класс TscriptEngine опять видоизменяется:
unit Script.Engine;
interface
uses
Script.Interfaces,
Script.WordsInterfaces,
Script.Word
;
type
TscriptEngine = class
protected
class function CompileToken(aContext : TscriptCompileContext): Boolean;
public
class function CompileScript(const aFileName: String;
const aCompileLog: IscriptCompileLog): IscriptCode;
class procedure RunScript(const aFileName: String;
const aCompileLog: IscriptCompileLog;
const aRunLog : IscriptRunLog);
class procedure RegisterKeyWord(const aKeyWord: String; aWordClass: RscriptWord);
end;//TscriptEngine
implementation
uses
TypInfo,
System.SysUtils,
Script.Parser,
Testing.Engine,
Script.Code,
Script.StringWord,
Script.UnknownToken,
Script.Axiomatics
;
class function TscriptEngine.CompileToken(aContext : TscriptCompileContext): Boolean;
var
l_FoundWord : IscriptWord;
begin
Result := true;
aContext.Parser.NextToken;
aContext.Log(aContext.Parser.TokenString);
Case aContext.Parser.TokenType of
script_ttEOF:
Result := false;
script_ttToken:
begin
if TscriptAxiomatics.Instance.TryGetValue(aContext.Parser.TokenString, l_FoundWord) then
// - слово зарегистрировано в аксиоматике
aContext.Compiler.CompileWord(l_FoundWord)
// - компилируем его
else
aContext.Compiler.CompileWord(TscriptUnknownToken.Make(aContext.Parser.TokenString));
// - пока компилируем заглушку
end;//script_ttToken
script_ttString:
aContext.Compiler.CompileWord(TscriptStringWord.Make(aContext.Parser.TokenString));
else
Assert(false, 'Неизвестный тип токена: ' + GetEnumName(TypeInfo(TscriptTokenType), Ord(aContext.Parser.TokenType)));
end;//Case l_CompileContext.Parser.TokenType
end;
class function TscriptEngine.CompileScript(const aFileName: String;
const aCompileLog: IscriptCompileLog): IscriptCode;
var
l_CompileContext : TscriptCompileContext;
l_FoundWord : IscriptWord;
begin
l_CompileContext := TscriptCompileContext.Create(aCompileLog,
TscriptParser.Make(aFileName),
TscriptCompiler.Make);
try
while CompileToken(l_CompileContext) do
;
Result := l_CompileContext.Compiler.CompiledCode;
finally
FreeAndNil(l_CompileContext);
end;//try..finally
end;
class procedure TscriptEngine.RunScript(const aFileName: String;
const aCompileLog: IscriptCompileLog;
const aRunLog : IscriptRunLog);
var
l_RunContext : TscriptRunContext;
l_Code : IscriptCode;
begin
l_Code := Self.CompileScript(aFileName, aCompileLog);
try
l_RunContext := TscriptRunContext.Create(aRunLog);
try
l_Code.Run(l_RunContext);
// - выполняем скомпилированный код
finally
FreeAndNil(l_RunContext);
end;//try..finally
finally
l_Code := nil;
end;//try..finally
end;
class procedure TscriptEngine.RegisterKeyWord(const aKeyWord: String; aWordClass: RscriptWord);
begin
TscriptAxiomatics.Instance.AddWord(aKeyWord, aWordClass);
end;
end.
Теперь вернёмся к тому "ради чего всё делалось". Научимся "программно нажимать на кнопки".
Тут мы добрались до стека значений.
Посмотрим как он выглядит:
unit Script.WordsInterfaces;
interface
uses
System.Generics.Collections,
Core.Obj,
Script.Interfaces
;
type
TscriptValueType = (script_vtUnknown, script_vtString, script_vtObject);
TscriptValue = record
public
rValueType : TscriptValueType;
private
rAsString : String;
rAsObject : TObject;
public
constructor Create(const aString: String); overload;
constructor Create(anObject: TObject); overload;
function AsString: String;
function AsObject: TObject;
end;//TscriptValue
TscriptValuesStack = TList<TscriptValue>;
TscriptContext = class(TCoreObject)
private
f_Log : IscriptLog;
f_Stack : TscriptValuesStack;
protected
procedure Cleanup; override;
public
constructor Create(const aLog: IscriptLog);
procedure Log(const aString: String);
{* - Выводит сообщение в лог. }
function PopString: String;
procedure PushString(const aString: String);
function PopObject: TObject;
procedure PushObject(const anObject: TObject);
end;//TscriptContext
IscriptCompiler = interface;
TscriptCompileContext = class(TscriptContext)
private
f_Parser : IscriptParser;
f_Compiler : IscriptCompiler;
protected
procedure Cleanup; override;
public
constructor Create(const aLog : IscriptCompileLog;
const aParser : IscriptParser;
const aCompiler : IscriptCompiler);
property Parser: IscriptParser
read f_Parser;
property Compiler: IscriptCompiler
read f_Compiler;
end;//TscriptCompileContext
TscriptRunContext = class(TscriptContext)
public
constructor Create(const aLog: IscriptRunLog);
end;//TscriptRunContext
IscriptWord = interface
{* - слово скриптовой машины. }
procedure DoIt(aContext: TscriptContext);
{* - собственно процедура для выполнения слова словаря. }
end;//IscriptWord
IscriptCode = interface
{* - компилированный код скриптовой машины. }
procedure Run(aContext : TscriptRunContext);
{* - выполняет компилированный код. }
end;//IscriptCode
IscriptCompiler = interface
{* - компилятор кода скриптовой машины. }
procedure CompileWord(const aWord: IscriptWord);
{* - компилирует указанное слово в код. }
function CompiledCode: IscriptCode;
{* - скомпилированный код }
end;//IscriptCompiler
implementation
uses
System.SysUtils
;
// TscriptValue
constructor TscriptValue.Create(const aString: String);
begin
inherited;
rValueType := script_vtString;
rAsString := aString;
end;
constructor TscriptValue.Create(anObject: TObject);
begin
inherited;
rValueType := script_vtObject;
rAsObject := anObject;
end;
function TscriptValue.AsString: String;
begin
Assert(rValueType = script_vtString);
Result := rAsString;
end;
function TscriptValue.AsObject: TObject;
begin
Assert(rValueType = script_vtObject);
Result := rAsObject;
end;
// TscriptContext
constructor TscriptContext.Create(const aLog: IscriptLog);
begin
inherited Create;
f_Log := aLog;
f_Stack := TscriptValuesStack.Create;
end;
procedure TscriptContext.Log(const aString: String);
{* - Выводит сообщение в лог. }
begin
if (f_Log <> nil) then
f_Log.Log(aString);
end;
function TscriptContext.PopString: String;
begin
Assert(f_Stack.Count > 0);
Result := f_Stack.Last.AsString;
f_Stack.Delete(f_Stack.Count - 1);
end;
procedure TscriptContext.PushString(const aString: String);
begin
f_Stack.Add(TscriptValue.Create(aString));
end;
function TscriptContext.PopObject: TObject;
begin
Assert(f_Stack.Count > 0);
Result := f_Stack.Last.AsObject;
f_Stack.Delete(f_Stack.Count - 1);
end;
procedure TscriptContext.PushObject(const anObject: TObject);
begin
f_Stack.Add(TscriptValue.Create(anObject));
end;
procedure TscriptContext.Cleanup;
begin
f_Log := nil;
FreeAndNil(f_Stack);
inherited;
end;
// TscriptCompileContext
constructor TscriptCompileContext.Create(const aLog : IscriptCompileLog;
const aParser : IscriptParser;
const aCompiler : IscriptCompiler);
begin
Assert(aParser <> nil);
Assert(aCompiler <> nil);
inherited Create(aLog);
f_Parser := aParser;
f_Compiler := aCompiler;
end;
procedure TscriptCompileContext.Cleanup;
begin
f_Parser := nil;
f_Compiler := nil;
inherited;
end;
// TscriptRunContext
constructor TscriptRunContext.Create(const aLog: IscriptRunLog);
begin
inherited Create(aLog);
end;
end.
- тут мы опять использовали обобщения из стандартной библиотеки.
Теперь на основе всех полученных знаний введём два слова аксиоматики - TkwFindComponent и TkwButtonClick.
Первое слово ищет на текущей форме приложения компонент с заданным именем и кладёт его в стек.
Второе слово выбирает из стека значение объекта и трактует его как кнопку и пытается нажать указанную кнопку.
Вот эти слова:
unit Script.Word.Buttons;
interface
uses
Script.WordsInterfaces,
Script.Word
;
type
TkwFindComponent = class(TscriptWord)
protected
procedure DoIt(aContext: TscriptContext); override;
end;//TkwFindComponent
TkwButtonClick = class(TscriptWord)
protected
procedure DoIt(aContext: TscriptContext); override;
end;//TkwButtonClick
implementation
uses
System.Classes,
Script.Engine,
FMX.Controls,
FMX.StdCtrls,
FMX.Forms
;
// TkwFindComponent
procedure TkwFindComponent.DoIt(aContext: TscriptContext);
var
l_Name : String;
l_Component : TComponent;
begin
aContext.Log(ClassName);
l_Name := aContext.PopString;
Assert(l_Name <> '');
l_Component := Screen.ActiveForm.FindComponent(l_Name);
Assert(l_Component <> nil);
aContext.PushObject(l_Component);
end;
// TkwButtonClick
type
TControlAccess = class(TControl)
end;//TControlAccess
procedure TkwButtonClick.DoIt(aContext: TscriptContext);
var
l_Component : TComponent;
begin
aContext.Log(ClassName);
l_Component := aContext.PopObject As TComponent;
Assert(l_Component Is TButton);
TControlAccess(l_Component).Click;
end;
initialization
TscriptEngine.RegisterKeyWord('FindComponent', TkwFindComponent);
TscriptEngine.RegisterKeyWord('ButtonClick', TkwButtonClick);
end.
Ремарка. Тип TControlAccess - служит для доступа к protected методу Click класса TControl. Это известный "трюк". Сами Borland и Embarcadero этим частенько пользуются. Позже мы рассмотрим "новый" RTTI. Там нам подобные "трюки" - не понадобятся.
Итак.
Всё готово для программного нажатия на кнопки из скрипта.
Код скрипта такой:
'Button1' FindComponent ButtonClick // - нажимаем на кнопку
или такой:
'Button2' FindComponent ButtonClick // - нажимаем на кнопку
Попробуйте их оба. Может быть вам понравится.
Заключение.
Итак. На достаточно простом примере "нажатия кнопки на форме" мы разобрали в общем не самые простые вещи.
1. Мы описали скриптовую машину.
2. Описали процесс компиляции скриптов.
3. Описали регистрацию слов в словаре аксиоматики.
4. Разобрали процесс запуска скриптов.
5. Описали стек значений и посмотрели на примеры его использования.
Ну и "на поверхности" мы добились поставленной цели - научились "программно нажимать кнопки приложения из скриптов".
Надеюсь, что понятна одна из основных идей - уже описанная скриптовая машина может расширяться путём регистрации новых слов в аксиоматике на стороне Delphi.
На этом данную статью я заканчиваю.
Если будет интерес у читателей - продолжу тему скриптовой машины и того "как она устроена".
Там вообще говоря можно многое обсудить:
1. Условные операторы.
2. Циклы.
3. Переменные.
4. Определение новых слов пользователем из кода скрипта.
5. Включение в код скрипта кода из других файлов.
6. И многое другое.
P.S. ДАВНО я не набивал СТОЛЬКО кода ВРУЧНУЮ.
Я давно уже "рисую квадраты на модели". Это - СИЛЬНО быстрее...
P.P.S. Повторюсь. Delphi - это всего лишь язык, на котором написано "это". Но "это" может быть написано на любом языке. Хоть Objective-C, хоть Python, хоть C++, хоть классический C, хоть классический Паскаль (в стиле Turbo Professional). Разница только в "запятых".
Главное это - суть ПОДХОДА.
P.P.P.S. Есть ещё одна "хотелка" - нарисовать GUITestRunner для DUnit для проектов на базе FM. Я постараюсь её реализовать.
P.P.P.P.S. Ну и анонс. Вот тут лежит новая версия примера - https://sourceforge.net/p/rumtmarc/code-0/HEAD/tree/trunk/Blogger/GUITests/Chapter2/ Там же доступен пример и VCL-проекта. Там же есть пример и для мобильных устройств, но я его пока запускал только под эмулятором.
Эта статья случайно "пошла в производство". Видимо я вчера ночью "с устатку" нажал кнопку "опубликовать", а не "сохранить". Я её сейчас уберу, а на выходных - доделаю.
ОтветитьУдалитьХотя (что не может не радовать) четырём людям она уже понравилась. Судя по "лайкам".
Удалить"Я давно уже "рисую квадраты на модели". Это - СИЛЬНО быстрее..."
УдалитьПосмотреть бы...Типа видеоурок...
Ну мне тут так долго рассказывали, что "нету мотивации" и "поощряется дублирование кода", что я решил пока с UML взять тайм-аут.
УдалитьИнтересный итог.
ОтветитьУдалить1000 людей (примерно) "просмотрели" или "прочитали" данную "заметку".
Которая писалась "не один день" и в которую "было вложено немало труда".
Один поставил - "интересно".
Шесть поставили - "круто".
Ещё пять написали по почте что-то вроде - "интересная статья. спасибо. полезно".
Т.е. остальным либо БЕЗРАЗЛИЧНО, либо они САМИ СДЕЛАЛИ "что-то подобное". Ну или "лень нажать на кнопку".
Просто интересно.
Да и двое коллег сказали что-то вроде - "это ОЧЕНЬ интересно. Полезно. Но НЕ В ЭТОЙ ЖИЗНИ".
УдалитьА у меня тут в голову мысля пришла, как ещё скрипты можно использовать. Не только для тестирования. Но и для создания таких помощников для юзеров. Ну типа визарда...
ОтветитьУдалитьТ.е. юзер запускает скрипт, скрипт в сою очередь "прокликивает" интерфейс в нужной последовательности, открывает нужную форму и потом ждёт ввода пользователя. Пользователь вводит в поля формы инфу и возвращается к помощнику, который делает следующий шаг...
Ну это например для автоматизации заполнения каких-нибудь данных (хотя тут больше макрос бы подошёл). Или для обучения...
Не поверите :-) Я думал и об этом :-)
ОтветитьУдалить"хотя тут больше макрос бы подошёл"
ОтветитьУдалитьА чем интересно "скрипты" это не "макрос"? :-)
Учитывая, что в "моих скриптах" есть даже создание объектов (http://programmingmindstream.blogspot.ru/2013/12/to-do.html) то это - БОЛЕЕ ЧЕМ "макрос".
ОтветитьУдалитьЭээм... наверное ничем :)
ОтветитьУдалитьПросто в данном случае я подразумеваю, что скрипт - это частный случай макроса. Макрос - это что-то автоматически записываемое.
Т.е. чтобы написать скрипт, необходимы знания о словаре и синтаксисе. Это для продвинутых юзеров.
А чтобы записать макрос - необходимо нажать кнопку "записать макрос", совершить последовательность действий, и нажать "остановить запись". И дать получившийся скрипт-макрос другому юзеру (или юзать самому).
Т.е., наверное, говоря слово "макрос", я подразумеваю, что система способна не только воспроизводить скрипты, но писать их. Как-то так :)
ААА!!! Ну это - ДА!
УдалитьМакро-рекодера у нас нету... Хотя вопрос и "стоит на повестке"...
Если кстати есть желание "поучаствовать" - Wellcome.
У меня понимаете есть код который я "пишу внутри конторы" и который "пишу ВНЕ конторы". Они - НЕ ПЕРЕСЕКАЮТСЯ,
Чтобы НЕ БЫЛО ПРОБЛЕМ.
Хотя конечно и "одно повторяет другое", а "другое - одно".
Так что если "есть интерес", то мы можем попробовать "развить Open-Source проект". А может и не Open-Source.
Можем "совместно" попробовать что-нибудь сделать. Тот же "макро-рекодер"...
Ну если интересно конечно...
Кстати писать такой "макро-рекодер" - ну "не очень сложно".
УдалитьПри наличии уже СУЩЕСТВУЮЩЕЙ скриптовой машины и её словаря.
Достаточно "логировать действия пользователя" в определённые слова словаря скриптовой машины.
Ну придётся описать несколько "отображений", что "такое-то действие пользователя мапируется сюда", а "такое-то - сюда". И ВСЁ.
Делов-то.
И нас кстати сделана некоторая "обвязка". Позже приведу на неё ссылку.
Вот ссылка - https://sourceforge.net/p/rumtmarc/code-0/HEAD/tree/trunk/Blogger/RealWork/VCM/l3ComponentNameHelper.pas
Удалить