Статьи

Извлечение, Внедрение, Убить: Разрушение иерархий — Часть 2

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

Хотя основные идеи Extract, Inject, Kill уже выражены, хорошо закончить упражнение только ради завершения. Вот где мы остановились:

Давайте посмотрим на VoucherPricingService, который сейчас является единственным конкретным классом в нижней части нашей иерархии.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
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 для вычисления значения ваучера.

1
2
3
4
5
6
7
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.java

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
@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. Давайте извлечем содержимое метода VoucherPricingService.applyAdditionalDiscounts (double, String) и добавим его в класс с именем VoucherDiscountCalculation. Давайте вызовем метод executeVoucherDiscount (). Конечно, давайте сначала сделаем это, написав наши тесты. Они должны проверять те же самые вещи, которые проверяются на
VoucherPricingService.applyAdditionalDiscounts (double, String). Мы также пользуемся возможностью передать объект VoucherService в конструктор VoucherDiscountCalculation.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
@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)));
    }
 
}
01
02
03
04
05
06
07
08
09
10
11
12
13
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 и посмотрим, пройдут ли все тесты еще.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
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, который является высшим классом в иерархии. Как всегда, давайте добавим тест, который проверит это новое сотрудничество.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
@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;
        }
 
    }
}

А вот и измененный PriningService.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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 (double total, String voucher), поскольку он больше не используется. Мы также можем уничтожить класс VoucherPricingServiceTest и исправить PricingServiceTest, удалив метод applyAdditionalDiscounts () из тестируемого класса.

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

Вот как выглядит наш граф объектов:

Наша иерархия — это еще один уровень. Единственное, что нам нужно сделать сейчас, это еще раз применить Extract, Inject, Kill , извлекая логику из UserDiscountPricingService в другой класс (например, UserDiscountCalculation), внедрить UserDiscountCalculation в PricingService, и, наконец, убить метод UserDiscountPricingService и шаблон шаблона метода CalculateDiscount (пользовательский пользователь). UserDiscountPricingService,

Поскольку подход был описан ранее, больше нет необходимости идти шаг за шагом. Давайте посмотрим на конечный результат.

Вот диаграмма, представляющая, с чего мы начали:

После последнего рефакторинга Extract, Inject, Kill мы получили следующее:

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

Вот так выглядит финальный класс PricingService:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
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 . Я очень доволен последним .

Ссылка: Извлечение, внедрение, убийство: нарушение иерархии (часть 3) от нашего партнера по JCG Сандро Манкузо в блоге Crafted Software .