Dependency Injection – A step to orthogonal code

Posted by | No Tags | Egy csésze kávé · Szoftverfejlesztés | Nincs hozzászólás a(z) Dependency Injection – A step to orthogonal code bejegyzéshez

Szoftverfejlesztés során nem elég egy meghatározott feladat megoldására alkalmas programsorokat előállítanunk, hanem szem előtt kell tartanunk az egyszerű karbantartás és továbbfejlesztés lehetőségét is. Az ilyen feltételeknek megfelelő szoftvereket ortogonális szoftvereknek nevezzük.

Az ortogonális kód írásának egyik alapelve a laza csatolás (loose coupling). A dependency injection a laza csatolást segíti elő.

 

 

Egyszerűen megfogalmazva a dependency injection azt jelenti, hogy egy osztály függőségeinek létrehozását és azok konfigurálását leválasztjuk az osztályról.

 

Egy példán keresztül szeretném bemutatni, hogy a DI használata milyen előnyökkel jár. A példa legyen a legközelebbi pontpár probléma: adott n darab pont a metrikus térben, keressük meg azt a két pontot, amelyek távolsága a legkisebb.

 

A feladat megoldására válasszuk a brute-force algoritmust: kiszámoljuk az összes pont közötti távolságot, majd kiválasztjuk azt a párost, amelynek a legkisebb távolsága.

Egy pontot reprezentáló osztály:

public class Point {

    private final double[] coordinates;

    public Point(double[] coordinates) {
        this.coordinates = coordinates;
    }

    public double[] getCoordinates() {
        return coordinates;
    }
}

A távolság számításához használjuk az euklideszi távolságot:

public class EuclideanMetric {

    public double metric(double x[], double y[]) {
        if (x.length != y.length) {
            throw new IllegalArgumentException("Different dimensions!");
        }
        
        double metric = 0.0;
        for (int i = 0; i < x.length; i++) {
            double diff = x[i] - y[i];
            metric += diff * diff;
        }
        
        metric = Math.sqrt(metric);
        
        return metric;
    }
}

És a brute-force algoritmus:

public class ClosestPairAlgorithm {
    
    private final EuclideanMetric metric = new EuclideanMetric();
    
    public ClosestPair calculate(List<Point> points) {
        ClosestPair closestPair = null;
        
        double minDist = Double.POSITIVE_INFINITY;
        for (int i = 0; i < points.size() - 1; i++) {
            final Point p = points.get(i);
            for (int j = i + 1; j < points.size(); j++) {
                final Point q = points.get(j);
                final double dist = dist(p, q);
                if (dist < minDist) {
                    minDist = dist;
                    closestPair = new ClosestPair(p, q);
                }
            }
        }
        
        return closestPair;
    }

    private double dist(Point p, Point q) {
        return metric.metric(p.getCoordinates(), q.getCoordinates());
    }

    public static class ClosestPair {
        private Point a, b;

        public ClosestPair(Point a, Point b) {
            this.a = a;
            this.b = b;
        }

        public Point getA() {
            return a;
        }

        public Point getB() {
            return b;
        }
    }
}

 

Ezzel a megoldással a feladatot teljesen jól megoldottuk, pontosan azt teszi, amit várunk tőle: n dimenziós pontok közül euklideszi távolságban a két legközelebbit kiválasztja.
Probléma csak akkor merül fel, ha más távolság mérést szeretnénk használni. Jelenleg nincs lehetőségünk arra, hogy a ClosestPairAlgorithm osztálynak megadjuk a távolság számításra felhasznált algoritmust. Ennek két oka van:

  • A távolság számítást megvalósító EuclideanMetric osztályt a ClosestPairAlgorithm saját maga példányosítja.
  • Nem adtunk lehetőséget arra, hogy az EuclideanMetric mellet más távolság számító osztályok is létezhessenek.

 

Az első probléma megoldható, ha nem ClosestPairAlgorithm végzi az EuclideanMetric példányosítását, hanem konstruktorban várja, egy felsőbb szintre delegálja ezt a feladatot. Ezt mondja ki a DI.

public class ClosestPairAlgorithm {
    
    private final EuclideanMetric metric;

    public ClosestPairAlgorithm(EuclideanMetric metric) {
        this.metric = metric;
    }
// ...
}

A második probléma megoldható, ha követjük a program to interface not implementation elvet és készítünk egy Metric nevű interface-t:

public interface Metric {

    double metric(double x[], double y[]);
}
public class EuclideanMetric implements Metric {

    @Override
    public double metric(double x[], double y[]) {
        // ...
    }
}
public class ClosestPairAlgorithm {
    
    private final Metric metric;

    public ClosestPairAlgorithm(Metric metric) {
        this.metric = metric;
    }
// ...
}

Így máris újrafelhasználható kódot kaptunk.

A DI előnyei

A szemléltető példát követően nézzük végig milyen előnyeink származhatnak a dependency injection-ből:

Csökkenti az osztályok közötti csatolást

Azáltal, hogy nem az osztály gondoskodik függőségeinek példányosításáról a szoftverünk osztályai lazábban lesznek csatoltak.

Növeli a kód újrafelhasználhatóságot

Mivel az osztályunk a függőségeinek konfigurálásától is mentes, így különféle helyzetekben másként tudjuk felhasználni.

Növeli a kód karbantarthatóságát

Az osztály függőségei könnyedén felfedezhetőek, ezáltal áttekinthetőbb, olvashatóbb kódot kapunk.

Elősegíti a tesztelést

A függőségek így a tesztekben könnyen mockolhatókká válnak, s a teszt függetlenedhet az osztály függőségeinek működésétől (a függőségek implementációjában történő változtatások, nem vezetnek az osztályunk tesztjének bukásához, lokalizáltá válik a teszt).

CDI (Contexts and Dependency Injection) és Mockito

Nem szeretnék ezen blogban a CDI-vel foglalkozni, de úgy érzem muszáj említést tennem róla. A CDI nagyon leegyszerűsítve: egy annotáció (@Inject) segítségével, az osztályokban definiált függőségek (az annotációval ellátottak) injektálását elvégzi.

public class MyClass {
  @Inject
  private MyDep1 md1;
  @Inject
  pirvate MyDep2 md2;

  public MyClass() {
  }

}

Tesztek írása során nagyon szépnek tartom, hogy a függőségek kimockolására a mockito felhasználásával olyan teszt osztályt kapunk, amely egy az egyben mása a tesztelendő osztálynak: szépen feltüntetve megadhatóak a függőségek.

@RunWith(MockitoJUnitRunner.class)
public class MyClassTest {
  @Mock
  private MyDep1 md1;
  @Mock
  private MyDep2 md2;

  @InjectMocks
  private MyClass mc;

}

No Comments

Leave a comment