Implementacja wzorca Dekorator w Javie

Ostatnimi czasy wzorzec Dekorator przydał nam się w sytuacji, w której nie chcieliśmy modyfikować widoku jednak chcieliśmy aby dane w pewnych przypadkach były inaczej prezentowane.

Zaimplementowaliśmy ten wzorzec w najprostszy możliwy sposób:

class Coffee {
  private final String name;

  public Coffee(String name) {
    this.name = name;
  }

  public int price() {
    return 4;
  }

  public String name() {
    return name;
  }
}

class Milk extends Coffee {
  private final Coffee coffee;

  public Milk(Coffee coffee) {
    super("");
    this.coffee = coffee;
  }

  @Override
  public int price() {
    return coffee.price() + 2;
  }

  @Override
  public String name() {
    return coffee.name();
  }
}

class BasicTest {
  private Coffee decorated;

  @Test
  public void testPriceDecoration() {
    assertEquals(6, decorated.price());
  }

  @Test
  public void testNameDecoration() {
    assertEquals("coffee", decorated.name());
  }

  @Test
  public void testMultiLevelDecoration() {
    assertEquals(8, new Milk(decorated).price());
  }

  @Before
  public void setUp() throws Exception {
    decorated = new Milk(new Coffee("coffee"));
  }
}

Powyższy test potwierdza, że implementacja jest prosta i skuteczna jednak posiada kilka wad:

  • klasa dekorowana musi nadpisywać wszystkie metody klasy dekorowanej,
  • dodanie metody do klasy dekorowanej nie wymusza na nas implementacji tej metody w klasie dekorującej.

Chcąc znaleźć rozwiązanie tych problemów postanowiłem użyć klasy Proxy.

interface Coffee {
  int price();
  String name();
}

class CoffeeImpl implements Coffee {
  private final String name;

  public CoffeeImpl(String name) {
    this.name = name;
  }

  public int price() {
    return 4;
  }

  public String name() {
    return name;
  }
}

class Milk extends CoffeeImpl {
  public static Coffee add(final Coffee coffee) {
    return Decorators.decorate(coffee, new Milk(coffee));
  }

  private final Coffee coffee;

  private Milk(Coffee coffee) {
    super("");
    this.coffee = coffee;
  }

  public int price() {
    return coffee.price() + 2;
  }
}

class Decorators {
  public static Coffee decorate(final Coffee coffee, final Coffee decorator) {
    InvocationHandler handler = new InvocationHandler() {
      public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        Class decoratorClass = decorator.getClass();
        Method m = decoratorClass.getMethod(method.getName(), method.getParameterTypes());
        Coffee target = m.getDeclaringClass() == decoratorClass ? decorator : coffee;
        return method.invoke(target, args);
      }
    };
   return (Coffee) Proxy.newProxyInstance(
     Coffee.class.getClassLoader(),
     new Class[] { Coffee.class },
     handler);
 }
}

class ProxyTest {
  private Coffee decorated;

  @Test
  public void testPriceDecoration() {
    assertEquals(6, decorated.price());
  }

  @Test
  public void testNameDecoration() {
    assertEquals("coffee", decorated.name());
  }

  @Test
  public void testMultiLevelDecoration() {
    assertEquals(8, Milk.add(decorated).price());
  }

  @Before
  public void setUp() throws Exception {
    decorated = Milk.add(new CoffeeImpl("coffee"));
  }
}

Implementacja z użyciem klasy Proxy także działa bez zarzutu oraz rozwiązuje problemy wersji podstawowej jednak nie podoba mi się konieczność zastosowania interfejsu wymuszana przez klasę Proxy. Postanowiłem bardziej zagłębić się w ten temat i natrafiłem na bibliotekę cglib, która pomogła w rozwiązaniu ostatniego problemu.

class Coffee {
  private final String name;

  public Coffee(String name) {
    this.name = name;
  }

  public int price() {
    return 4;
  }

  public String name() {
    return name;
  }
}

class Milk extends Coffee {
  public static Coffee add(final Coffee coffee) {
    return Enhancers.enhance(coffee, new Milk(coffee));
  }

  private final Coffee coffee;

  public Milk(Coffee coffee) {
    super("");
    this.coffee = coffee;
  }

  @Override
  public int price() {
    return coffee.price() + 2;
  }
}

public class Enhancers {
  public static Coffee enhance(final Coffee coffee, final Coffee enhancer) {
    MethodInterceptor interceptor = new MethodInterceptor() {
      public Object intercept(Object obj, Method method, Object[] objects,
          MethodProxy methodProxy) throws Throwable {
        Class enhancerClass = enhancer.getClass();
        Method m = enhancerClass.getMethod(method.getName(), method.getParameterTypes());
        Object target = m.getDeclaringClass() == enhancerClass ? enhancer : coffee;
        return method.invoke(target, objects);
      }
    };

    Enhancer e = new Enhancer();
    e.setSuperclass(Coffee.class);
    e.setCallback(interceptor);

    return (Coffee) e.create(new Class[] { String.class }, new Object[] { "" });
  }
}

class CglibTest {
  private Coffee decorated;

  @Test
  public void testPriceDecoration() {
   assertEquals(6, decorated.price());
  }

  @Test
  public void testNameDecoration() {
    assertEquals("coffee", decorated.name());
  }

  @Test
  public void testMultiLevelDecoration() {
    assertEquals(8, Milk.add(decorated).price());
  }

  @Before
  public void setUp() throws Exception {
    decorated = Milk.add(new Coffee("coffee"));
  }
}