Как лучше всего использовать тот язык, который знает ваша команда?

  • Использовать те же принципы реализации.
  • Для создания обзора дизайна и обзора кодирования.
  • Создавать одинаковые папки во всех ваших проектах.
  • Чтобы использовать шаблон проектирования в вашем коде.
  • Чтобы создать такое же соглашение для вашего кода с помощью # 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);
}
}

Журналы после всего запуска:

Вывод:

Используя код генератора, чтобы ваш код оставался простым, с теми же соглашениями и повторно используйте свои шаблоны, сделайте это, и ваша команда будет более эффективной!

Github