В первой части я объясняю основную идею этого подхода, а во второй части я начинаю этот пример. Пожалуйста, прочитайте части 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 . Я очень доволен последним.



