0.0.14 • Published 2 years ago

gt9p5kz-lib v0.0.14

Weekly downloads
-
License
ISC
Repository
-
Last release
2 years 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

2 years ago

0.0.13

2 years ago

0.0.12

2 years ago

0.0.11

2 years ago

0.0.10

2 years ago

0.0.9

2 years ago

0.0.8

2 years ago

0.0.7

2 years ago

0.0.6

2 years ago

0.0.5

2 years ago

0.0.4

2 years ago

0.0.3

2 years ago

0.0.2

2 years ago

0.0.1

2 years ago