Umetanje zavisnosti - Dependency Injection

Prof. dr Igor Dejanović (igord at uns ac rs)

Kreirano 2019-11-09 Sat 09:21, pritisni ESC za mapu, m za meni, Ctrl+Shift+F za pretragu

Sadržaj

1 Motivacija

1.1 Motivacija

  • Objekti iole složenijih aplikacija formiraju složene grafove zavisnosti.
  • Kako objekat "dobija" reference na zavisne objekte?

1.2 Motivacija

Motivation.png

1.3 Klasičan pristup dobavljanja referenci

public class RealBillingService implements BillingService {

  @Override
  public Receipt chargeOrder(PizzaOrder order, CreditCard creditCard) {
      CreditCardProcessor processor = new PaypalCreditCardProcessor();
      TransactionLog transactionLog = new DatabaseTransactionLog();

      try {
        ChargeResult result = processor.charge(creditCard, order.getAmount());
        transactionLog.logChargeResult(result);

        return result.wasSuccessful()
            ? Receipt.forSuccessfulCharge(order.getAmount())
            : Receipt.forDeclinedCharge(result.getDeclineMessage());
       } catch (UnreachableException e) {
        transactionLog.logConnectException(e);
        return Receipt.forSystemFailure(e.getMessage());
      }
  }
}

1.4 Upotreba Singleton/Factory obrasca

Objekat se sam brine o dobavljanju referenci ali to čini posredstvom globalne deljene reference.

public class RealBillingService implements BillingService {

  public Receipt chargeOrder(PizzaOrder order, CreditCard creditCard) {
    CreditCardProcessor processor = CreditCardProcessorFactory.getInstance();
    TransactionLog transactionLog = TransactionLogFactory.getInstance();

    try {
      ChargeResult result = processor.charge(creditCard, order.getAmount());
      transactionLog.logChargeResult(result);

      return result.wasSuccessful() ?
            Receipt.forSuccessfulCharge(order.getAmount()) :
            Receipt.forDeclinedCharge(result.getDeclineMessage());
    } catch (UnreachableException e) {
      transactionLog.logConnectException(e);
      return Receipt.forSystemFailure(e.getMessage());
    }
  }
}

1.5 Singleton/Factory - testiranje

public class RealBillingServiceTest extends TestCase {

  private final PizzaOrder order = new PizzaOrder(100);
  private final CreditCard creditCard = new CreditCard(5000);
  private final InMemoryTransactionLog transactionLog = new InMemoryTransactionLog();
  private final FakeCreditCardProcessor creditCardProcessor = new FakeCreditCardProcessor();

  @Override
  public void setUp() {
    TransactionLogFactory.setInstance(transactionLog);
    CreditCardProcessorFactory.setInstance(creditCardProcessor);
  }

  @Override
  public void tearDown() {
    TransactionLogFactory.setInstance(null);
    CreditCardProcessorFactory.setInstance(null);
  }

  public void testSuccessfulCharge() {
    RealBillingService billingService = new RealBillingService();
    Receipt receipt = billingService.chargeOrder(order, creditCard);

    assertTrue(receipt.hasSuccessfulCharge());
    assertEquals(100.0, receipt.getAmount(), 0.001);
    assertEquals(creditCard, creditCardProcessor.getCardOfOnlyCharge());
    assertEquals(100.0, creditCardProcessor.getAmountOfOnlyCharge(), 0.001);
    assertTrue(transactionLog.wasSuccessLogged());
  }
}

1.6 Upotreba Singleton/Factory obrasca - problemi

  • Deljena referenca - moramo posebno da pazimo da je postavljamo na prave vrednosti.
  • Nemoguće paralelizovati testove.

2 Dependency Injection

2.1 Umetanje zavisnosti - Dependency Injection

  • Izmeštanje nadležnosti za dobavljanje referenci van objekta - neko drugi će se brinuti da "umetne" reference pre njihove upotrebe.
  • Prednosti:
    • Kod se pojednostavljuje. Zavisnost između klasa je bazirana na apstraktnim interfejsima što pozitivno utiče na održavanje (maintability), ponovnu iskoristljivost (reusability) i podelu posla i nadležnosti.
    • Objekat će do trenutka poziva njegovih servisnih metoda već biti na odgovarajući način inicijalizovan. Smanjuje se tzv. boilerplate kod.
    • Testiranje je daleko jednostavnije. Kreiranje "lažnih" objekata (mockup) je moguće i jednostavno se izvodi. Moguća paralelizacija testova.

2.2 Mehanizmi umetanja zavisnosti

  • Putem parametara konstruktora.
  • Putem mutator metoda (setters).
  • Putem implementiranog interfejsa.

2.3 Injekcija putem parametara konstruktora

Client(Service service) {
    this.service = service;
}

2.4 Injekcija putem setter metoda

public void setService(Service service) {
    this.service = service;
}

2.5 Injekcija putem interfejsa

public interface ServiceSetter {
    public void setService(Service service);
}
public class client implements ServiceSetter {

    private Service service;

    @Override
    public void setService(Service service) {
        this.service = service;
    }
}

2.6 Upotreba DI

public class RealBillingService implements BillingService {
  private final CreditCardProcessor processor;
  private final TransactionLog transactionLog;

  public RealBillingService(CreditCardProcessor processor,
      TransactionLog transactionLog) {
    this.processor = processor;
    this.transactionLog = transactionLog;
  }

  public Receipt chargeOrder(PizzaOrder order, CreditCard creditCard) {
    try {
      ChargeResult result = processor.charge(creditCard, order.getAmount());
      transactionLog.logChargeResult(result);

      return result.wasSuccessful() ?
           Receipt.forSuccessfulCharge(order.getAmount()) :
           Receipt.forDeclinedCharge(result.getDeclineMessage());
    } catch (UnreachableException e) {
      transactionLog.logConnectException(e);
      return Receipt.forSystemFailure(e.getMessage());
    }
  }
}

2.7 Upotreba DI - testiranje

public class RealBillingServiceTest extends TestCase {

  private final PizzaOrder order = new PizzaOrder(100);
  private final CreditCard creditCard = new CreditCard(5000);

  private final InMemoryTransactionLog transactionLog =
                                             new InMemoryTransactionLog();
  private final FakeCreditCardProcessor creditCardProcessor =
                                             new FakeCreditCardProcessor();

  public void testSuccessfulCharge() {
    RealBillingService billingService = new RealBillingService(
        creditCardProcessor, transactionLog);
    Receipt receipt = billingService.chargeOrder(order, creditCard);

    assertTrue(receipt.hasSuccessfulCharge());
    assertEquals(100.0, receipt.getAmount(), 0.001);
    assertEquals(creditCard, creditCardProcessor.getCardOfOnlyCharge());
    assertEquals(100.0, creditCardProcessor.getAmountOfOnlyCharge(), 0.001);
    assertTrue(transactionLog.wasSuccessLogged());
  }
}

2.8 DI kontejneri

  • DI se može implementirati i bez posebnog alata/okvira.
  • DI kontejneri omogućavaju nametanje određenih konvencija za primenu ovog obrasca.
  • Korišćenje DI kontejera donosi određene prednosti:
    • Upotreba najbolje prakse
    • Standardizacija

2.9 DI kontejneri za Javu

  • Google Guice
  • PicoContainer
  • Spring

2.10 Standardizacija za programski jezik Java

  • JSR-3301
  • Definiše skup standardnih Java anotacija za DI:
    • Provider<T> - Provides instances of T
    • Inject - Identifies injectable constructors, methods, and fields.
    • Named - String-based qualifier.
    • Qualifier - Identifies qualifier annotations.
    • Scope - Identifies scope annotations.
    • Singleton - Identifies a type that the injector only instantiates once.

3 Google Guice

3.1 Google Guice

  • Lightweight okvir za DI u Javi.
  • Razvijen od strane Google-a.
  • Konfiguracija bazirana na Java anotacijama.

3.2 Injekcija putem konstruktora

public class RealBillingService implements BillingService {
  private final CreditCardProcessor processor;
  private final TransactionLog transactionLog;

  @Inject
  public RealBillingService(CreditCardProcessor processor,
      TransactionLog transactionLog) {
    this.processor = processor;
    this.transactionLog = transactionLog;
  }

  public Receipt chargeOrder(PizzaOrder order, CreditCard creditCard) {
    try {
      ChargeResult result = processor.charge(creditCard, order.getAmount());
      transactionLog.logChargeResult(result);

      return result.wasSuccessful()
          ? Receipt.forSuccessfulCharge(order.getAmount())
          : Receipt.forDeclinedCharge(result.getDeclineMessage());
     } catch (UnreachableException e) {
      transactionLog.logConnectException(e);
      return Receipt.forSystemFailure(e.getMessage());
    }
  }
}

3.3 Konfiguracija za povezivanje - binding/wiring

public class BillingModule extends AbstractModule {
  @Override
  protected void configure() {
    bind(TransactionLog.class).to(DatabaseTransactionLog.class);
    bind(CreditCardProcessor.class).to(PaypalCreditCardProcessor.class);
    bind(BillingService.class).to(RealBillingService.class);
  }
}

3.4 Upotreba kontejnera

public static void main(String[] args) {
  Injector injector = Guice.createInjector(new BillingModule());
  BillingService billingService = injector.getInstance(BillingService.class);
  Receipt result = billingService.chargeOrder(new PizzaOrder(100),
                                              new CreditCard(500));
  System.out.println(result.hasSuccessfulCharge());
}

3.5 Linked Bindings

public class BillingModule extends AbstractModule {
  @Override
  protected void configure() {
    bind(TransactionLog.class).to(DatabaseTransactionLog.class);
    bind(DatabaseTransactionLog.class).to(MySqlDatabaseTransactionLog.class);
  }
}

3.6 Custom Bindings Annotations

package example.pizza;
import com.google.inject.BindingAnnotation;
import java.lang.annotation.Target;
import java.lang.annotation.Retention;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;

@BindingAnnotation @Target({ FIELD, PARAMETER, METHOD }) @Retention(RUNTIME)
public @interface PayPal {}
...
public class RealBillingService implements BillingService {

  @Inject
  public RealBillingService(@PayPal CreditCardProcessor processor,
      TransactionLog transactionLog) {
    ...
  }
...
    bind(CreditCardProcessor.class)
        .annotatedWith(PayPal.class)
        .to(PayPalCreditCardProcessor.class);

3.7 @Named Binding Annotation

public class RealBillingService implements BillingService {

  @Inject
  public RealBillingService(@Named("Checkout") CreditCardProcessor processor,
      TransactionLog transactionLog) {
    ...
  }
 ...
 ...
    bind(CreditCardProcessor.class)
        .annotatedWith(Names.named("Checkout"))
        .to(CheckoutCreditCardProcessor.class);

3.8 Instance Bindings

bind(String.class)
    .annotatedWith(Names.named("JDBC URL"))
    .toInstance("jdbc:mysql://localhost/pizza");
bind(Integer.class)
    .annotatedWith(Names.named("login timeout seconds"))
    .toInstance(10);

3.9 @Provides Methods

public class BillingModule extends AbstractModule {
  @Override
  protected void configure() {
    ...
  }

  @Provides
  TransactionLog provideTransactionLog() {
    DatabaseTransactionLog transactionLog = new DatabaseTransactionLog();
    transactionLog.setJdbcUrl("jdbc:mysql://localhost/pizza");
    transactionLog.setThreadPoolSize(30);
    return transactionLog;
  }
}
...
  @Provides @PayPal
  CreditCardProcessor providePayPalCreditCardProcessor(
      @Named("PayPal API key") String apiKey) {
    PayPalCreditCardProcessor processor = new PayPalCreditCardProcessor();
    processor.setApiKey(apiKey);
    return processor;
  }

3.10 Provider Bindings

public class DatabaseTransactionLogProvider 
      implements Provider<TransactionLog> {
  private final Connection connection;

  @Inject
  public DatabaseTransactionLogProvider(Connection connection) {
    this.connection = connection;
  }

  public TransactionLog get() {
    DatabaseTransactionLog transactionLog = new DatabaseTransactionLog();
    transactionLog.setConnection(connection);
    return transactionLog;
  }
}
...
public class BillingModule extends AbstractModule {
  @Override
  protected void configure() {
    bind(TransactionLog.class)
        .toProvider(DatabaseTransactionLogProvider.class);
  }
}

3.11 Scopes

@Singleton
public class InMemoryTransactionLog implements TransactionLog {
  /* everything here should be threadsafe! */
}
...
bind(TransactionLog.class)
  .to(InMemoryTransactionLog.class).in(Singleton.class);
...
@Provides @Singleton
TransactionLog provideTransactionLog() {
  ...
}
...
bind(Bar.class).to(Applebees.class).in(Singleton.class);
bind(Grill.class).to(Applebees.class).in(Singleton.class);

4 Injector

4.1 Injector

4.2 Jednostavan primer

>>> from injector import Injector, inject
>>> class Inner(object):
...     def __init__(self):
...         self.forty_two = 42
...
>>> class Outer(object):
...     @inject
...     def __init__(self, inner: Inner):
...         self.inner = inner
...
>>> injector = Injector()
>>> outer = injector.get(Outer)
>>> outer.inner.forty_two
42

4.3 Složeniji primer

from injector import Key
Name = Key('name')
Description = Key('description')
from injector import inject, provider, Module

class User(object):
    @inject
    def __init__(self, name: Name, description: Description):
        self.name = name
        self.description = description


class UserModule(Module):
    def configure(self, binder):
       binder.bind(User)


class UserAttributeModule(Module):
    def configure(self, binder):
        binder.bind(Name, to='Sherlock')

    @provider
    def describe(self, name: Name) -> Description:
        return '%s is a man of astounding insight' % name

4.4 Složeniji primer

from injector import Injector
injector = Injector([UserModule(), UserAttributeModule()])

ili

injector = Injector([UserModule, UserAttributeModule])

Upotreba:

>>> injector.get(Name)
'Sherlock'
>>> injector.get(Description)
'Sherlock is a man of astounding insight'
>>> user = injector.get(User)
>>> isinstance(user, User)
True
>>> user.name
'Sherlock'
>>> user.description
'Sherlock is a man of astounding insight'

5 Flask injector

5.1 Flask injector

  • Veza između injector biblioteke i Flask okvira za razvoj.

5.2 Primer upotrebe

import sqlite3
from flask import Flask, Config
from flask.views import View
from flask_injector import FlaskInjector
from injector import inject

app = Flask(__name__)

@app.route("/bar")
def bar():
    return render("bar.html")

@app.route("/foo")
@inject(db=sqlite3.Connection)
def foo(db):
    users = db.execute('SELECT * FROM users').all()
    return render("foo.html")

def configure(binder):
    binder.bind(
        sqlite3.Connection,
        to=sqlite3.Connection(':memory:'),
        scope=request,
    )

FlaskInjector(app=app, modules=[configure])

app.run()

6 Reference