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!
Od kodu spaghetti do kontenerów IoC
By Łukasz Rybka
Od kodu spaghetti do kontenerów IoC
Angular Tricity #8, Gdańsk, 25.06.2019
- 1,315