Это третья часть моего выступления « Шаблоны проектирования в 21 веке» .
Шаблон Адаптер соединяет миры. В одном мире у нас есть интерфейс для концепции; в другом мире у нас другой интерфейс. Эти два интерфейса служат разным целям, но иногда нам нужно передавать вещи через. В хорошо написанном юниверсе мы можем использовать адаптеры, чтобы заставить объекты, следующие одному протоколу, придерживаться другого.
Существует два вида шаблона адаптера. Мы не будем говорить об этом:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
|
interface Fire { <T> Burnt<T> burn(T thing); } interface Oven { Food cook(Food food); } class WoodFire implements Fire { ... } class MakeshiftOven extends WoodFire implements Oven { @Override public Food cook(Food food) { Burnt<Food> noms = burn(food); return noms.scrapeOffBurntBits(); } } |
Эта форма, шаблон адаптера класса , выводит меня из себя, потому что extends
дает мне хиби джиби. Почему выходит за рамки этого эссе; не стесняйтесь спрашивать меня в любое время, и я с радостью отговорю ваши уши (и, вероятно, ваш нос) об этом.
Вместо этого давайте поговорим об объектном шаблоне Adapter , который обычно считается гораздо более полезным и гибким во всех отношениях.
Давайте посмотрим на тот же класс, следуя этой альтернативе:
01
02
03
04
05
06
07
08
09
10
11
12
|
class MakeshiftOven implements Oven { private final Fire fire; public MakeshiftOven(Fire fire) { this .fire = fire; } @Override public Food cook(Food food) { Burnt<Food> noms = fire.burn(food); return noms.scrapeOffBurntBits(); } } |
И мы будем использовать это так:
1
2
|
Oven oven = new MakeshiftOven(fire); Food bakedPie = oven.cook(pie); |
Шаблон обычно следует этой простой структуре:
Это хорошо, правда?
Да. Вроде, как бы, что-то вроде. Мы можем сделать лучше.
У нас уже есть ссылка на Fire
, поэтому создание другого объекта, чтобы поиграть с ним, кажется немного … излишним. И этот объект реализует Oven
. Который имеет один абстрактный метод . Я вижу тенденцию здесь.
Вместо этого мы можем сделать функцию, которая делает то же самое.
1
2
|
Oven oven = food -> fire.burn(food).scrapeOffBurntBits(); Food bakedPie = oven.cook(pie); |
Мы могли бы пойти еще дальше и составить ссылки на методы, но на самом деле все становится еще хуже.
1
2
3
4
5
|
// Do *not* do this. Function<Food, Burnt<Food>> burn = fire::burn; Function<Food, Food> cook = burn.andThen(Burnt::scrapeOffBurntBits); Oven oven = cook::apply; Food bakedPie = oven.cook(pie); |
Это потому, что Java не может конвертировать между функциональными интерфейсами неявно, поэтому нам нужно дать ей много подсказок о том, что представляет собой каждый этап операции. С другой стороны, лямбда-выражения неявно применимы к любому функциональному интерфейсу с правильными типами, и компилятор довольно неплохо разбирается, как это сделать.
Наша новая UML-диаграмма будет выглядеть примерно так:
Однако часто все, что нам действительно нужно, — это ссылка на метод. Например, возьмем интерфейс Executor
.
1
2
3
4
5
6
7
8
|
package java.util.concurrent; /** * An object that executes submitted {@link Runnable} tasks. */ public interface Executor { void execute(Runnable command); } |
Он потребляет объекты Runnable
, и это очень полезный интерфейс.
Теперь предположим, что у нас есть одна из них и куча задач Runnable
, которые находятся в Stream
.
1
2
|
Executor executor = ...; Stream<Runnable> tasks = ...; |
Как мы выполняем их все на нашем Executor
?
Это не сработает:
1
|
tasks.forEach(executor); |
Оказывается, метод forEach
в Stream
действительно принимает потребителя, но очень специфического типа:
1
2
3
4
5
6
7
|
public interface Stream<T> { ... void forEach(Consumer<? super T> action); ... } |
Consumer
выглядит так:
1
2
3
4
5
6
7
|
@FunctionalInterface public interface Consumer<T> { void accept(T t); ... } |
На первый взгляд, это не выглядит таким уж полезным. Но обратите внимание, что Consumer
— это функциональный интерфейс, поэтому мы можем использовать лямбда-выражения для их простого определения. Это означает, что мы можем сделать это:
1
|
tasks.forEach(task -> executor.execute(task)); |
Что можно упростить до этого:
1
|
tasks.forEach(executor::execute); |
Java 8 сделала адаптеры настолько простыми, что я не решаюсь называть их шаблонами. Концепция все еще очень важна; явно создавая адаптеры, мы можем разделить эти два мира, кроме как в определенных граничных точках. Реализации, правда? Они просто функции.
Ссылка: | Шаблоны проектирования в 21-м веке: шаблон адаптера от нашего партнера JCG Самира Талвара в блоге Crafted Software . |