В первой части я объясняю основную идею этого подхода, а во второй части я начинаю этот пример. Пожалуйста, прочитайте части 1 и 2, прежде чем читать этот пост
Хотя основные идеи Extract, Inject, Kill уже выражены, хорошо закончить упражнение только ради завершения. Вот где мы остановились:
Давайте посмотрим на VoucherPricingService , который сейчас является единственным конкретным классом в нижней части нашей иерархии.
public class VoucherPricingService extends UserDiscountPricingService { private VoucherService voucherService; @Override protected double applyAdditionalDiscounts(double total, User user, String voucher) { double voucherValue = voucherService.getVoucherValue(voucher); double totalAfterValue = total - voucherValue; return (totalAfterValue > 0) ? totalAfterValue : 0; } public void setVoucherService(VoucherService voucherService) { this.voucherService = voucherService; } }
Обратите внимание, что он использует класс VoucherService для вычисления значения ваучера.
public class VoucherService { public double getVoucherValue(String voucher) { // Imagine that this calculate the voucher price. // Keeping it simple so we can understand the approach. return 0; } }
Прежде всего, давайте напишем несколько тестов для VoucherPricingService . Ява
@RunWith(MockitoJUnitRunner.class) public class VoucherPricingServiceTest { private static final User UNUSED_USER = null; private static final String NO_VOUCHER = null; private static final String TWENTY_POUNDS_VOUCHER = "20"; @Mock private VoucherService voucherService; private TestableVoucherPricingService voucherPricingService; @Before public void initialise() { voucherPricingService = new TestableVoucherPricingService(); voucherPricingService.setVoucherService(voucherService); when(voucherService.getVoucherValue(TWENTY_POUNDS_VOUCHER)).thenReturn(20D); } @Test public void should_not_apply_discount_if_no_voucher_is_received() { double returnedAmount = voucherPricingService.applyAdditionalDiscounts(1000, UNUSED_USER, NO_VOUCHER); assertThat(returnedAmount, is(1000D)); } @Test public void should_subtract_voucher_value_from_total() { double returnedAmount = voucherPricingService.applyAdditionalDiscounts(30D, UNUSED_USER, TWENTY_POUNDS_VOUCHER); assertThat(returnedAmount, is(equalTo(10D))); } @Test public void shoudl_return_zero_if_voucher_value_is_higher_than_total() { double returnedAmount = voucherPricingService.applyAdditionalDiscounts(10D, UNUSED_USER, TWENTY_POUNDS_VOUCHER); assertThat(returnedAmount, is(equalTo(0D))); } private class TestableVoucherPricingService extends VoucherPricingService { @Override protected double applyAdditionalDiscounts(double total, User user, String voucher) { return super.applyAdditionalDiscounts(total, user, voucher); } } }
Следует отметить, что параметр User ни для чего не используется. Итак, давайте удалим это.
Теперь пришло время использовать извлечение, внедрение, уничтожение на сервисе VoucherPricingService . Давайте Extract содержание VoucherPricingService.applyAdditionalDiscounts (дважды String) метод и добавить его к классу называется VoucherDiscountCalculation . Давайте вызовем метод executeVoucherDiscount () . Конечно, давайте сначала сделаем это, написав наши тесты. Они должны проверять те же самые вещи, что и VoucherPricingService.applyAdditionalDiscounts (double, String) . Мы также пользуемся возможностью передать объект VoucherService в конструктор VoucherDiscountCalculation .
@RunWith(MockitoJUnitRunner.class) public class VoucherDiscountCalculationTest { private static final String NO_VOUCHER = null; private static final String TWENTY_POUNDS_VOUCHER = "20"; @Mock private VoucherService voucherService; private VoucherDiscountCalculation voucherDiscountCalculation; @Before public void initialise() { voucherDiscountCalculation = new VoucherDiscountCalculation(voucherService); when(voucherService.getVoucherValue(TWENTY_POUNDS_VOUCHER)).thenReturn(20D); } @Test public void should_not_apply_discount_if_no_voucher_is_received() { double returnedAmount = voucherDiscountCalculation.calculateVoucherDiscount(1000, NO_VOUCHER); assertThat(returnedAmount, is(1000D)); } @Test public void should_subtract_voucher_value_from_total() { double returnedAmount = voucherDiscountCalculation.calculateVoucherDiscount(30D, TWENTY_POUNDS_VOUCHER); assertThat(returnedAmount, is(equalTo(10D))); } @Test public void should_return_zero_if_voucher_value_is_higher_than_total() { double returnedAmount = voucherDiscountCalculation.calculateVoucherDiscount(10D, TWENTY_POUNDS_VOUCHER); assertThat(returnedAmount, is(equalTo(0D))); } }
public class VoucherDiscountCalculation { private VoucherService voucherService; public VoucherDiscountCalculation(VoucherService voucherService) { this.voucherService = voucherService; } public double calculateVoucherDiscount(double total, String voucher) { double voucherValue = voucherService.getVoucherValue(voucher); double totalAfterValue = total - voucherValue; return (totalAfterValue > 0) ? totalAfterValue : 0; } }
Если вы заметили, что при выполнении извлечения мы воспользовались возможностью дать собственные имена нашим новым классам и методам, а также передать их существенные зависимости конструктору вместо использования внедрения метода.
Давайте теперь изменим код в VoucherPricingService для использования нового VoucherDiscountCalculation и посмотрим, пройдут ли все тесты еще.
public class VoucherPricingService extends UserDiscountPricingService { private VoucherService voucherService; @Override protected double applyAdditionalDiscounts(double total, String voucher) { VoucherDiscountCalculation voucherDiscountCalculation = new VoucherDiscountCalculation(voucherService); return voucherDiscountCalculation.calculateVoucherDiscount(total, voucher); } public void setVoucherService(VoucherService voucherService) { this.voucherService = voucherService; } }
Здорово. Все тесты по-прежнему проходят, что означает, что у нас такое же поведение, но теперь в классе VoucherDiscountCalculation , и мы готовы перейти к этапу Inject .
Давайте теперь инъекционное VoucherDiscountCalculation в PricingService , то есть высший класс в иерархии. Как всегда, давайте добавим тест, который проверит это новое сотрудничество.
@RunWith(MockitoJUnitRunner.class) public class PricingServiceTest { private static final String NO_VOUCHER = ""; private static final String FIVE_POUNDS_VOUCHER = "5"; private TestablePricingService pricingService = new TestablePricingService(); private ShoppingBasket shoppingBasket; @Mock private PriceCalculation priceCalculation; @Mock private VoucherDiscountCalculation voucherDiscountCalculation; @Before public void initialise() { this.pricingService.setPriceCalculation(priceCalculation); this.pricingService.setVoucherDiscountCalculation(voucherDiscountCalculation); } @Test public void should_calculate_price_of_all_products() { Product book = aProduct().named("book").costing(10).build(); Product kindle = aProduct().named("kindle").costing(80).build(); shoppingBasket = aShoppingBasket() .with(2, book) .with(3, kindle) .build(); double price = pricingService.calculatePrice(shoppingBasket, new User(), NO_VOUCHER); verify(priceCalculation, times(1)).calculateProductPrice(book, 2); verify(priceCalculation, times(1)).calculateProductPrice(kindle, 3); } @Test public void should_calculate_voucher_discount() { Product book = aProduct().named("book").costing(10).build(); when(priceCalculation.calculateProductPrice(book, 2)).thenReturn(20D); shoppingBasket = aShoppingBasket() .with(2, book) .build(); double price = pricingService.calculatePrice(shoppingBasket, new User(), FIVE_POUNDS_VOUCHER); verify(voucherDiscountCalculation, times(1)).calculateVoucherDiscount(20, FIVE_POUNDS_VOUCHER); } private class TestablePricingService extends PricingService { @Override protected double calculateDiscount(User user) { return 0; } @Override protected double applyAdditionalDiscounts(double total, String voucher) { return 0; } } }
А вот и измененный PricingService .
public abstract class PricingService { private PriceCalculation priceCalculation; private VoucherDiscountCalculation voucherDiscountCalculation; public double calculatePrice(ShoppingBasket shoppingBasket, User user, String voucher) { double discount = calculateDiscount(user); double total = 0; for (ShoppingBasket.Item item : shoppingBasket.items()) { total += priceCalculation.calculateProductPrice(item.getProduct(), item.getQuantity()); } total = voucherDiscountCalculation.calculateVoucherDiscount(total, voucher); return total * ((100 - discount) / 100); } protected abstract double calculateDiscount(User user); protected abstract double applyAdditionalDiscounts(double total, String voucher); public void setPriceCalculation(PriceCalculation priceCalculation) { this.priceCalculation = priceCalculation; } public void setVoucherDiscountCalculation(VoucherDiscountCalculation voucherDiscountCalculation) { this.voucherDiscountCalculation = voucherDiscountCalculation; } }
Extract, Inject, Kill ,
извлекая логику из
UserDiscountPricingService в другой класс (например,
UserDiscountCalculation ),
внедрить
UserDiscountCalculation в
PricingService , и, наконец,
убить
метод UserDiscountPricingService и
шаблон шаблона метода CalculateDiscount (пользовательский пользователь) .
UserDiscountPricingService ,
Крутая вещь в финальной модели, изображенной выше, состоит в том, что теперь у нас больше нет абстрактных классов. Все классы и методы являются конкретными, и каждый отдельный класс тестируется независимо.
Вот так выглядит финальный класс PricingService:
public class PricingService { private PriceCalculation priceCalculation; private VoucherDiscountCalculation voucherDiscountCalculation; private PrimeUserDiscountCalculation primeUserDiscountCalculation; public PricingService(PriceCalculation priceCalculation, VoucherDiscountCalculation voucherDiscountCalculation, PrimeUserDiscountCalculation primeUserDiscountCalculation) { this.priceCalculation = priceCalculation; this.voucherDiscountCalculation = voucherDiscountCalculation; this.primeUserDiscountCalculation = primeUserDiscountCalculation; } public double calculatePrice(ShoppingBasket shoppingBasket, User user, String voucher) { double total = getTotalValueFor(shoppingBasket); total = applyVoucherDiscount(voucher, total); return totalAfterUserDiscount(total, userDiscount(user)); } private double userDiscount(User user) { return primeUserDiscountCalculation.calculateDiscount(user); } private double applyVoucherDiscount(String voucher, double total) { return voucherDiscountCalculation.calculateVoucherDiscount(total, voucher); } private double totalAfterUserDiscount(double total, double discount) { return total * ((100 - discount) / 100); } private double getTotalValueFor(ShoppingBasket shoppingBasket) { double total = 0; for (ShoppingBasket.Item item : shoppingBasket.items()) { total += priceCalculation.calculateProductPrice(item.getProduct(), item.getQuantity()); } return total; } }
Для полной реализации окончательного кода, пожалуйста, посмотрите https://github.com/sandromancuso/breaking-hierarchies
Примечание. В этом посте из трех частей я использовал три разных подхода к рисованию UML-диаграмм. Рукой, используя ArgoUML и Astah Community Edition . Я очень доволен последним.