Od kodu spaghetti

do kontenerów IoC

Angular Tricity #8

Gdańsk, 25.06.2019

Łukasz Rybka

Co-founder / Chief Technology Officer

Trener

Full-stack JavaScript Developer

Mariusz

  • Programista JavaScript
  • Uwielbia TypeScript
  • Od dwóch lat pracuje nad projektem sklepu internetowego dla swojego klienta
  • Uwielbia się uczyć!

Scenariusz #1

Klient prosi o rozszerzenie systemu do zarządzania dostawami o wysłanie powiadomienia SMS do klienta, kiedy tylko pojawi się problem z dostawą i zakupiony przedmiot dotrze do niego z opóźnieniem.

Shipping Controller

import { ShippingService } from './shipping.service';

export class ShippingController {
    private service;

    constructor() {
        this.service = new ShippingService();
    }

    public ship(oderId: number) {
        this.service.ship(oderId);
    }
}

Shipping Service

import { ProductLocatorService } from './product-locator.service';
import { PricingService } from './pricing.service';
import { TrackingService } from './tracking.service';

export class ShippingService {
    private locator: ProductLocatorService;
    private pricing: PricingService;
    private tracking: TrackingService;

    constructor() {
        this.locator = new ProductLocatorService();
        this.pricing = new PricingService();
        this.tracking = new TrackingService();
    }

    public ship(oderId: number): void {
        // Complicated business logic here...
    }
}

Shipping Service

import { ProductLocatorService } from './product-locator.service';
import { PricingService } from './pricing.service';
import { TrackingService } from './tracking.service';
import { SMSService } from './sms.service';

export class ShippingService {
    private locator: ProductLocatorService;
    private pricing: PricingService;
    private tracking: TrackingService;
    private messaging: SMSService;

    constructor() {
        this.locator = new ProductLocatorService();
        this.pricing = new PricingService();
        this.tracking = new TrackingService();
        this.messaging = new SMSService();
    }

    public ship(oderId: number): void {
        // Complicated business logic here...
    }
}

Scenariusz #2

W trakcie kolejnej sesji planowania rozwoju sklepu, klient informuje Mariusza, że otrzymał wiele próśb od klientów, aby zamienić medium powiadomień z SMS-ów na e-mail.

Aktualne problemy

  • Kodu w obecnej formie nie da się (łatwo) testować jednostkowo
  • Z każdą kolejną nową funkcjonalnością jej implementacja zajmuje coraz więcej czasu
  • Zmiana obecnie istniejącej funkcjonalności również staje się problematyczna

Dependency Injection

Polega na przekazywaniu gotowych, utworzonych instancji obiektów udostępniających swoje metody i właściwości obiektom, które z nich korzystają (np. jako parametry konstruktora). Stanowi alternatywę do podejścia, gdzie obiekty tworzą instancję obiektów, z których korzystają np. we własnym konstruktorze.

Shipping Service

import { ProductLocatorService } from './product-locator.service';
import { PricingService } from './pricing.service';
import { TrackingService } from './tracking.service';
import { SMSService } from './sms.service';

export class ShippingService {
    private locator: ProductLocatorService;
    private pricing: PricingService;
    private tracking: TrackingService;
    private messaging: SMSService;

    constructor(_locator: ProductLocatorService, _pricing: PricingService,
        _tracking: TrackingService, _messaging: SMSService) {

        this.locator = _locator;
        this.pricing = _pricing;
        this.tracking = _tracking;
        this.messaging = _messaging;
    }

    public ship(oderId: number): void {
        // Complicated business logic here...
    }
}

Problem

Pomimo zastosowania wzorca Dependency Injection w aplikacji pozostał poważny problem - ShippingService jest zależny od konkretnych typów.

 

Łamie to zasadę "Dependency Inversion Principle" z grupy S.O.L.I.D.

Shipping Service

import { IProductLocator, IPricing, ITracking, IMessaging } from './interfaces';

export class ShippingService {
    private locator: IProductLocator;
    private pricing: IPricing;
    private tracking: ITracking;
    private messaging: IMessaging;

    constructor(_locator: IProductLocator, _pricing: IPricing,
        _tracking: ITracking, _messaging: IMessaging) {
        this.locator = _locator;
        this.pricing = _pricing;
        this.tracking = _tracking;
        this.messaging = _messaging;
    }

    public ship(oderId: number): void {
        // Complicated business logic here...
    }
}

Shipping Controller

 

import { ShippingService } from './shipping.service';
import { IProductLocator, IPricing, ITracking, IMessaging } from './interfaces';
import { ProductLocatorService } from './product-locator.service';
import { PricingService } from './pricing.service';
import { TrackingService } from './tracking.service';
import { SMSService } from './sms.service';

export class ShippingController {
    private service;

    constructor() {
        const locator: IProductLocator = new ProductLocatorService();
        const pricing: IPricing = new PricingService();
        const tracking: ITracking = new TrackingService();
        const messaging: IMessaging = new SMSService();

        this.service = new ShippingService(locator, pricing, tracking, messaging);
    }

    public ship(oderId: number) {
        this.service.ship(oderId);
    }
}

Korzyści

  • Łatwiejsze testowanie jednostkowo
  • Swoboda używania serwisów w wielu miejscach aplikacji
  • Łatwa podmiana implementacji dowolnego z serwisów

Shipping Controller

 

import { ShippingService } from './shipping.service';
import { IProductLocator, IPricing, ITracking, IMessaging } from './interfaces';
import { ProductLocatorService } from './product-locator.service';
import { PricingService } from './pricing.service';
import { TrackingService } from './tracking.service';
import { EmailService } from './email.service';

export class ShippingController {
    private service;

    constructor() {
        const locator: IProductLocator = new ProductLocatorService();
        const pricing: IPricing = new PricingService();
        const tracking: ITracking = new TrackingService();
        const messaging: IMessaging = new EmailService();

        this.service = new ShippingService(locator, pricing, tracking, messaging);
    }

    public ship(oderId: number) {
        this.service.ship(oderId);
    }
}

Scenariusz #3

W związku z rosnącymi kosztami utrzymywania własnej floty serwerów mailowych klient zdecydował zamienić ją na jedno z popularnych na rynku rozwiązań SaaS (Software as a Service).

Scenariusz #3

Nowa infrastruktura projektu wymaga zmian w klasie EmailService oraz jej inicjalizacji. Ponieważ jest ona wykorzystywana w ponad 20 miejscach aplikacji Mariusz zaczął zastanawiać się jak w przyszłości zabezpieczyć się przed takimi sytuacjami.

Inversion of Control

Sterowanie wykonywaniem programu (w naszym przypadku tworzenia obiektów serwisów), zostaje przeniesione z kodu naszej aplikacji do osobnego narzędzia, potocznie nazywanego kontenerem. Dla Mariusza oznacza to, że zamiast tworzyć w różnych miejscach aplikacji obiekty serwisów, będzie on mógł “sięgnąć” do takiego kontenera i “wyjąć z niego” potrzebny mu serwis.

TypeDI

TypeDI is a dependency injection tool for JavaScript and TypeScript. Using TypeDI you can build well-structured and easily tested applications.

TypeDI - Krok #1

import "reflect-metadata";
import { Service } from "typedi";

import { IMessaging } from "./interfaces";

@Service()
export class EmailService implements IMessaging {
    // Complicated business logic here...
}

TypeDI - Krok #2

import "reflect-metadata";
import { Inject, Service } from "typedi";

import { IProductLocator, IPricing, ITracking, IMessaging } from './interfaces';

@Service()
export class ShippingService {
    @Inject() locator: IProductLocator;
    @Inject() pricing: IPricing;
    @Inject() tracking: ITracking;
    @Inject() messaging: IMessaging;

    public ship(oderId: number): void {
        // Complicated business logic here...
    }
}

TypeDI - Krok #3

import "reflect-metadata";
import { Inject } from "typedi";

import { ShippingService } from './shipping.service';

export class ShippingController {
    @Inject() service: ShippingService;

    public ship(oderId: number) {
        this.service.ship(oderId);
    }
}

Prostota

Mając tak zaadaptowany mechanizm Inversion of Control, Mariusz nie musi się już nigdy więcej zastanawiać, jak zainicjalizować daną klasę, którą potrzebuje wykorzystać - jest ona rozwiązana na poziomie rejestracji klasy w kontenerze IoC (wykorzystanie dekoratora @Service).

Podsumowanie

Przemyślenia Mariusza, gdy po kolejnym wdrożeniu wyjechał na urlop...

Wniosek #1

Zasada SOLID gromadzi szereg praktycznych zasad pomagających w przyszłości łatwiej i szybciej pracować nad rozwojem aplikacji.

Wniosek #2

Stosowanie zasady “open/closed” pozwala na tworzenie bardziej elastycznych aplikacji, w których wprowadzanie zmian jest znacznie prostsze.

Wniosek #3

Dzięki wzorcowi Dependency Injection można łatwiej testować jednostkowo aplikację, łatwiej dokonywać w niej zmian oraz jaśniejsze jest, jakie zależności mają jaki fragment naszego programu (ponieważ te zależności są przekazywane, a nie tworzone).

Wniosek #4

Wzorzec Inversion of Control, chociaż z początku jest trudny do zrozumienia, okazuje się z czasem bardzo przydatny i w znaczny sposób upraszcza zarządzanie powiązaniami w aplikacji.

Co dalej?

Named services

import { Container, Service, Inject } from "typedi";

interface Factory {
    create(): void;
}

@Service("bean.factory")
class BeanFactory implements Factory {
    create() {}
}

@Service("coffee.maker")
class CoffeeMaker {
    @Inject("water.factory") waterFactory: Factory;
    @Inject("bean.factory") beanFactory: Factory;

    make() {
        this.beanFactory.create();
        this.waterFactory.create();
    }
}

Settings injection

import { Container, Service, Inject } from "typedi";

Container.set("authorization-token", "RVT9rVjSVN");

@Service()
class UserRepository {
    @Inject("authorization-token")
    authorizationToken: string;
}

Factory functions

import { Container, Service } from "typedi";

function createCar() {
    return new Car("V8");
}

@Service({ factory: createCar })
class Car {
    constructor (public engineType: string) {
    }
}

Service groups

export interface Factory {
    create(): any;
}

export const FactoryToken = new Token<Factory>("factories");

@Service({ id: FactoryToken, multiple: true })
export class BeanFactory implements Factory {
    create() { console.log("bean created"); }
}

@Service({ id: FactoryToken, multiple: true })
export class WaterFactory implements Factory {
    create() { console.log("water created"); }
}

Container.import([ BeanFactory, WaterFactory ]);

const factories: Factory[] = Container.getMany(FactoryToken);

factories.forEach(factory => factory.create());

Thank you!