0.0.14 • Published 10 months ago

gt9p5kz-lib v0.0.14

Weekly downloads
-
License
ISC
Repository
-
Last release
10 months ago

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

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 модуль я выложу позже

0.0.14

10 months ago

0.0.13

10 months ago

0.0.12

10 months ago

0.0.11

10 months ago

0.0.10

10 months ago

0.0.9

10 months ago

0.0.8

10 months ago

0.0.7

10 months ago

0.0.6

10 months ago

0.0.5

10 months ago

0.0.4

10 months ago

0.0.3

10 months ago

0.0.2

10 months ago

0.0.1

10 months ago