Модел на стратегически дизайн в Java 8

1. Въведение

В тази статия ще разгледаме как можем да приложим шаблона за проектиране на стратегии в Java 8.

Първо ще дадем преглед на модела и ще обясним как той традиционно се прилага в по-старите версии на Java.

След това ще изпробваме модела отново, само че този път с Java 8 lambdas, намалявайки многословието на нашия код.

2. Стратегически модел

По същество стратегическият модел ни позволява да променим поведението на алгоритъма по време на изпълнение.

Обикновено бихме започнали с интерфейс, който се използва за прилагане на алгоритъм, и след това да го внедрим няколко пъти за всеки възможен алгоритъм.

Да приемем, че имаме изискване да прилагаме различни видове отстъпки при покупка, в зависимост от това дали е Коледа, Великден или Нова година. Първо, нека създадем интерфейс Discounter, който ще бъде реализиран от всяка от нашите стратегии:

public interface Discounter { BigDecimal applyDiscount(BigDecimal amount); } 

Тогава да кажем, че искаме да приложим 50% отстъпка по Великден и 10% отстъпка по Коледа. Нека приложим нашия интерфейс за всяка от тези стратегии:

public static class EasterDiscounter implements Discounter { @Override public BigDecimal applyDiscount(final BigDecimal amount) { return amount.multiply(BigDecimal.valueOf(0.5)); } } public static class ChristmasDiscounter implements Discounter { @Override public BigDecimal applyDiscount(final BigDecimal amount) { return amount.multiply(BigDecimal.valueOf(0.9)); } } 

И накрая, нека опитаме стратегия в тест:

Discounter easterDiscounter = new EasterDiscounter(); BigDecimal discountedValue = easterDiscounter .applyDiscount(BigDecimal.valueOf(100)); assertThat(discountedValue) .isEqualByComparingTo(BigDecimal.valueOf(50));

Това работи доста добре, но проблемът е, че може да е малко мъчно да се създаде конкретен клас за всяка стратегия. Алтернативата би била да се използват анонимни вътрешни типове, но това все още е много подробно и не много по-удобно от предишното решение:

Discounter easterDiscounter = new Discounter() { @Override public BigDecimal applyDiscount(final BigDecimal amount) { return amount.multiply(BigDecimal.valueOf(0.5)); } }; 

3. Използване на Java 8

Откакто беше пусната Java 8, въвеждането на ламбда направи анонимните вътрешни типове повече или по-малко излишни. Това означава, че създаването на стратегии в съответствие вече е много по-чисто и лесно.

Освен това декларативният стил на функционалното програмиране ни позволява да реализираме модели, които не са били възможни преди.

3.1. Намаляване на многословността на кода

Нека опитаме да създадем вграден EasterDiscounter, само че този път използваме ламбда израз:

Discounter easterDiscounter = amount -> amount.multiply(BigDecimal.valueOf(0.5)); 

Както виждаме, нашият код сега е много по-чист и по-поддържаем, постигайки същото като преди, но в един ред. По същество ламбда може да се разглежда като заместител на анонимен вътрешен тип .

Това предимство става по-очевидно, когато искаме да декларираме още повече дискаунтъри на линия:

List discounters = newArrayList( amount -> amount.multiply(BigDecimal.valueOf(0.9)), amount -> amount.multiply(BigDecimal.valueOf(0.8)), amount -> amount.multiply(BigDecimal.valueOf(0.5)) );

Когато искаме да дефинираме много дискоунтери, можем да ги декларираме статично на едно място. Java 8 дори ни позволява да дефинираме статични методи в интерфейсите, ако искаме.

Така че, вместо да избираме между конкретни класове или анонимни вътрешни типове, нека опитаме да създадем ламбди в един клас:

public interface Discounter { BigDecimal applyDiscount(BigDecimal amount); static Discounter christmasDiscounter() { return amount -> amount.multiply(BigDecimal.valueOf(0.9)); } static Discounter newYearDiscounter() { return amount -> amount.multiply(BigDecimal.valueOf(0.8)); } static Discounter easterDiscounter() { return amount -> amount.multiply(BigDecimal.valueOf(0.5)); } } 

Както виждаме, постигаме много в не много кода.

3.2. Състав на функцията на лоста

Нека модифицираме нашия интерфейс Discounter, така че да разширява интерфейса UnaryOperator и след това да добавим метод comb () :

public interface Discounter extends UnaryOperator { default Discounter combine(Discounter after) { return value -> after.apply(this.apply(value)); } }

По същество ние рефакторираме нашия Discounter и използваме факта, че прилагането на отстъпка е функция, която преобразува екземпляр BigDecimal в друг екземпляр BigDecimal , което ни позволява достъп до предварително дефинирани методи . Тъй като UnaryOperator идва с метод apply () , ние можем просто да заменим applyDiscount с него.

Методът comb () е просто абстракция около прилагането на един Discounter към резултатите от това. Той използва вградената функционална apply (), за да постигне това.

Сега, нека опитаме да приложим множество Discounters кумулативно към сума. Ще направим това, като използваме функционалния намаляване () и нашия комбиниран ():

Discounter combinedDiscounter = discounters .stream() .reduce(v -> v, Discounter::combine); combinedDiscounter.apply(...);

Обърнете специално внимание на първия аргумент за намаляване . Когато не се предоставят отстъпки, трябва да върнем непроменената стойност. Това може да се постигне чрез предоставяне на функция за идентичност като дискаунтер по подразбиране.

Това е полезна и по-малко подробна алтернатива на извършването на стандартна итерация. Ако разгледаме методите, които изваждаме от кутията за функционален състав, това също ни дава много повече функционалност безплатно.

4. Заключение

В тази статия обяснихме стратегическия модел и демонстрирахме как можем да използваме ламбда изрази, за да го приложим по начин, който е по-малко подробен.

Изпълнението на тези примери може да бъде намерено в GitHub. Това е проект, базиран на Maven, така че трябва да се изпълнява лесно, както е.