程序代码是从上到下编写的。 指令以相同的顺序读取和执行。 这是合乎逻辑的,每个人早已习惯了。 在某些情况下,您可以更改操作顺序。 但是有时函数调用的顺序很重要,尽管从语法上讲这并不明显。 事实证明,即使重新安排了调用之后,该代码看起来仍然可以正常工作,并且结果出乎意料。
有一次,类似的代码引起了我的注意。
问题
一次,在联合项目中查看别人的代码,我发现了一个类似以下的函数:
public Product fill(Product product, Images images, Prices prices, Availabilities availabilities){ priceFiller.fill(product, prices);
当然,并不是最“优雅”的风格引起您的注意:类存储数据(POJO),函数更改传入的对象...
总的来说,似乎什么都没有。 我们有一个对象,其中没有足够的数据,并且数据本身来自其他来源(服务),我们现在将其放置在该对象中以使其完整。
但是有一些细微差别。
- 我不喜欢没有一个函数以FP的样式编写并修改作为参数传递的对象的事实。
- 但是,可以说这样做是为了减少处理时间和减少创建对象的数量。
- 仅代码中的注释说明调用顺序很重要,并且在嵌入新
Filler
时应格外小心
- 但是从事该项目的人数超过1,并不是每个人都知道这一技巧。 特别是团队中的新人(不一定在企业中)。
最后一点特别警惕我。 毕竟,API的构建方式使得调用此函数后我们不知道对象中发生了什么变化。 例如,在我们拥有Product::getImages
方法之前和之后,在调用fill
函数之前,该方法将生成一个空列表,然后生成一个包含图片的列表到我们的产品。
使用Filler
情况甚至更糟。 AvailabilityFiller
并未明确表示它期望有关商品价格的信息已经嵌入到转移的对象中。
因此,我想到了如何保护同事免受错误使用功能的影响。
拟议的解决方案
首先,我决定与同事讨论此案。 不幸的是,在我看来,他们提出的所有解决方案都不是正确的方法。
运行时异常
建议的选项之一是:然后在Objects.requireNonNull(product.getPrices)
函数的开头写入AvailabilityFiller
,然后任何程序员在本地测试期间将已经收到错误。
- 但是价格实际上可能不存在,如果服务不可用或其他错误,则该产品应处于“缺货”状态。 我们必须赋予各种标志或其他任何属性以区分“无数据”与“甚至没有请求”。
- 如果您在
getPrices
本身中引发异常,那么我们将创建与现代Java具有列表相同的问题
- 假设将一个列表传递给在其API中提供get方法的函数...我知道您不需要更改已传输的对象,而只需创建一个新对象。 但最重要的是,API为我们提供了这种方法,但是在运行时,如果它是不可变的列表(例如从Collectors.toList()获得的列表),则可能会发生错误。
- 如果
AvailabilityFiller
被其他人使用,则编写该调用的程序员将不会立即了解问题所在。 仅在启动和调试之后。 然后,他仍然必须理解代码以弄清楚从何处获取数据。
测验
“而且您编写了一个测试,如果更改呼叫顺序,该测试将失败。” 即 如果所有Filler
返回“新”产品,则结果如下:
given(priceFillerMock.fill(eq(productMock), any())).willReturn(productWithPricesMock); given(availabilityFillerMock.fill(eq(productMockWithPrices), any())).willReturn(productMockWithAvailabilities); given(imageFillerMock.fill(eq(productMockWithAvailabilities), any())).willReturn(productMockWithImages); var result = productFiller.fill(productMock, p1, p2, p3); assertThat("unexpected return value", result, is(productMockWithImages));
- 我不喜欢“白盒”这样的测试
- 每次
Filler
新Filler
- 更改独立呼叫的顺序时会中断
- 同样,它不能解决重用
AvailabilityFiller
本身的问题
自己尝试解决问题
主意
我想您已经猜到我想在编译级别解决问题。 好吧,有人问,如果我无法防止错误,为什么需要一种具有强类型的编译语言。
我想知道一个没有附加数据的对象和一个“扩展”对象是否属于同一类?
将对象的各种可能状态描述为单独的类或接口是否正确?
所以我的想法是这样的:
public Product fill(<? extends BaseProduct> product, Images images, Prices prices, Availabilities availabilities){ var p1 = priceFiller.fill(product, prices); var p2 = availabilityFiller.fill(p1, availabilities); return imageFiller.fill(p2, images); } PriceFiller public ProductWithPrices fill(<? extends BaseProduct> product, Prices prices) AvailabilityFiller public ProductWithAvailabilities fill(<? extends ProductWithPrices> product, Prices prices) public <BaseProduct & PriceAware & AvailabilityAware> fill(<? extends BaseProduct & PriceAware> product, Prices prices)
即 最初定义的产品是除返回的类之外的其他类的实例,该类已显示数据更改。
Filler
在其API中,确切指定了所需的数据以及返回的数据。
这样可以防止错误的呼叫顺序。
实作
如何在Java中将其转化为现实? (回想一下,不可能从Java中的多个类继承。)
复杂性是通过独立操作来增加的。 例如,可以在添加价格之前和之后以及在功能的最后添加图片。
那也许
class ProductWithImages extends BaseProduct implements ImageAware{} class ProductWithImagesAndPrices extends BaseProduct implements ImageAware, PriceAware{} class Product extends BaseProduct implements ImageAware, PriceAware, AvailabilityAware{}
如何形容这一切?
创建适配器?
public ProductWithImagesAndPrices(<? extends BaseProduct & PriceAware> base){ this.base = base; this.images = Collections.emptyList(); } public long getId(){ return this.base.getId(); } public Price getPrice(){ return this.base.getPrice(); } public List<Image> getImages(){ return this.images; }
复制数据/链接?
public ProductWithImagesAndPrices(<? extends BaseProduct & PriceAware> base){ this.id = base.getId(); this.prices = base.getPrices(); this.images = Collections.emptyList(); } public List<Image> getImages(){ return this.images; }
众所周知,这全都归结为大量的代码。 尽管在示例中我仅留下了3种类型的输入数据,但事实并非如此。 在现实世界中,可能还有更多。
事实证明,尽管将状态划分为单独的类的想法对我来说似乎很有吸引力,但是编写和维护此类代码的成本并不能证明其合理性。
撤退
如果您使用其他语言,则此问题更容易解决,但不容易解决。
例如,在Go中,您可以编写对可扩展类的引用,而无需“复制”或“重载”方法。 但这与围棋无关
另一个题外话
在撰写本文时, Proxy
提出了另一个可能的解决方案,该解决方案仅要求编写新方法,但需要接口层次结构。 一般来说,吓人,生气和不合适。 如果某人突然感兴趣:
上床吃饭之前,不建议看这个 public class Application { public static void main(String[] args) { var baseProduct = new BaseProductProxy().create(new BaseProductImpl(100L)); var productWithPrices = fillPrices(baseProduct, BigDecimal.TEN); var productWithAvailabilities = fillAvailabilities(productWithPrices, "available"); var productWithImages = fillImages(productWithAvailabilities, List.of("url1, url2")); var product = productWithImages; System.out.println(product.getId()); System.out.println(product.getPrice()); System.out.println(product.getAvailability()); System.out.println(product.getImages()); } static <T extends BaseProduct> ImageAware fillImages(T base, List<String> images) { return (ImageAware) Proxy.newProxyInstance(base.getClass().getClassLoader(), new Class[]{ImageAware.class, BaseProduct.class}, new MyInvocationHandler<>(base, new ImageAware() { @Override public List<String> getImages() { return images; } })); } static <T extends BaseProduct> PriceAware fillPrices(T base, BigDecimal price) { return (PriceAware) Proxy.newProxyInstance(base.getClass().getClassLoader(), new Class[]{PriceAware.class}, new MyInvocationHandler<>(base, new PriceAware() { @Override public BigDecimal getPrice() { return price; } })); } static AvailabilityAware fillAvailabilities(PriceAware base, String availability) { return (AvailabilityAware) Proxy.newProxyInstance(base.getClass().getClassLoader(), new Class[]{AvailabilityAware.class}, new MyInvocationHandler<>(base, new AvailabilityAware() { @Override public String getAvailability() { return base.getPrice().intValue() > 0 ? availability : "sold out"; } })); } static class BaseProductImpl implements BaseProduct { private final long id; BaseProductImpl(long id) { this.id = id; } @Override public long getId() { return id; } } static class BaseProductProxy { BaseProduct create(BaseProduct base) { return (BaseProduct) Proxy.newProxyInstance(this.getClass().getClassLoader(), new Class[]{BaseProduct.class}, new MyInvocationHandler<>(base, base)); } } public interface BaseProduct { default long getId() { return -1L; } } public interface PriceAware extends BaseProduct { default BigDecimal getPrice() { return BigDecimal.ZERO; } } public interface AvailabilityAware extends PriceAware { default String getAvailability() { return "sold out"; } } public interface ImageAware extends AvailabilityAware { default List<String> getImages() { return Collections.emptyList(); } } static class MyInvocationHandler<T extends BaseProduct, U extends BaseProduct> implements InvocationHandler { private final U additional; private final T base; MyInvocationHandler(T base, U additional) { this.additional = additional; this.base = base; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if (Arrays.stream(additional.getClass().getInterfaces()).anyMatch(i -> i == method.getDeclaringClass())) { return method.invoke(additional, args); } var baseMethod = Arrays.stream(base.getClass().getMethods()).filter(m -> m.getName().equals(method.getName())).findFirst(); if (baseMethod.isPresent()) { return baseMethod.get().invoke(base, args); } throw new NoSuchMethodException(method.getName()); } } }
结论
结果如何? 一方面,存在一种有趣的方法,该方法将单独的类应用于处于不同状态的“对象”,并确保防止由于对修改该对象的方法的调用顺序不正确而导致的错误。
另一方面,这种方法使您编写了太多代码,以至于您立即希望拒绝它。 大量的接口和类只会使理解项目变得困难。
在我的另一个项目中,我仍然尝试使用这种方法。 首先,在接口级别,一切都很好。 我写了函数:
<T extends Foo> List<T> firstStep(List<T> ts){} <T extends Foo & Bar> List<T> nStep(List<T> ts){} <T extends Foo> List<T> finalStep(List<T> ts){}
因此已经表明某个数据处理步骤需要在处理开始时或在处理结束时不需要的附加信息。
使用mock
'和,我设法测试了代码。 但是当涉及到实现时,数据和各种来源的数量开始增长,我很快就放弃了,并将所有内容重新制作成“正常”外观。 一切正常,没有人抱怨。 事实证明,代码的效率和简便性胜过“预防”错误,并且即使错误仅在手动测试阶段出现,您也可以手动跟踪正确的调用顺序。
也许如果我退后一步,从另一端看代码,我将拥有完全不同的解决方案。 但是碰巧的是,我对这条特别的评论感兴趣。
在文章的末尾,已经考虑到以下事实:由于在接口中描述设置器不太好,您可以想象以Builder
的形式组装产品数据,该方法在添加定义的数据后将返回一个不同的接口。 同样,这都取决于构建对象逻辑的复杂性。 如果您使用过Spring Security,那么您将熟悉这种解决方案。
对于我的示例,它是这样的:
基于生成器模式的解决方案 public class Application_2 { public static void main(String[] args) { var product = new Product.Builder() .id(1000) .price(20) .availability("available") .images(List.of("url1, url2")) .build(); System.out.println(product.getId()); System.out.println(product.getAvailability()); System.out.println(product.getPrice()); System.out.println(product.getImages()); } static class Product { private final int price; private final long id; private final String availability; private final List<String> images; private Product(int price, long id, String availability, List<String> images) { this.price = price; this.id = id; this.availability = availability; this.images = images; } public int getPrice() { return price; } public long getId() { return id; } public String getAvailability() { return availability; } public List<String> getImages() { return images; } public static class Builder implements ProductBuilder, ProductWithPriceBuilder { private int price; private long id; private String availability; private List<String> images; @Override public ProductBuilder id(long id) { this.id = id; return this; } @Override public ProductWithPriceBuilder price(int price) { this.price = price; return this; } @Override public ProductBuilder availability(String availability) { this.availability = availability; return this; } @Override public ProductBuilder images(List<String> images) { this.images = images; return this; } public Product build(){ var av = price > 0 && availability != null ? availability : "sold out"; return new Product(price, id, av, images); } } public interface ProductBuilder { ProductBuilder id(long id); ProductBuilder images(List<String> images); ProductWithPriceBuilder price(int price); Product build(); } public interface ProductWithPriceBuilder{ ProductBuilder availability(String availability); } } }
这样:
- 编写干净的功能
- 编写精美清晰的代码
- 请记住,简洁是姐妹,最重要的是,代码可以正常工作
- 为此,您可以随意提问,寻找其他方式,甚至提出其他解决方案
- 不要沉默! 当向别人解释问题时,更好的解决方案应运而生(橡胶鸭驱动开发)
谢谢您的关注。