Как лучше всего использовать тот язык, который знает ваша команда?
- Использовать те же принципы реализации.
- Для создания обзора дизайна и обзора кодирования.
- Создавать одинаковые папки во всех ваших проектах.
- Чтобы использовать шаблон проектирования в вашем коде.
- Чтобы создать такое же соглашение для вашего кода с помощью # typescript-compiler (например, ts-morph / ts-simple-ast)
Сегодня мы собираемся использовать # typescript-compiler для создания нашего State-Machine:
Прежде всего, нам нужно понять, где мы отключим / используем генератор?
Где код, который нам нужно продублировать (использовать генератор)?
Приступим к написанию неиспользуемого кода генератора.
Общий Код (неиспользуемый генератор)
- StateManager и BaseState будут иметь отношение ко всему конкретному коду, поэтому прежде всего нам нужно его реализовать, и после того, как мы сможем использовать его в нашем шаблоне генератора (конкретный код)
- Вспомогательные функции (например, создатель, декораторы и т. Д.)
BaseManager.ts — manage state import { Subject, Observable } from ‘rxjs’; import { BaseState } from ‘./base-state’; export abstract class BaseManager { protected states: IStates; public abstract getSubject(): Subject<any>; } export interface IStates { [key: string]: BaseState; }
- состояния - ссылка на все наши состояния из общего кода.
- getSubject - актуален для вызова событий между состояниями менеджера ‹-›.
BaseState.ts - create the same logic to all our states import { BaseManager } from “../base-state-machine/base-manager”; export abstract class BaseState { stateMachineManager: BaseManager; nextState(index: string): void { this.stateMachineManager.getSubject().next(index); } error(message): void { this.stateMachineManager.getSubject().error({state: this, message}); } complete(): void { this.stateMachineManager.getSubject().complete() } abstract start(); // run on every state starting }
- nextState - обновить диспетчер после завершения состояния и позволить диспетчеру взять на себя ответственность за весь переход к следующему состоянию.
- ошибка - диспетчер обновлений, если одно из состояний завершилось с ошибкой, и диспетчер получит возможность выбора, каким будет следующий шаг к конечному автомату.
- завершено - после успешного выполнения всех состояний.
- start () - нам нужно реализовать только этот код для каждого состояния с разной логикой после того, как мы запустили генератор и были созданы наши файлы.
BaseStateFactory.ts - using to initiate all states when manager starting to run. import { BaseState } from '../../base-state-machine/base-state'; import { BaseManager } from '../../base-state-machine/base-manager'; export class BaseStateFactory { static createStates<T extends any>(typeContr: string[], baseManager: BaseManager, allStates:T, ... args: any[]): {} { const states: {} = {}; typeContr.forEach(state => { const construtorFunction = (allStates as any)[state]; if (!construtorFunction) throw new Error('No such BaseState'); let stateCtor: BaseState = new construtorFunction(... args); stateCtor.stateMachineManager = baseManager; states[state] = stateCtor; }) return states; }}
Конкретный код (используйте генератор):
- ConcreteMachineManager1 / 2
- ConcreteState1 / 2….
Давайте посмотрим на один из наших конкретных кодов, чтобы понять, что нам нужно сгенерировать с помощью нашего компилятора машинописного текста:
import { ExampleMachineEnum } from "../types/example-machine.enum"; import { logs } from "../../../global/utils/decorators/logs"; import { BaseState } from "../../../global/base-state-machine/base-state"; export class ConcreteState1 extends BaseState { @logs() start() { // Here we need to add our code after we generate it via reflection } }
Создайте список для динамического создания с помощью компилятора ts:
- Для динамического создания имени состояния класса.
- Добавить 3 импорта.
- Добавить декоратор логов в метод start.
- Создать каркас метода запуска.
*** Как вы понимаете, нам просто нужно написать содержимое start, и все, все наши соглашения о коде были сохранены.
Посмотрим, как произошло волшебство:
Generator.ts - (полный код)
function createStates(project: Project, stateMachineItem:IStateMachine): void { let allExports = ``; const dasherizeManager = fromCamelCase(stateMachineItem.name); stateMachineItem.states.forEach(stateItem => { const simpleClass = `export class ${stateItem} extends BaseState { @logs() start() {} }`; const dasherizeItem = fromCamelCase(stateItem); const pathToFile = `./src/state-machines-generators/${dasherizeManager}/states/${dasherizeItem}.ts`; const myClassFile = project.createSourceFile(pathToFile, simpleClass, {overwrite: true}); allExports+= `\n export * from './${dasherizeItem}'` myClassFile.addImportDeclaration({ defaultImport: `{ ${stateMachineItem.name}Enum }`, moduleSpecifier: `../types/${dasherizeManager}.enum`, }); myClassFile.addImportDeclaration({ defaultImport: '{ logs }', moduleSpecifier: '../../../global/utils/decorators/logs', }); myClassFile.addImportDeclaration({ defaultImport: '{ BaseState }', moduleSpecifier: '../../../global/base-state-machine/base-state', }); myClassFile.saveSync(); }); const pathToFile = `./src/state-machines-generators/${dasherizeManager}/states/index.ts`; const simpleClass = allExports; const indexFile = project.createSourceFile(pathToFile, simpleClass, {overwrite: true}); indexFile.saveSync(); }
- Прежде всего, мы получаем оболочку компилятора #typescript с объектом ts-simple-ast (ts-morph):
import Project, { Scope } from ‘ts-simple-ast’;
Теперь мы можем использовать его для createSourceFile с:
- место, где мы хотим создать файл (pathToFile)
- каково содержимое этого файла (simpleClass):
const simpleClass = `export class ${stateItem} extends BaseState { @logs() start() {} }`; const pathToFile = `./src/state-machines-generators/${dasherizeManager}/states/${dasherizeItem}.ts`;
- Мы используем createSourceFile api
const myClassFile = project.createSourceFile(pathToFile, simpleClass, {overwrite: true});
*** Вот и родился новый файл !!!
После этого мы можем получить файл и добавить импорт, методы и свойства:
- addImportDeclaration
- addMethod
- addProperty
** не волнуйтесь, это тоже просто - получите класс с помощью getClassOrThrow и измените этот файл с помощью принадлежащих ему методов:
const myClass = myClassFile.getClassOrThrow(`${stateMachineItem.name}Manager`); myClass.addImportDeclaration({ // Add new import defaultImport: `{ ${stateMachineItem.name}Enum }`, moduleSpecifier: `../types/${dasherizeManager}.enum`, }); myClass.addProperty({ // Add new properties name: 'externalNotifier', isStatic: false, type: 'Subject<any>', initializer: 'new Subject<any>()' }); myClass.addMethod({ name: 'initStates', isStatic: false, parameters: [{name: 'coffeeStateManager', type: 'BaseManager'}], returnType: 'void', bodyText: `this.states = BaseStateFactory.createStates(Object.keys(${stateMachineItem.name}Enum),coffeeStateManager, States);`, });
Итак, после того, как мы поймем, как использовать компилятор машинописного текста в нашем коде, давайте посмотрим
Как мы можем использовать это во всех наших штатах
Кто говорит нашему генератору, как называются наши состояния?
Для этого я создаю файл конфигурации с именем state-machines.json, и наш генератор может его использовать:
state-machines.json // You can add many state-machines and in one click to build it [{ "name": "ExampleMachine", "states": ["AddState", "RemoveState", "CompleteState", "FailedState", "StartState"], "description": "Some Example" }]
Как видите, это просто:
- добавить / удалить новые состояния в нашу ExampleStateMachine,
- добавить / удалить новый конечный автомат, добавив новый элемент в наш файл JSON массива и просто сгенерировать его.
Теперь мы можем сгенерировать наш код, чтобы использовать его и запустить, посмотрим, что мы сгенерируем?
На рисунке 2 показано решение после запуска наших генераторов в файле state-machine.json.
Что есть в нашей среде:
- Все состояния (сгенерировано)
- Перечисление со всеми состояниями для «перехода из состояния в другое» (сгенерировано)
- Менеджер, отвечающий за все переходы между состояниями (сгенерировано)
- глобальный каталог (не был сгенерирован весь код: BaseState, BaseManager, Decorators и helper)
Я добавил пример перехода между состояниями:
export class AddCoffeeState extends BaseState { @logs() start() { setTimeout(()=>{ this.nextState(CoffeeMachineEnum.AddSugarState); // this raise message by subject to Manager and the manager get responsibilities on start our next state "AddSugarState" }, 5000); } }
Журналы после всего запуска:
Вывод:
Используя код генератора, чтобы ваш код оставался простым, с теми же соглашениями и повторно используйте свои шаблоны, сделайте это, и ваша команда будет более эффективной!