Java 8的功能性编程模式


自从Java实现函数式编程以来已经有四年了。我们使用Java 8已经有四年的时间了。

而且我们玩过...玩过。在开发了几个大量使用Lambda和Streams的大型企业项目,并在此之后咨询了许多其他项目,并以独立培训师的身份向数百名开发人员讲授了这些概念之后,我认为是时候总结一下模式,最佳做法和反模式。

由于我今年在Devoxx进行的会谈中获得的热情,我写了这篇文章。如果您想从充满激情,娱乐性和闪电般的实时编码视频中学习更多信息,请查看我的谈话录像。

本文将引导您完成一系列简化的重构练习,从传统的命令式代码到Java 8中的函数式代码,都将继续着眼于Simplicity Clean Code。为了从本文中获得最大的收益,您应该对Java 8功能有一些实践经验。我将练习的每个阶段都提交到了GitHub存储库,因此,您可以自己浏览存储库以查看全部内容。

1)在匿名Lambda上优先使用命名函数 为了进行热身,让我们从将一些用户的详细信息带到UI的简单任务开始。我们将从对实体列表的传统回顾开始,将每个实体列表转换User为UserDto:

public List<UserDto> getAllUsers() {
        List<User> users = userRepo.findAll();
        List<UserDto> dtos = new ArrayList<>();
        for (User user : users) {
                UserDto dto = new UserDto();
                dto.setUsername(user.getUsername());
                dto.setFullName(user.getFirstName() + " " + user.getLastName().toUpperCase());
                dto.setActive(user.getDeactivationDate() == null);
                dtos.add(dto);
        }
        return dtos;
}

但是,我对这个代码不是很骄傲,因为在很多情况下,我可能会重复编写类似的代码。因此,让我们使用Java 8削减样板:

public List<UserDto> getAllUsers() {
        return userRepo.findAll().stream()
                .map(user -> {
                        UserDto dto = new UserDto();
                        dto.setUsername(user.getUsername());
                        dto.setFullName(user.getFirstName() + " " + user.getLastName().toUpperCase());
                        dto.setActive(user.getDeactivationDate() == null);
                        return dto;
                })
                .collect(toList());
}

这样更好 但是,我仍然不满意。我写的这个lambda演示了一个“匿名函数”。作为一个干净的代码疯子,我对此有一个疑问–我想要表达性的名字。因此,我迅速将lambda内容提取到一个单独的方法中:

public List<UserDto> getAllUsers() {
        return userRepo.findAll().stream().map(this::toDto).collect(toList());
}
private UserDto toDto(User user) {
        UserDto dto = new UserDto();
        dto.setUsername(user.getUsername());
        dto.setFullName(user.getFirstName() + " " + user.getLastName().toUpperCase());
        dto.setActive(user.getDeactivationDate() == null);
        return dto;
}

好的。该代码在以前的版本中足够简单,但现在略胜一筹。我只用了三秒钟就完成了我的IDE。我总是建议我的学员掌握那些重构捷径!

有时,这种逻辑转换是如此微不足道,如本例所示,以至于我们可以将其直接放入DTO构造函数中。请注意,我将要求DTO依赖实体,而不是实体(依赖关系反转原理):

public class UserFacade {
        private UserRepo userRepo;        
        public List<UserDto> getAllUsers() {
                return userRepo.findAll().stream().map(UserDto::new).collect(toList());
        }
}
public class UserDto {
        private String username;
        private String fullName;
        private boolean active;
        public UserDto(User user) {
                username = user.getUsername();
                fullName = user.getFirstName() + " " + user.getLastName().toUpperCase();
                active = user.getDeactivationDate() == null;
        }
        ...
}

现在,让我们想象一下这种转换需要其他一些组件的帮助,我们希望使用Spring,Guice,CDI等注入这些组件。但是,将依赖注入到我们实例化的类中将需要非常复杂的代码。相反,我会回到以前的版本,但是如果这种转换确实变得太复杂了,我们应该将其移到一个单独的UserMapper类中并从那里引用它:

@Service
public class UserFacade {
          @Autowired
          private UserRepo userRepo;
          @Autowired
          private UserMapper mapper;         
          public List<UserDto> getAllUsers() {
                   return userRepo.findAll().stream().map(mapper::toDto).collect(toList());
          }
}
@Component
public class UserMapper {
          @Autowired
          private OtherClass otherClass;
          public UserDto toDto(User user) {
                   UserDto dto = new UserDto();
                   dto.setUsername(user.getUsername());
                   ... // code using otherClass
                   return dto;
          }
}

关键点是:始终将复杂的lambda提取为具有表达性名称的函数,然后可以使用以下命令中的四点(::)进行引用:

  • the same class ( this:: );
  • another class ( mapper:: );
  • some static helper method ( SomeClass:: );
  • the Stream item type ( Item::);
  • even some constructor ( UserDto::new), if it’s simple enough;

简而言之:永远不要输入-> {

2)Stream Wrecks 假设自从Lambda和Stream添加到语言以来,您就一直在使用它们。而且,您需要证明这一点,对吗?因此,您实现了一个用例:

public List<Product> getFrequentOrderedProducts(List<Order> orders) {
        return orders.stream()
                        .filter(o -> o.getCreationDate().isAfter(LocalDate.now().minusYears(1)))
                        .flatMap(o -> o.getOrderLines().stream())
                        .collect(groupingBy(OrderLine::getProduct, summingInt(OrderLine::getItemCount)))
                        .entrySet()
                        .stream()
                        .filter(e -> e.getValue() >= 10)
                        .map(Entry::getKey)
                        .filter(p -> !p.isDeleted())
                        .filter(p -> !productRepo.getHiddenProductIds().contains(p.getId()))
                        .collect(toList());
}

您可以计算上一年订购产品的次数。现在,如果没有按逻辑删除或从数据库中明确隐藏它们,则仅获取频繁订购的产品(> = 10)并返回它们。

你开心回家...

但是我们会找到你的!管理层无法解雇您-我同意-但是,谁能读懂这些东西?承认吧……谁会愿意与您合作?

此代码最糟糕的事情是每一行都返回不同的类型。除非您将鼠标悬停在IDE中(侦探工作),否则您将看不到这些类型。

干净代码最重要的规则之一是: 小方法。因此,通过查看.collect(..)紧随其后的代码,让我们将此长链分为两个方法.stream()。既然您还是将这些项目收集到一个集合中,为什么我们不通过提取一个名字很好的方法来解释那个集合是什么呢?此外,让我们替换!order.isDeleted()order.isNotDeleted(),就可以使用了::。

public List<Product> getFrequentOrderedProducts(List<Order> orders) {
          return getProductCountsOverTheLastYear(orders).entrySet().stream()
                             .filter(e -> e.getValue() >= 10)
                             .map(Entry::getKey)
                             .filter(Product::isNotDeleted)
                             .filter(p -> !productRepo.getHiddenProductIds().contains(p.getId()))
                             .collect(toList());
}
private Map<Product, Integer> getProductCountsOverTheLastYear(List<Order> orders) {
          return orders.stream()
                             .filter(o -> o.getCreationDate().isAfter(LocalDate.now().minusYears(1)))
                             .flatMap(o -> o.getOrderLines().stream())
                             .collect(groupingBy(OrderLine::getProduct, summingInt(OrderLine::getItemCount)));
}

但是,只有到那时,我们才注意到在第6行中,我们可能正在循环查询一个外部系统!我的天啊!那是您永远都不要做的事情。因此,让hiddenProductIds我们在开始流式传输之前获取该列表。我们甚至可以走得更远,提取检查产品是否隐藏在Predicate局部变量中。如果他愿意将函数保留在变量中,则可以为读者提供帮助:)。

public List<Product> getFrequentOrderedProducts(List<Order> orders) {
        List<Long> hiddenProductIds = productRepo.getHiddenProductIds();
        Predicate<Product> productIsNotHidden = p -> !hiddenProductIds.contains(p.getId());
        return getProductCountsOverTheLastYear(orders).entrySet().stream()
                        .filter(e -> e.getValue() >= 10)
                        .map(Entry::getKey)
                        .filter(Product::isNotDeleted)
                        .filter(productIsNotHidden)
                        .collect(toList());
}

我们还可以做一件事:我们可以命名常用产品流并将其设置为type变量Stream。如我们所知,Stream项目实际上并未在此时进行评估,而只是在最终评估时才进行评估.collect()。但是,Stream<>有时不鼓励使用变量,因为粗心的开发人员可能会尝试重用(重新遍历)变量,因此在执行变量之前,请确保您的团队完全了解这种常见情况。

public List<Product> getFrequentOrderedProducts(List<Order> orders) {
        List<Long> hiddenProductIds = productRepo.getHiddenProductIds();
        Predicate<Product> productIsNotHidden = p -> !hiddenProductIds.contains(p.getId());
        Stream<Product> frequentProducts = getProductCountsOverTheLastYear(orders).entrySet().stream()
                        .filter(e -> e.getValue() >= 10)
                        .map(Entry::getKey);
        return frequentProducts
                        .filter(Product::isNotDeleted)
                        .filter(productIsNotHidden)
                        .collect(toList());
}
[...]

这里的想法是通过引入解释变量来避免过多的方法链接。 这意味着 提取方法,甚至使用函数或Stream类型的变量,以使您的读者尽可能清楚地看到代码。

3)与所有人中最伟大的野兽战斗:Null Pointer 是的!空指针并不总是在那里!实际上,实际上是最频繁发生的错误的原因!

让我们现在摆脱它,对吧?

练习很简单:我们需要返回一个格式正确的行,以便根据他收集的忠诚度点为客户打印适用的折扣:

public String getDiscountLine(Customer customer) {
        return "Discount%: " + getDiscountPercentage(customer.getMemberCard());
}
private Integer getDiscountPercentage(MemberCard card) {
        if (card.getFidelityPoints() >= 100) {
                return 5;
        }
        if (card.getFidelityPoints() >= 50) {
                return 3;
        }
        return null;
}

让我们看一下它返回60点然后返回10点的结果:

System.out.println(discountService.getDiscountLine(new Customer(new MemberCard(60))));
System.out.println(discountService.getDiscountLine(new Customer(new MemberCard(10))));

Prints:

Discount%: 3
Discount%: null

但是,我想向您的用户展示“ null”并不是您每天都想做的事情。显然,问题在于我们连接了一个潜在的null整数。修复它是微不足道的:

public String getDiscountLine(Customer customer) {
        Integer discount = getDiscountPercentage(customer.getMemberCard());
        if (discount != null) {
                return "Discount%: " + discount;
        } else {
                return "";
        }
}

Prints:

Discount%: 3

接下来,如果没有折扣,我们将返回一个空字符串。但是,为此,我们用空检查污染了我们的代码。而且,更糟的是,我们必须通过偷看该getDiscountPercentage()函数以查看何时可能返回a来找到我们遇到的问题null。但是,该技术无法在大型代码库中扩展。相反,您的API应该明确指出该函数可能不返回任何内容。

import java.util.Optional.*;
public String getDiscountLine(Customer customer) {
        Optional<Integer> discount = getDiscountPercentage(customer.getMemberCard());
        if (discount.isPresent()) {
                return "Discount%: " + discount.get();
        } else {
                return "";
        }
}
private Optional<Integer> getDiscountPercentage(MemberCard card) {
        if (card.getFidelityPoints() >= 100) {
                return of(5);
        }
        if (card.getFidelityPoints() >= 50) {
                return of(3);
        }
        return empty();
}

自然,我所做的就是用第Optional.isPresent()3行的调用替换了空检查。但是,有时候,突然出现在我们脑海中的第一个想法并不总是最好的。与一起玩时Optional,您需要反过来考虑。每当您尝试更改魔术盒.map(中的内容时,请使用)在该box上应用函数,以便仅在内部有东西的情况下变换box的内容。

public String getDiscountLine(Customer customer) {
return getDiscountPercentage(customer.getMemberCard())
      .map(d -> "Discount%: " + d).orElse("");
}

不仅代码更简洁,而且一旦习惯了这种样式,它也更易于阅读。

!我们只需要再做一个测试:没有的客户MemberCard

System.out.println(discountService.getDiscountLine(new Customer()));

Prints:

Exception in thread "main" java.lang.NullPointerException...

KABOOM!你去!我们经常惊慌失措,看看异常从何而来。在这里,它在getDiscountPercentage()函数的第一行中。这是由于我们从未处理过nullMemberCard参数有一些边界值()。让我们立即修复该问题-尽快隐藏该错误,并假装我们从未见过:

private Optional<Integer> getDiscountPercentage(MemberCard card) {
        if (card == null) {
                return empty();
        }
        if (card.getFidelityPoints() >= 100) {
                return of(5);
        }
        if (card.getFidelityPoints() >= 50) {
                return of(3);
        }
        return empty();
}

看,我们又冲了。而且,我们再次错过了一项设计见解(您看到模式了吗?)。我们在此处快速应用了防御性编程,并保护我们的代码免受所有其他无效数据的侵害。但是,据说最好的防御是进攻。如果,我们没有害怕担心地保护我们的代码,而是说:“等等。那么,客户可以不使用会员卡吗?然后,Customer.getMemberCard()应当返回一个Optional<MemberCard>。”

public class Customer {
          ...
          public Optional<MemberCard> getMemberCard() {
                   returnofNullable(memberCard);
          }
}

是的,我在这里谈论神圣的事情。我敢于更改域实体!但是,我相信我之所以更具表现力,是因为客户与会员卡之间的链接实际上是可选的(注意:字段和设置者仍使用'raw'类型MemberCard)。然后,代替测试null,我们测试是否.isPresent():

private Optional<Integer> getDiscountPercentage(Optional<MemberCard> cardOpt) {
        if (!cardOpt.isPresent()) {
                returnempty();
        }
...// Wait a bit!

停止!

我们一无所获!我会说这段代码变得更加丑陋!但是,这里有一条干净的代码规则:不要采用可为空的参数,因为您需要做的第一件事就是检查null。在Java 8中,这表示不采用Optional参数。

让我们再次扭曲我们的心灵,看到我们应该运用getDiscountPercentage()MemberCard,只要有一个会员卡。因此,让我们撤消更改以getDiscountPercentage()返回到,getDiscountLine()然后从开始Optional<MemberCard>

public String getDiscountLine(Customer customer) {
          return customer.getMemberCard()
                             .map(card -> getDiscountPercentage(card))
                             .map(d -> "Discount%: " + d)
                             .orElse("");
}

但是,输出结果可能会让我们感到惊讶:

Discount%: Optional[3]
Discount%: Optional.empty

这是因为第4行的d不再是an Integer,而是an Optional<Integer>。如果将鼠标悬停在第一个上,.map()然后查看返回类型:,您会自己看到它Optional<Optional<Integer>>。在这里,您在另一个框内的框内有一个数字。这就像将您的孩子的圣诞节礼物包装成多层嵌套包装一样,只是为了增加发现的乐趣。在我们的例子中,我们将使用的一元性质Optional和使用.flatMap(),而不是.map()摆脱多余的包装的。(要在寒冷的大风夜晚做:请阅读有关Monads的更多信息)。使用Optionals还有很多,但是以我作为培训师的经验,最困难的思维步骤是此处描述的步骤。

public String getDiscountLine(Customer customer) {
            return customer.getMemberCard()
                                    .flatMap(this::getDiscountPercentage)
                                    .map(d -> "Discount%: " + d)
                                    .orElse("");
}

因此,每当null给Java 8带来问题时,请立即跳转到Optional 并将转换函数应用于可能为空的魔术盒。干净的代码规则变为:不要采用Optional参数;相反,只要您的函数希望向调用者发信号通知在某些情况下可能没有返回值,则返回Optional。

4) The Loan Pattern/Passing a block 对于以下练习,让我们将订单导出到CSV文件:

public File exportFile(String fileName) throws Exception {
       File file = new File("export/" + fileName);
       try (Writer writer = new FileWriter(file)) {
              writer.write("OrderID;Date\n");
              repo.findByActiveTrue()
                     .map(o -> o.getId() + ";" + o.getCreationDate())
                     .forEach(writer::write);
              return file;
       } catch (Exception e) {
              // TODO send email notification
              log.debug("Gotcha!", e); // TERROR-Driven Development
              throw e;
       }
}

我将打开一个Writer,对所有订单进行流处理,将它们转换,然后将它们分别写入文件。在这个例子的最后,隐隐的恐惧气味源于一种可能性,即以后可能没有人会抓住我的例外。理想情况下,您应该相信您的团队具有这些异常,这样,如果将它们抛出在任何线程上,就可以正常地捕获和记录它们。

完美的!

但是,它无法编译!

这是因为Writer.write()方法声明了throws IOException,即使的Consumer期望值.forEach没有。如果抛出检查异常,您应该受苦!但是,如何隐藏JDK类抛出的已检查异常?我们可以将第7行扩展为一个内联lambda,然后在其中执行以下操作:

try {...} catch(IOException) {throw new RuntimeException(e);}

但是,阅读本文会伤害我们的眼睛。我们可能应该立即以某种方法将其掩埋,否则我们可以让jOOL做到这一点。然后,第7行变成:

.forEach(Unchecked.consumer(writer::write));

我们很高兴回家...

.forEach(Unchecked.consumer(writer::write));

在执行此操作时,有很多方法可能会偏离正义之路,包括布尔值,枚举ExportType和@Overriding具体方法(我在我的演讲中提到了其中的一些方法,只是为了好玩),但在此我将概述以下方法的应用的模板方法设计模式[GoF]。

abstract class FileExporter {
          public File exportFile(String fileName) throws Exception {
                   File file = new File("export/" + fileName);
                   try (Writer writer = new FileWriter(file)) {
                             writeContent(writer);
                             return file;
                   } catch (Exception e) {
                             // TODO send email notification
                             throw e;
                   }
          }
          protected abstract void writeContent(Writer writer) throws IOException;
}
class OrderExporter extends FileExporter{
          private OrderRepo repo;
          @Override
          protected void writeContent(Writer writer) throws IOException {
                   writer.write("OrderID;Date\n");
                   repo.findByActiveTrue()
                             .map(o -> o.getId() + ";" + o.getCreationDate())
                             .forEach(Unchecked.consumer(writer::write));
          }
}
class UserExporter extends FileExporter {
          @Override
          protected void writeContent(Writer writer) throws IOException {
                   ...
          }
}

我想让你问自己:我们为什么在那使用那个危险的词?我们为什么玩火?extends代码中如此糟糕的借口是什么?为了强制我提供缺少的逻辑,f(Writer):void每当我将子类化时,都会有一个函数FileExporter

但是,我们可以在Java 8中更轻松地做到这一点!我们只需要将aConsumer<Writer>作为方法参数!

class FileExporter {
          public File exportFile(String fileName, Consumer<Writer> contentWriter) throws Exception {
                   File file = new File("export/" + fileName);
                   try (Writer writer = new FileWriter(file)) {
                             contentWriter.accept(writer);
                             return file;
                   } catch (Exception e) {
                             // TODO send email notification
                             throw e;
                   }
          }
}
class OrderExportWriter {
          private OrderRepo repo;
          public void writeOrders(Writer writer) throws IOException {
                   writer.write("OrderID;Date\n");
                   repo.findByActiveTrue()
                             .map(o -> o.getId() + ";" + o.getCreationDate())
                             .forEach(Unchecked.consumer(writer::write));
          }
}
class UserExportWriter {
          protected void writeUsers(Writer writer) throws IOException {
                   ...
          }
}

哇,这里发生了很多变化。该函数代替abstractextendsexportFile()获得了一个新Consumer<Writer>参数,调用该参数可写入实际的导出内容。为了获得整体情况,让我们草绘客户端代码:

fileExporter.exportFile("orders.csv", Unchecked.consumer(orderWriter::writeOrders));
fileExporter.exportFile("users.csv", Unchecked.consumer(userWriter::writeUsers));

在这里,我不得不Unchecked再次使用它来进行编译,因为writeOrders()声明它会引发异常!或者,如果您是Lombok的粉丝,则可以在@SneakyThrows上放一个writeOrders(),然后从其中删除throws子句。您可以自己尝试。我在专用的GitHub存储库上执行了所有步骤(包括这一步)。我不会争论这是否是一个好习惯,我只是在玩。另外,请注意,要进行尝试,您将必须在IDE上配置Lombok。

基本思想是,只要您具有一些“可变逻辑”,就可以考虑将其用作方法参数。在我的培训中,我将其称为“通过块”模式。但是,上面的示例略有变化,其中作为参数给出的功能与由主机功能管理的资源一起使用。在我们的示例中,OrderExportWriter.writeOrders接收一个Writer作为参数将内容写入其中。但是,writeOrders与创建,关闭或处理与相关的错误无关FileWriter。这就是为什么我们称之为THI š贷款模式。我们传入的这个函数本质上是借来的,因此Writer可以完成它的工作。

贷款模式的一个主要好处是,它与基础结构代码(FileExporter)与实际的导出格式逻辑(OrderExportWriter)很好地分离了。通过抽象层,较好的分离,这种格局使得依赖在v版为,即你可以保持OrderWriter在一个更大的内部层。由于它很好地解耦了代码,因此设计和单元测试变得容易得多。您可以writeOrders()通过传递a进行测试StringWriter,然后查看写入其中的内容。要测试FileExporter,您可以仅传递一个Consumer只写“ dummy”的虚拟对象,然后验证基础代码是否能完成其工作。

而且,这一切都没有任何扩展。众所周知,扩展重用逻辑会对设计造成长期损害。因此,在Java 8中可以使用Passing-a-Block模式的优雅方式可能是见证模板方法设计模式的葬礼,因为它实现了几乎相同的目标,而无需强迫您扩展任何内容。

“通过块”模式的另一种变体是“执行环绕”模式。从语法上讲,代码非常相似:

measure(() -> stuff());
executeInTransaction(() -> stuff());

但是,目的略有不同。在这里,stuff()已经实现了,但是之后,我们想要围绕它执行一些任意代码(之前和之后)。使用Execute Around,我们在一些辅助函数中编写此代码之前/之后,然后将原始调用包装在对该辅助对象的调用中。如果我们仔细观察第二行,它闻起来像面向方面的编程(AOP),对吗?使用Spring,我们通常只是@Transactional使用一种方法来获取该方法的事务。为了使TransactionInterceptor发挥作用,您需要调用@TransactionalSpring所提供的对此类的(代理)引用上的方法(并非总是如此)。因此,以上两行都涉及需要临时编织的情况,即在任意实用工具函数中运行函数并在代码之前和/或之后运行。

免责声明:模式名称在您可以从Internet上阅读的文章中互换使用。

本部分的主要内容是,您应该强迫自己思考如何处理逻辑部分,并将其作为Java 8中的一等公民来使用。

这将使您的代码更加优雅,简单和富于表现力。

5)实现特定类型逻辑的五种方法 任务很简单:有三种电影类型,每种类型都有其自己的公式,用于根据借出的天数来计算价格。

class Movie {
        enum Type {
                REGULAR, NEW_RELEASE, CHILDREN
        }
        private final Type type;
        public Movie(Type type) {
                this.type = type;
        }
        public int computePrice(int days) {
                switch (type) {
                case REGULAR: return days + 1;
                case NEW_RELEASE: return days * 2;
                case CHILDREN: return 5;
                default: throw new IllegalArgumentException(); // Always have this here!
                }
        }
}

如果我们测试:

System.out.println(new Movie(Movie.Type.REGULAR).computePrice(2));
System.out.println(new Movie(Movie.Type.NEW_RELEASE).computePrice(2));
System.out.println(new Movie(Movie.Type.CHILDREN).computePrice(2));

我们得到:

3
4
5

该示例是Bob叔叔的经典视频商店代码Kata的提炼。上面代码中的问题可能是switch:每当向枚举添加新值时,都需要查找所有开关并确保处理新情况。但这是脆弱的。该IllegalArgumentException会弹出,但只有当你走这条道路,从测试/ UI / API。简而言之,尽管任何人都可以阅读此代码,但这样做有些冒险。

规避风险的一种方法是OOP解决方案:

abstract class Movie {
        public abstract int computePrice(int days);
}
class RegularMovie extends Movie {
        public int computePrice(int days) {
                return days+1;
        }
}
class NewReleaseMovie extends Movie {
        public int computePrice(int days) {
                return days*2;
        }
}
class ChildrenMovie extends Movie {
        public int computePrice(int days) {
                return 5;
        }
}

如果您创建一种新型的电影,实际上是一个新的子类,那么除非您实现,否则代码不会编译computePrice()。但是,extends又来了!如果要按其他标准(例如发行年份)对电影进行分类怎么办?还是几个月后您将如何处理从AType.NEW_RELEASEType.REGULAR电影的“降级” ?

让我们回到第一种形式,让我们寻找实现它的其他方式。在我的Devoxx演讲中,我还现场编码了如何使用枚举上的抽象方法来实现此逻辑。但是,在这里,我想直接提出更改请求:“新发行电影的价格公式中的因素(在我们的示例中为2)必须可以通过数据库进行更新。”

uch!这意味着我必须从某些注入的存储库中获取此因素。但是,由于我无法在自己的Movie实体中注入存储库,因此让我们将逻辑移到一个单独的类中:

public class PriceService {
        private final NewReleasePriceRepo repo;
        public PriceService(NewReleasePriceRepo repo) {
                this.repo = repo;
        }
        public int computeNewReleasePrice(int days) {
                return (int) (days * repo.getFactor());
        }
        public int computeRegularPrice(int days) {
                return days + 1;
        }
        public int computeChildrenPrice(int days) {
                return 5;
        }
        public int computePrice(Movie.Type type, int days) {
                switch (type) {
                case REGULAR: return computeRegularPrice(days);
                case NEW_RELEASE: return computeNewReleasePrice(days);
                case CHILDREN: return computeChildrenPrice(days);
                default: thrownew IllegalArgumentException();
                }
        }
}

但是,switch固有的风险又回来了!有什么方法可以确保没有人忘记定义相关的价格公式?

现在,对于大结局:

public class Movie { 
       public enum Type {
              REGULAR(PriceService::computeRegularPrice),
              NEW_RELEASE(PriceService::computeNewReleasePrice),
              CHILDREN(PriceService::computeChildrenPrice);
              public final BiFunction<PriceService, Integer, Integer> priceAlgo;

              private Type(BiFunction<PriceService, Integer, Integer> priceAlgo) {
                     this.priceAlgo = priceAlgo;
              }
       }
       ...

}

并且,而不是switch:

class PriceService {
       ...
       public int computePrice(Movie.Type type, int days) {
              return type.priceAlgo.apply(this, days);
       }
}

?!!?!

我将每个方法的引用存储到每个枚举值中PriceService。由于我以静态方式(从PriceService::)引用实例方法,因此我需要PriceService在调用时将实例作为第一个参数提供。我给它this。这样,我可以从枚举值定义的静态上下文中的任何[Spring] bean中有效地引用方法。

在我所举办的各种Coding Dojos中,开发人员也提出了建议Map<Type,Function<>>,但与一门老派相比,switch, 它没有任何真正的好处-如果添加新的影片类型,编译仍然不会中断,但是代码变得更加神秘。

结论

简而言之:

  • 始终将复杂的lambda提取到您使用引用的命名函数中::。你不应该写-> {
  • 避免过多的调用链接-使用解释性方法和变量将它们分开,尤其是当这些调用的返回类型有所不同时。
  • 每当null让您感到烦恼时,请考虑使用Optional。改变主意-您必须将功能应用于魔术盒。
  • 当变量事物是一个函数时,要意识到这一点,并且您明确地使用它,将一个函数传递给另一个函数。
  • 贷款模式意味着让您提供的功能作为您用于“主机”功能管理的资源的参数。从概念上讲,这使重量更轻,耦合松散并且易于测试设计。
  • 有时,您可能希望有一些任意代码来围绕另一个函数执行。如果是这种情况,请将该代码作为参数传递给函数。
  • 您可以使用方法引用将类型特定的逻辑挂接到您的枚举,以确保每个枚举值都与逻辑的相应位相关联。 始终以尽可能简单的代码为目标,并积极提炼您的代码,直到它变得微不足道/“无聊”为止。


原文链接:http://codingdict.com/