Статьи

Извлечение, внедрение, убийство: нарушение иерархий — часть 3

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

}
Теперь настало время , чтобы убить в VoucherPricingService класс и убить в PricingService.applyAdditionalDiscounts (двойной тотал, String ваучера) метод шаблона, так как он не используется больше. Мы также можем убить в VoucherPricingServiceTest класса и исправить PricingServiceTest удаляющего () applyAdditionalDiscounts метод из проверяемого класса.
Так что теперь, конечно, у нас больше нет конкретного класса в нашей иерархии, поскольку VoucherPricingService был единственным. Теперь мы можем безопасно продвигать UserDiscountPricingService к конкретным.
Вот как выглядит наш граф объектов:
Наша иерархия — это еще один уровень. Единственное, что нам нужно сделать сейчас, это еще раз применить   
Extract, Inject, Kill  ,
извлекая логику из
UserDiscountPricingService в другой класс (например,
UserDiscountCalculation ),
внедрить
UserDiscountCalculation в
PricingService , и, наконец,
убить
метод UserDiscountPricingService и
шаблон шаблона метода CalculateDiscount
(пользовательский пользователь)
UserDiscountPricingService
Поскольку подход был описан ранее, больше нет необходимости идти шаг за шагом. Давайте посмотрим на конечный результат.
Вот диаграмма, представляющая, с чего мы начали:
  После последнего рефакторинга Extract, Inject, Kill мы получили следующее:

Крутая вещь в финальной модели, изображенной выше, состоит в том, что теперь у нас больше нет абстрактных классов. Все классы и методы являются конкретными, и каждый отдельный класс тестируется независимо. 
Вот так выглядит финальный класс 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 . Я очень доволен последним.