尽管已经表达了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.java进行一些测试
@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,Inject,Kill。 让我们提取 VoucherPricingService.applyAdditionalDiscounts(double,String)方法的内容,并将其添加到名为VoucherDiscountCalculation的类中。 我们将其称为calculateVoucherDiscount()方法。 当然,让我们首先编写测试。 他们需要测试与测试时完全相同的东西
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中,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;
}
}
}
这是更改后的PriningService。
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(双总,字符串凭证)模板方法,因为它不再使用。 我们也可以杀死 VoucherPricingServiceTest类和固定PricingServiceTest取出applyAdditionalDiscounts从可测试类()方法。
因此,当然,现在,我们的层次结构中不再有具体的类了,因为VoucherPricingService是唯一的类。 现在,我们可以安全地将UserDiscountPricingService提升为具体。
现在,我们的对象图如下所示:
我们的层次结构又短了一层。 现在我们唯一要做的就是再次应用Extract,Inject,Kill , 将 UserDiscountPricingService内部的逻辑提取到另一个类中(例如,UserDiscountCalculation), 将 UserDiscountCalculation注入PricingService中,最后杀死 UserDiscountPricingService和calculateDiscount(User user)模板方法。 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社区版 。 我对后者很满意。
参考:在Crafted Software博客上,从我们的JCG合作伙伴 Sandro Mancuso处提取,注入,杀死:破坏层次结构(第3部分) 。
翻译自: https://www.javacodegeeks.com/2012/06/extract-inject-kill-breaking_13.html