Статьи

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

В части 1 этой должности я объясняю проблемы с помощью метода шаблона в глубоких иерархиях классов и , как я пошел , чтобы решить ее. Пожалуйста, прочтите его, прежде чем читать этот пост.

Вот более конкретный пример того, как разбить глубокие иерархии, используя подход Извлечь, Внедрить, Убить. Представьте себе следующую иерархию.

public abstract class PrincingService {
    
    public double calculatePrice(ShoppingBasket shoppingBasket, User user, String voucher) {
        double discount = calculateDiscount(user);
        double total = 0;
        for (ShoppingBasket.Item item : shoppingBasket.items()) {
            total += calculateProductPrice(item.getProduct(), item.getQuantity());
        }
        total = applyAdditionalDiscounts(total, user, voucher);
        return total * ((100 - discount) / 100);
    }

    protected abstract double calculateDiscount(User user);

    protected abstract double calculateProductPrice(Product product, int quantity);

    protected abstract double applyAdditionalDiscounts(double total, User user, String voucher);

}

public abstract class UserDiscountPricingService extends PrincingService {

    @Override
    protected double calculateDiscount(User user) {
        int discount = 0;
        if (user.isPrime()) { 
            discount = 10;
        }
        return discount;
    }
}

public abstract class VoucherPrincingService 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;
    }
}

public class BoxingDayPricingService extends VoucherPrincingService {
    public static final double BOXING_DAY_DISCOUNT = 0.60;

    @Override
    protected double calculateProductPrice(Product product, int quantity) {
        return ((product.getPrice() * quantity) * BOXING_DAY_DISCOUNT);
    }
}

public class StandardPricingService extends VoucherPrincingService {

    @Override
    protected double calculateProductPrice(Product product, int quantity) {
        return product.getPrice() * quantity;
    }
}

Давайте начнем с
StandardPricingService . Сначала напишем несколько тестов:

public class StandardPricingServiceTest {

    private TestableStandardPricingService standardPricingService = new TestableStandardPricingService();
    
    @Test public void
    should_return_product_price_when_quantity_is_one() {
        Product book = aProduct().costing(10).build();

        double price = standardPricingService.calculateProductPrice(book, 1);

        assertThat(price, is(10D));
    }
    
    @Test public void
    should_return_product_price_multiplied_by_quantity() {
        Product book = aProduct().costing(10).build();

        double price = standardPricingService.calculateProductPrice(book, 3);

        assertThat(price, is(30D));
    }

    @Test public void
    should_return_zero_when_quantity_is_zero() {
        Product book = aProduct().costing(10).build();

        double price = standardPricingService.calculateProductPrice(book, 0);

        assertThat(price, is(0D));
    }

    private class TestableStandardPricingService extends StandardPricingService {
        @Override
        protected double calculateProductPrice(Product product, int quantity) {
            return super.calculateProductPrice(product, quantity);
        }
    }
}

Обратите внимание, что здесь я использовал небольшую хитрость, расширив класс StandardPricingService внутри тестового класса, чтобы иметь доступ к защищенному методу. Мы не должны использовать этот трюк в нормальных условиях. Помните, что если вы чувствуете необходимость тестировать защищенные или частные методы, то это потому, что ваш дизайн не совсем правильный, это означает, что в вашем дизайне отсутствует концепция предметной области. Другими словами, есть класс, который плачет, чтобы выйти из класса, который вы пытаетесь проверить.

Теперь давайте сделаем первый шаг в нашей стратегии «Извлечь, ввести, убить». Извлеките содержимое метода convertProductPrice () в другой класс с именем StandardPriceCalculation, Это можно сделать автоматически с помощью IntelliJ или Eclipse. После нескольких незначительных корректировок, вот что у нас есть. 

public class StandardPriceCalculation {

    public double calculateProductPrice(Product product, int quantity) {
        return product.getPrice() * quantity;
    }
}

И теперь StandardPriceService выглядит так:

public class StandardPricingService extends VoucherPrincingService {

    private final StandardPriceCalculation standardPriceCalculation = new StandardPriceCalculation();

    @Override
    protected double calculateProductPrice(Product product, int quantity) {
        return standardPriceCalculation.calculateProductPrice(product, quantity);
    }
}

Все ваши тесты должны пройти.


Когда мы создадим новый класс, давайте добавим к нему несколько тестов.
Это должны быть те же тесты, что и у
StandardPricingService .

public class StandardPriceCalculationTest {

    private StandardPriceCalculation priceCalculation = new StandardPriceCalculation();
    
    @Test public void
    should_return_product_price_when_quantity_is_one() {
        Product book = aProduct().costing(10).build();

        double price = priceCalculation.calculateProductPrice(book, 1);

        assertThat(price, is(10D));
    }
    
    @Test public void
    should_return_product_price_multiplied_by_quantity() {
        Product book = aProduct().costing(10).build();

        double price = priceCalculation.calculateProductPrice(book, 3);

        assertThat(price, is(30D));
    }

    @Test public void
    should_return_zero_when_quantity_is_zero() {
        Product book = aProduct().costing(10).build();

        double price = priceCalculation.calculateProductPrice(book, 0);

        assertThat(price, is(0D));
    }

}

Отлично, один брат готов. Теперь давайте сделаем то же самое для BoxingDayPricingService .

public class BoxingDayPricingServiceTest {
    
    private TestableBoxingDayPricingService boxingDayPricingService = new TestableBoxingDayPricingService();

    @Test public void
    should_apply_boxing_day_discount_on_product_price() {
        Product book = aProduct().costing(10).build();

        double price = boxingDayPricingService.calculateProductPrice(book, 1);

        assertThat(price, is(6D));
    }

    @Test public void
    should_apply_boxing_day_discount_on_product_price_and_multiply_by_quantity() {
        Product book = aProduct().costing(10).build();

        double price = boxingDayPricingService.calculateProductPrice(book, 3);

        assertThat(price, is(18D));
    }

    private class TestableBoxingDayPricingService extends BoxingDayPricingService {
        
        @Override
        protected double calculateProductPrice(Product product, int quantity) {
            return super.calculateProductPrice(product, quantity);
        }
        
    }
}

Теперь давайте выделим поведение в другой класс. Давайте назовем это BoxingDayPricingCalculation .

public class BoxingDayPriceCalculation {
    public static final double BOXING_DAY_DISCOUNT = 0.60;

    public double calculateProductPrice(Product product, int quantity) {
        return ((product.getPrice() * quantity) * BOXING_DAY_DISCOUNT);
    }
}

Новый BoxingDayPriceService теперь

public class BoxingDayPricingService extends VoucherPrincingService {
    private final BoxingDayPriceCalculation boxingDayPriceCalculation = new BoxingDayPriceCalculation();

    @Override
    protected double calculateProductPrice(Product product, int quantity) {
        return boxingDayPriceCalculation.calculateProductPrice(product, quantity);
    }
}

Теперь нам нужно добавить тесты для нового класса.

public class BoxingDayPriceCalculationTest {
    
    private BoxingDayPriceCalculation priceCalculation = new BoxingDayPriceCalculation();

    @Test public void
    should_apply_boxing_day_discount_on_product_price() {
        Product book = aProduct().costing(10).build();

        double price = priceCalculation.calculateProductPrice(book, 1);

        assertThat(price, is(6D));
    }

    @Test public void
    should_apply_boxing_day_discount_on_product_price_and_multiply_by_quantity() {
        Product book = aProduct().costing(10).build(); 

        double price = priceCalculation.calculateProductPrice(book, 3);

        assertThat(price, is(18D));
    }

}

Теперь и StandardPricingService, и BoxingDayPricingService не имеют собственной реализации. Единственное, что они делают, — это делегируют расчет цены соответственно StandardPriceCalculation и BoxingDayPriceCalculation . Оба класса расчета цены имеют один и тот же публичный метод, поэтому теперь давайте извлечем интерфейс PriceCalculation и сделаем так, чтобы они оба реализовали его.

public interface PriceCalculation {
    double calculateProductPrice(Product product, int quantity);
}

public class BoxingDayPriceCalculation implements PriceCalculation 

public class StandardPriceCalculation implements PriceCalculation 

Потрясающие.
Теперь мы готовы к инъекционной части подхода Extract, Inject, Kill. Нам просто нужно
внедрить желаемое поведение в родительский (класс, который определяет метод шаблона).
CalculateProductPrice () определяется в
PricingService , класса на самом верху в иерархии. Вот где мы хотим внедрить
реализацию
PriceCalculation . Вот новая версия:

public abstract class PricingService {

    private PriceCalculation priceCalculation;

    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 = applyAdditionalDiscounts(total, user, voucher);
        return total * ((100 - discount) / 100);
    }

    protected abstract double calculateDiscount(User user);

    protected abstract double applyAdditionalDiscounts(double total, User user, String voucher);

    public void setPriceCalculation(PriceCalculation priceCalculation) {
        this.priceCalculation = priceCalculation;
    }

}

Обратите внимание ,
что метод шаблона
calculateProductPrice () был удален из
PricingService , так как его поведение в настоящее время вводится вместо того , чтобы реализовать с помощью подклассов.


Поскольку мы здесь, давайте напишем несколько тестов для этого последнего изменения, проверяя, правильно ли PricingService вызывает PriceCalculation. 

Отлично.
Теперь мы готовы к последнему этапу рефакторинга Extract, Inject, Kill. Давайте
убьем  как
дочерние классы
StandardPricingService, так и
BoxingDayPricingService
VoucherPricingService , теперь самый глубокий класс в иерархии, может быть повышен до конкретного класса. Давайте еще раз посмотрим на иерархию:

Вот и все. Теперь нужно просто повторить те же шаги для
VoucherPricingService и
UserDiscountPricingService .
Extract  реализации методов их шаблонов в классы,
вводят их в
PricingService и
убить классы.

При этом каждый раз, когда вы извлекаете класс, старайтесь давать им собственные имена, а не называть их
Service . Предложения могут быть
VoucherDiscountCalculation и
PrimeUserDiscountCalculation .

Было несколько небезопасных шагов в ре-факторинге, описанном выше, и я также изо всех сил пытался описать, как именно я это сделал, так как я довольно много играл с кодом. Предложения и идеи очень приветствуются.

ПРИМЕЧАНИЕ.

Если вы не привыкли использовать в своих тестах компоновщики и спрашиваете себя, откуда взялись черты aProduct () и aShoppingBasket (), проверьте код здесь:

ProductBuilder.java

ShoppingBasketBuilder.java

Для получения дополнительной информации об исходной проблеме, которая возникла все это, пожалуйста, прочитайте часть 1 этого блога.

 

От http://craftedsw.blogspot.com/2012/03/extract-inject-kill-breaking_06.html