gt9p5kz-lib
v0.0.14
Published
Все мы знаем про солид, но делаем вид что придерживаемся его и в итоге достаточно быстро проекты приходят к состоянию поддержки через боль. Давайте попробуем с этим что то сделать.
Downloads
4
Readme
Все мы знаем про солид, но делаем вид что придерживаемся его и в итоге достаточно быстро проекты приходят к состоянию поддержки через боль. Давайте попробуем с этим что то сделать.
S, Единая ответственность.
class UserService {
async getUsers() {
const headers = { 'content-type': 'application/tson' };
const requestInit = { method: 'POST', headers };
const url = 'url';
return await fetch(url, requestInit);
}
}
Привычная и кажется хорошо написанная функция, но полностью нарушающая принцип единой ответственности и принцип Don't Repeat Yourself.
const headers = {'content-type': 'application/tson'}
с точки зрения ООП это const headers = new Headers(…)
то есть в функции есть ответственность по инстанциированию класса и не одного.
Очень бы хотелось иметь функцию примерно в таком виде:
class UserService {
constructor(fetcher) {}
async getUsers() {
const url = 'url';
return await this.fetcher.posttsON(url);
}
}
И не писать миллион раз по всему проекту const headers = {'content-type': 'application/tson'}
.
I. Разделение интерфейсов.
class ClassicLogger {
log() {...}
}
class UserService {
constructor(private readonly logger: ClassicLogger) {}
getUser() {
...
this.logger.log()
}
}
Вполне привычная картина, рабочая но если вдруг нужно поменять логгер, то придется пройтись по всему коду и поменять класс логгера. Наверное хотелось бы иметь что то типо того:
interface ILogger {
log(): void
}
class UserService {
constructor(private readonly logger: ILogger) {}
getUser() {
...
this.logger.log()
}
}
и в случае замены логгера сделать эту замену только в одном месте. И D это общение верхних пунктов. Принцип делегирования инстанциирований во внешнюю систему и описание ожидания от конкретного инстанциирования интерфейсом.
Первый шаг - это сделать внешнюю систему, которая заберет ответственность за инстанциирование. Это будет делать фреймворк. Второй шаг - это договоренность в команде о:
- каждый класс или объект или примитив должен импортироваться только ОДИН раз и строго в сторону фреймворка
- каждый класс должен имплементить один или несколько интерфейсов или расширять класс, который имплементит один или несколько интерфейсов К библиотеке.
Установка библиотеки:
$ npm install gt9p5kz-lib
потом импортим:
import { Athlete } from 'gt9p5kz-lib';
import type { IUse, IExecutableUse } from 'gt9p5kz-lib';
Базовое применение. Инициализация фреймворка:
import { Athlete } from 'gt9p5kz-lib';
new Athlete().createFramework().start();
Для добавления сущностей во фреймворк необходимо имплементировать интерфейс IUse. Например добавим класс Service:
import { Athlete } from 'gt9p5kz-lib';
import type { IBind, IBindFactory, IUse } from 'gt9p5kz-lib';
interface IService {
showTime(): void;
}
class Service implements IService {
public showTime(): void {
const now = new Date();
const time = `${now.getHours()}:${now.getMinutes()}`;
console.log(time);
}
}
class ServiceUse implements IUse {
public exportBinds(bindFactory: IBindFactory): IBind[] {
return [bindFactory.createBind(Service, 'Service')];
}
}
new Athlete().createFramework().use(new ServiceUse()).start();
Теперь при старте фреймворка создаться инстанс класса Service и его можно будет использовать.
Для того чтобы добавить слушаетля необходимо имплементировать интерфейс IExectableUse. Например добавим интервал:
import { Athlete } from 'gt9p5kz-lib';
import type { IBind, IBindFactory, IContainer, IExecutableUse, IUse } from 'gt9p5kz-lib';
interface IInterval {
start(fn: Function): void;
}
class Interval implements IInterval {
public start(fn: Function): void {
try {
setInterval(fn, 1000);
} catch {}
}
}
class IntervalUse implements IUse, IExecutableUse {
public exportBinds(bindFactory: IBindFactory): IBind[] {
return [bindFactory.createBind(Interval, 'Interval')];
}
public execute(container: IContainer): void {
const interval = container.getInstanceByInterface<IInterval>('Interval', 'start');
if (interval) interval.start(() => console.log('athlete'));
}
}
new Athlete().createFramework().use(new IntervalUse()).start();
Теперь можно сделать так, чтобы интервал брал функию из сервиса:
import { Athlete } from 'gt9p5kz-lib';
import type { IBind, IBindFactory, IContainer, IExecutableUse, IUse } from 'gt9p5kz-lib';
interface IService {
showTime(): void;
}
class Service implements IService {
public showTime(): void {
const now = new Date();
const time = `${now.getHours()}:${now.getMinutes()}`;
console.log(time);
}
}
interface IInterval {
start(fn: Function): void;
}
class Interval implements IInterval {
public start(fn: Function): void {
try {
setInterval(fn, 1000);
} catch {}
}
}
class ServiceUse implements IUse {
public exportBinds(bindFactory: IBindFactory): IBind[] {
return [bindFactory.createBind(Service, 'Service')];
}
}
class IntervalUse implements IUse, IExecutableUse {
public exportBinds(bindFactory: IBindFactory): IBind[] {
return [bindFactory.createBind(Interval, 'Interval')];
}
public execute(container: IContainer): void {
const interval = container.getInstanceByInterface<IInterval>('Interval', 'start');
const service = container.getInstanceByInterface<IService>('Service', 'showTime');
if (service && interval) interval.start(service.showTime.bind(service));
}
}
new Athlete().createFramework().use(new ServiceUse()).use(new IntervalUse()).start();
Теперь зарефакторим логгер:
import { Athlete } from 'gt9p5kz-lib';
import type { IBind, IBindFactory, IContainer, IExecutableUse, IUse } from 'gt9p5kz-lib';
interface ILogger {
log(str: string): void;
}
class ClassicLogger implements ILogger {
public log(str: string): void {
console.log('classic');
console.log(str);
}
}
class FastLogger implements ILogger {
public log(str: string): void {
process.stdout.write('fast\n');
process.stdout.write(str + '\n');
}
}
interface IService {
showTime(): void;
}
class Service implements IService {
constructor(private readonly logger: ILogger) {}
public showTime(): void {
const now = new Date();
const time = `${now.getHours()}:${now.getMinutes()}`;
this.logger.log(time);
}
}
interface IInterval {
start(fn: Function): void;
}
class Interval implements IInterval {
public start(fn: Function): void {
try {
setInterval(fn, 1000);
} catch {}
}
}
class ClassicLoggerUse implements IUse {
public exportBinds(bindFactory: IBindFactory): IBind[] {
return [bindFactory.createBind(ClassicLogger, 'Logger')];
}
}
class FastLoggerUse implements IUse {
public exportBinds(bindFactory: IBindFactory): IBind[] {
return [bindFactory.createBind(FastLogger, 'Logger')];
}
}
class ServiceUse implements IUse {
public exportBinds(bindFactory: IBindFactory): IBind[] {
return [bindFactory.createBind(Service, 'Service', ['Logger'])];
}
}
class IntervalUse implements IUse, IExecutableUse {
public exportBinds(bindFactory: IBindFactory): IBind[] {
return [bindFactory.createBind(Interval, 'Interval')];
}
public execute(container: IContainer): void {
const interval = container.getInstanceByInterface<IInterval>('Interval', 'start');
const service = container.getInstanceByInterface<IService>('Service', 'showTime');
if (service && interval) interval.start(service.showTime.bind(service));
}
}
new Athlete()
.createFramework()
.use(new ClassicLoggerUse())
.use(new ServiceUse())
.use(new IntervalUse())
.start();
В случае смены логгера нужно будет только:
new Athlete()
.createFramework()
.use(new FastLoggerUse())
.use(new ServiceUse())
.use(new IntervalUse())
.start();
Детали:
Entities
IBindFactory['createBind']
в качестве первого аргумента можно передавать любой тип кроме undefined. см SingletonEntityType
При этом для класса дополнительно нужно передаваться массив токенов инстансов, которые он ожидает в конструкторе (при необходимости) и строго в правильном порядке.
Singleton / Factory
при использовании IBindFactory['createBind']
во фреймворке будет храниться единственный экземпляр класса. Если нужно чтобы инстансов класса было много, нужно использовать метод IBindFactory['createFactoryBind']
Вышеописанное справедливо только для классов. Функцииб объекты и примитивы всегда singleton
IUse
Интерфейс для добавления сущностей перед стартом фреймворка
IUse['createBinds]
имеет второй параметр функии с типом IContainer
пока что это бесполезно, так что не тратьте время
IExectableUse Интерфейс для описания поведения, которое необходимо сделать после старта фреймворка. Нужеен так как другой способ запустить слушатель - это прописать запуск в конструктор какого нибудь класса, что делать нехорошо, так как нет уверености что класс инстанциируется посленим
Athlete Единственный класс который предоставляет библиотека. Все остальное - это типы и интерфейсы
JavaScritp полностью поддерживается
FrontEnd Еще не успел попробовать, но в целом специфичного нодовсого там нет. Транспилятор тайпскрипта должен импорты поправлять чтобы везде работало
А где HTTP? фреймворк не httpшный, так что напиши свой модуль - это не сложно :) Свой http модуль я выложу позже