Angular Tricity #8
Gdańsk, 25.06.2019
Co-founder / Chief Technology Officer
Trener
Full-stack JavaScript Developer
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.
import { ShippingService } from './shipping.service';
export class ShippingController {
private service;
constructor() {
this.service = new ShippingService();
}
public ship(oderId: number) {
this.service.ship(oderId);
}
}
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...
}
}
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...
}
}
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.
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.
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...
}
}
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.
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...
}
}
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);
}
}
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);
}
}
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).
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.
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 is a dependency injection tool for JavaScript and TypeScript. Using TypeDI you can build well-structured and easily tested applications.
import "reflect-metadata";
import { Service } from "typedi";
import { IMessaging } from "./interfaces";
@Service()
export class EmailService implements IMessaging {
// Complicated business logic here...
}
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...
}
}
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);
}
}
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).
Przemyślenia Mariusza, gdy po kolejnym wdrożeniu wyjechał na urlop...
Zasada SOLID gromadzi szereg praktycznych zasad pomagających w przyszłości łatwiej i szybciej pracować nad rozwojem aplikacji.
Stosowanie zasady “open/closed” pozwala na tworzenie bardziej elastycznych aplikacji, w których wprowadzanie zmian jest znacznie prostsze.
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).
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.
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();
}
}
import { Container, Service, Inject } from "typedi";
Container.set("authorization-token", "RVT9rVjSVN");
@Service()
class UserRepository {
@Inject("authorization-token")
authorizationToken: string;
}
import { Container, Service } from "typedi";
function createCar() {
return new Car("V8");
}
@Service({ factory: createCar })
class Car {
constructor (public engineType: string) {
}
}
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());