Typed Constructors

In Java ist es vorgesehen Abstraktionen mittels Interfaces vorzunehmen.
Schon in einer der ersten Vorlesungen zum Thema Java wird einem dieses beigebracht.
Manchmal jedoch kann es auch zu etwas unsauberen Quelltext führen.
In meinen Projekten sehen ich immer mal wieder, das interne Implementierungsklassen zwei
oder mehr Interfaces eines API implementieren. Das an sich ist nichts schlimmes,
nur auf der anderen Seite führt es dann zu folgenden Konstrukten.

Gehen wir im Folgenden davon aus, das wir zwei Interfaces (ServiceA und ServiceB) haben.
(siehe nachfolgendes Listing)

public interface Service_A {
  String doWork_A();
}

public interface Service_B {
  String doWork_B();
}

In der Implementierung des Frameworks gibt es nun eine Klasse ServiceInternImpl
die diese beiden Interfaces implementiert.

public class ServiceImplInternal implements Service_A, Service_B {
  @Override
  public String doWork_A() {
    return "A";
  }

  @Override
  public String doWork_B() {
    return "B";
  }
}

Innerhalb des Frameworks ist das weiterhin nicht von Bedeutung.
An gegebener Stelle wird intern nun diese Implementierung verwendet.
Nur ausserhalb möchte man nicht auf diese Implementierung expliziet casten,
da nicht sichergestellt ist, dass diese Implementierung auf Dauer so bestehen bleibt.

Es gibt also kein Interface das von beiden Interfaces erbt und damit diese zusammenführt.
Nun soll es eine Methode geben, die einen Datenholder erzeugt,
basierend auf Methodenrückgabewerten von Methoden beider Interfaces.

public static class DataHolder_A {
    private String a; //doWork_A()
    private String b; //doWork_B()

    public DataHolder_A(final String a, final String b) {
      this.a = a;
      this.b = b;
    }
  }

Das führt dann zu Transformationen wie z.B.

final List<Service_A> service_A_list = getServices(); //auf ein Interface reduziert

final List<DataHolder_A> list = service_A_list.stream()
        .map(a -> {
          final Service_B b = (Service_B) a;
          return new DataHolder_A(a.doWork_A(), b.doWork_B());
        })
        .collect(Collectors.toList());

Das ist nicht sonderlich elegant, was also kann man nun machen ?
Der erste Schritt ist, das man z.B. die Liste die man von dem Framework bekommt
transformiert. Das kann dann als eine map() - Stufe bei
der Streamverarbeitung realisiert werden.

    final List<DataHolder_A> collect = service_A_list
        .stream()
        .map(e -> (Service_A & Service_B) e)
        .map(e -> new DataHolder_A(e.doWork_A(), e.doWork_B()))
        .collect(Collectors.toList());

Hier kann man ausnutzen, dass man in einem Schritt auf eine beliebige Anzahl
Interfaces casten kann. e -> (Service_A & Service_B) e
Aber warum einen Cast durchführen?
Sehen wir uns mal den DataHolder_A an.
Hier ist das Ziel, die Ergebnisse beide Methoden zu halten.
Von Methoden ist bekannt, das man den Typ T in der Methode selbst deklarieren kann.
Wenden wir dieses auf unsere Transformation von eben an,
so bekommen wir eine Methode mit der folgenden Signatur.

  private <T extends Service_A & Service_B> List<T> transform(List<Service_A> services) {
    return services.stream()
        .map(e -> (Service_A & Service_B) e)
        .map(value -> (T) value)
        .collect(Collectors.toList());
}

Wichtig an der Stelle ist <T extends Service_A & Service_B> List<T>.

Nun ist es nur noch ein kleiner Schritt.
Wenn man nun in diesem Fall einen Konstruktor wie eine Methode versteht,
kann man auch hier ein T definieren. Der Vorteil ist, das es nicht auf
Klassenebene passieren muss.

Ändern wir nun unseren DataHolder wie folgt.

public class DataHolder_B {

    private String a;
    private String b;

    public <T extends Service_A & Service_B> DataHolder_B(T input) {
      this.a = input.doWork_A();
      this.b = input.doWork_B();
    }
  }

Nun kann man die Transformation recht einfach formulieren.

    final List<Service_A> service_A_list = getServices(); //auf ein Interface reduziert   
    final List<DataHolder_B> collect = service_A_list
        .stream()
        .map(e -> (Service_A & Service_B) e)
        .map(DataHolder_B::new)
        .collect(Collectors.toList());

Wenn man das nun ein wenig formaler formuliert bekommen wir das nachfolgende Beispiel. Hiermit ist auch sichergestellt,
das wir nie eine ungültige Kombination verwenden, bzw Implementierungsklassen
ausserhalb des Frameworks sichtbar werden.
Sollte sich die Implementierung innerhalb des Frameworks in der Zusammensetzung der Interfaces verändern,
wird dieses direkt beim kompilieren sichtbar.

public class Main {
  interface Service_A { String doWork_A(); }
  interface Service_B { String doWork_B(); }
  interface Service_C { String doWork_C(); }
  interface Service_D { String doWork_D(); }

  public static class Impl_A implements Service_A , Service_B{
    public String doWork_A() { return "A"; }
    public String doWork_B() { return "B"; }
  }
  public static class Impl_B implements Service_C , Service_D{
    public String doWork_C() { return "C";}
    public String doWork_D() { return "D";}
  }

  public static class DataHolder{
    String a;
    String b;

    public DataHolder(final String a, final String b) {
      this.a = a;
      this.b = b;
    }

    public <T extends Service_A & Service_B> DataHolder(T value) {
      this.a = value.doWork_A();
      this.b = value.doWork_B();
    }
    public <T extends Service_C & Service_D> DataHolder(T value) {
      this.a = value.doWork_C();
      this.b = value.doWork_D();
    }
  }

  public static void main(String[] args) {
    new DataHolder("A","B");
    new DataHolder(new Impl_A());
    new DataHolder(new Impl_B());
  } 
}

Aber manchmal trifft man auch Fälle an, in denen es zu einem Interface mehrere Implementierungen gibt. Dann kommt man manchmal in folgende Situation.

  public interface Service_A {
    String doWork_A();
  }

  public static class Service_A_Impl_A implements Service_A {
    @Override
    public String doWork_A() {
      return null;
    }
  }

  public static class Service_A_Impl_B implements Service_A {
    @Override
    public String doWork_A() {
      return null;
    }
  }


  public interface Service_B {
    String doWork_B();
  }

  public static class Service_B_Impl_A implements Service_B {
    @Override
    public String doWork_B() {
      return null;
    }
  }

  public static class Service_B_Impl_B implements Service_B {
    @Override
    public String doWork_B() {
      return null;
    }
  }

Nun ist es leider so, dass die Kombinationen der Ergebnisse nur fachlich dann richtig sind wenn jeweils die A Implementierung oder B Implementierung von beiden genommen worden ist.

(ja soetwas gibt es wirklich ;-) )

D.h. es gibt gültige und ungültige Kombinationen der Implementierungen.

//the only valid combinations
  // Service_A_Impl_A && Service_B_Impl_A
  // Service_A_Impl_B && Service_B_Impl_B

  // not allowed
  // Service_A_Impl_A && Service_B_Impl_B
  // Service_A_Impl_B && Service_B_Impl_A

Das wiederum führte zu Konstruktionen wir nachfolgend aufgelistet.

//not nice
  public static class DataHolder_AB {
    private String a;
    private String b;

    //not secure
    public DataHolder_AB(final Service_A service_a, final Service_B service_b) {
      a = service_a.doWork_A();
      b = service_b.doWork_B();
    }

    //not secure
    public DataHolder_AB(String a, String b) {
      this.a = a;
      this.b = b;
    }

    //not nice
    public DataHolder_AB(final Service_A_Impl_A service_a, final Service_B_Impl_A service_b) {
      a = service_a.doWork_A();
      b = service_b.doWork_B();
    }

    //not nice
    public DataHolder_AB(final Service_A_Impl_B service_a, final Service_B_Impl_B service_b) {
      a = service_a.doWork_A();
      b = service_b.doWork_B();
    }
  }

Wenn man nun damit konfrontiert ist und es nicht erlaubt ist die Klasse DataHolder selbst

zu typisieren. Also kein DataHolder<...> erlaubt ist. Dann kann man evtl auch hier mit Typed Constructors

helfen.

  //no generics on class level
  public static class DataHolder {

    //not secure
    //public <A extends Service_A, B extends Service_B> DataHolder(A serviceA, B serviceB) {}

    //ok
    public <A extends Service_A_Impl_A, B extends Service_B_Impl_A> DataHolder(A serviceA, B serviceB) {}
    public <A extends Service_A_Impl_B, B extends Service_B_Impl_B> DataHolder(A serviceA, B serviceB) {}
  }

In der Verwendung sind dann nur Kombinationen erlaubt die hier explizit als Konstruktor vorgegeben sind.

Die Schreibweise hier ist wesentlich kürzer und übersichtlicher wenn man es mit mehreren Kombinationen zu tun hat.

Fazit

Wenn man mit alten und stark gewachsenen Software-Systemen zu "kämpfen" hat kommen einem die merkwürdigsten Dinge entgegen. Leider kann man nicht immer die gesamte Architektur verbessern, auch wenn man es als sehr sinnvoll und als gute Investition aus technischer Sicht so sehen möge.

In solchen Fällen kann man aber mit kleinen Verbesserungen manchmal doch dafür sorgen das einiges in die statische Semantik kommt und einem der Compiler hilft (fachliche) Fehler zu finden.

Die Sourcen sind unter https://github.com/Java-Publications/jaxenter.de-0028-CoreJava-TypedConstructor zu finden.

Happy Coding

results matching ""

    No results matching ""