实用功能型Java简介

103 阅读11分钟

实用功能型Java介绍

本文试图帮助开发者以一种新的Java编码风格进行编写,这种风格被称为务实的功能Java(PFJ)。PDF是通过使用编译器生成的干净、可信、可理解的代码。

即使Java 8继续采用这种编码风格,Java 11也大大精简和明确了它。随着Java 17的出现,它变得更加具有描述性,并从每一个新的Java功能中获益。

Java对开发者的工作习惯和方法论造成了重大调整。改变习惯并不是一件简单的事情,当这些习惯很重要并且已经存在了很长时间的时候,它就更加困难了。

实用功能的Java组件

PFJ使用功能实用主义(FP)的概念,但并不试图执行功能实用主义的具体术语。

在服务于实际目的的意义上,实用主义有很多方面是Java所强调的。

比如说。

  • 降低心理负担。
  • 增加代码的可依赖性。
  • 增加系统的长期维护能力。
  • 为了帮助构建无错误的代码,你可以使用一个编译器。
  • 如果我们想让你的代码看起来和感觉自然,这将更难创造出不正确的代码。

PFJ有崇高的目标,然而,只有两条规则需要遵循。

  • 避免使用null 对象。
  • 在商业世界中,在这种情况下根本不应该有例外。

我们将对这些规则逐一进行深入研究。

ANAMAP规则,避免使用空部件

变量的可空性是一个Special State 。他们在程序中使用的模板代码和他们引入的运行时故障是常见的。为了绕过这些问题,利用PFJ的Option<Q> 容器来容纳任何缺失的值。输入的数据和字段以及返回的结果都包括在内。

一个类可以在内部利用值null ,以提高效率或在特定的实例中保留向后的兼容性。Option<Q> ,应该总是被仔细指定,在某些实例中对类的用户不明显,这样每个类的API都可以利用它。

使用这种策略有很多好处。

  • 很容易看到什么时候有可归零变量的代码。不需要阅读任何东西,因为文档、源代码和注解都是可以信任的。
  • 它们是不同类型的nullable,所以它们不会被错误地分配。
  • 这个用于null 检查的模板脚本之前已经从代码库中完全删除。

NBE规定,无效业务例外

灾难性的(技术)缺陷是PFJ中的例外,而不是正常的错误。当这个异常被抛出时,没有办法优雅地结束应用程序。严格不鼓励异常和拦截。

Special States 也可能包括业务异常。它利用 容器来处理企业级问题。这包括返回值、输入参数和字段。对字段使用这种容器是很罕见的。Result<Q>

企业级的例外情况只有在需要时才会被授权。旧的Java库可以通过使用包装方法来进行交互。包装是由Result<Q> 容器支持的。这个容器实现了它们。

不对空业务例外规则进行任何例外,有以下好处。

  • 源代码中充斥着错误。花费在阅读上的时间是不必要的。如果你想知道什么时候和什么情况下可能会抛出什么异常,请看一下调用树、文档和源代码。
  • 在编译过程中,编译器会保证进行适当的错误管理和传播。错误管理和传播有少量的模板代码。
  • 对于一切按计划进行的情况,故障可以在最方便的时候解决,可以编写程序来适应这种情况。
  • 这种代码很容易阅读和理解,因为在执行序列中没有意外的中断或过渡。

遗留代码到PFJ格式代码的转换

有准则是非常好的,但是如何才能准确的写出代码呢?

让我们从一些基本的后端功能开始。

public interface ClientRepository
{
    Client findByIdentity(Client.Identity clientIdentity);
  }

   public interface ClientProfileRepository
  {
   ClientProfile findByIdentity(Client.Identity clientId);
     }

   public class ClientService
      {
   private final ClientRepository clientRepository;
   private final ClientProfileRepository clientProfileRepository;

   public ClientWithProfile getClientWithProfile(Client.Identity clientIdentity)
          {
           Client client = clientRepository.findByIdentity(clientIdentity);

           if (client == null) {
           throw ClientNotFoundException("Clients with an ID card " +
           clientIdentity + " page not found");
         }

    ClientProfile details = clientProfileRepository.findByIdentity    (clientIdentity);

            return ClientWithProfile.of(client, details == null
            ? ClientProfile.defaultDetails()
            : details);
    }
}

解释

这个例子使用接口来提供上下文的清晰性。最关键的方面是getClientWithProfile 方法。

让我们一步步来研究这个。

第一行在客户端数据库中找到客户端变量的值的存储库。如果客户端不在存储库中,用户变量将为空。如果值是空的,那么就引发一个可抛的业务异常。

一旦客户的资料信息被检索出来,下面的步骤就是获得对它的访问。没有任何细节,这不是一个错误。当要求不充分时,就会利用默认的配置文件。

在上面这段代码中可能会发现许多缺陷。当资源库不包含任何值时,空值是一个坏主意,但这一点在用户界面上并没有说清楚。

在我们对这些资源库的文档和实现进行彻底调查之前,我们需要对它们的工作方式做出明智的猜测。

然而,即使利用注释来提供建议,也不能保证API的功能。

储存库将受制于以下标准来解决这个问题。

public interface ClientRepository
  {
    Option<Client> findByIdentity(Client.Identity clientIdentity);
      }

public interface ClientProfileRepository
      {
    Option<ClientProfile> findByIdentity(Client.Identity clientIdentity);
}

因此,不需要做任何假设,API会明确说明返回的项目是否存在。

让我们再看一下getUserWithProfile 方法。该过程也有可能引发一个异常,而不是返回一个值。

因为这是一个与业务有关的豁免,所以可以应用这个规则。这一变化的根本目的是在方法可能提供一个值或一个错误的可能性上放一个免责声明。

与遗留代码的衔接

现有的代码并不遵守PFJ准则。当异常被触发时,诸如NullUndefined 等值将被返回。然而,重写代码以使其与PFJ兼容并不总是一种选择。特别是,这对第三方库和框架是有效的。

使用遗留代码

调用旧代码有两个缺点。每一个实例都可能被追溯到对相应PFJ法规的侵犯。

解决业务异常

lift() 在 ,包含了大多数使用场景的方便方法。Result<Q>

一个Result<Q> 对象很可能是由一个异常创建的,作为一个Cause instance. 的调用。另一个参数是一个lambda,它将PFJ兼容的代码封装在其函数中。

一个可抛出的异常可以使用Causesutility 类的fromThrowable() 函数变成一个Cause的实例。

通过这些函数的组合,使用方法Result.lift() ,可以得到以下结果。

public static Results<> buildURI(String uri)
 {
    return Results.lift(Cause::fromThrowable, () -> URI.build(uri));
}

如果你对一个空值进行处理会发生什么?option.option() 方法可用于封装一个返回null的<Q> API响应。

提供一个旧式的API

旧的代码经常需要使用PFJ风格的代码来运作。如果你正在使用一个旧的API,你可能不得不保留它,以便与现代的PFJ方法兼容。正因为如此,你会想先创建一个新的PFJ风格的API,然后再创建一个传统的适配器。

有一些简单的技术可能是很方便的。

public static <Q> Q unwrap(Results<Q> values) {
    return values.fold(
        reason -> { throw new StatesException(cause.remark()); },
        info -> info
    );
}

因为这些因素,Result<Q> 中没有现成的辅助技术。

  • 被检查和未被检查的特定情况的异常会以各种方式抛出。
  • 根据不同的用例,原因可能会被转化为各种各样的各种异常。

管理变量作用域

在这一节中,我们将看看在开发PFJ风格的代码时可能出现的各种实际情况。

请注意,下面的例子假定应该使用option<Q> ,而不是Result<Q> ,尽管这完全没有意义,因为所有的关注点对于两种选择都是一样的。而不是抛出异常,函数调用将被转换为Result<Q>

  1. 嵌套作用域这些容器使用大量的lambdas来对它们持有的数据进行计算和操作。它们只能在它们的lambda主体内部使用,因为每个lambda都自动指定了它的参数范围。

义务式编程并不经常使用这个,如果你这样做,可能会导致误解。幸运的是,对这个问题有一个直接的解决方案。

考虑一下下面这段命令式代码。

variable value01 = function01(...);
variable value02 = function02(value1, ...);
variable value03 = function03(value1, value2, ...);

为了让function02function 03 能够访问值01,他们应该调用value01() 。换句话说,立即从PFJ风格转换是行不通的。

    function01(...)
       .flatMap(value01 -> function02(value01, ...))
       .flatMap(value02 -> function03(value01, value02, ...));

解释

Value01不能被访问,因为有一个错误。我们必须利用一个嵌套的作用域,即嵌套调用,以保持该值的可用性。

 function01(...)
       .flatMap(value01 -> function02(value01, ...)
           .flatMap(value02 -> function03(value01, value02, ...)));

解释

第一个flatMap利用函数1的值作为返回值,而flatMap02对函数02的元素也是如此。function03现在可以访问和使用value1,因为它仍然在作用域内。

嵌套的作用域越多,理解代码就越困难。在这种情况下,强烈建议扩大一个函数的作用域。

  1. 平行作用域许多不相关的变量必须在开始构建一个项目之前被计算或检索出来。考虑一下下面的例子。
variable value01 = function01(...);
variable value02 = function02(...);
variable value03 = function03(...);

return new MyObj(value01, value02, value03);

乍一看,转移到PFJ风格的作用域似乎很像转换到嵌套作用域。在命令式编程中,所有的值都是同样可见的。如果需要众多的值,作用域会变得过于嵌套,这是一件坏事。

当使用option<Q> ,所有的Result<Q> 方法都可以访问。这样一来,所有的计算都在parallel ,结果就是一个独立的MapperX<...> 用户界面。

在这个界面上总共有三个方法,每个方法都以不同的返回值命名。这些方法接受输入的lambdas,它们的行为与option<Q>Result<Q> 中的同等方法完全一样。这是一个用PFJ风格重写的命令式代码的例子。

return Result.all(
          function01(...),
          function02(...),
          function03(...)
        ).map(MyObj::new);

解释

这种方法提供了一些额外的好处,比如说扁平化和小型化。首先,它通过做大量的计算,然后保存所有的结果来证明其目的。

命令式编程的顺序模式使得人们很难看到最终的目的。第二个原因是,由于每个数字都是单独计算的,所以没有其他的值被扔到混合中。由于背景较少,所以更容易理解和证明每个函数的调用。

选项和结果的简要技术概述

这两个实体在函数式编程中被称为单体。

为了实现单体,我们使用它的基本变体,即Option<Q>

Result<Q> 因此,是Either的 接口的一个精简和集中的版本。由于所需类型的减少,更集中的API与 ,但它也牺牲了普遍性和灵活性。Either<L,R> Option<Q>

两个方面是这个实现的重点。

  • 将这个类与其他JDK类一起使用应该没有问题,比如Optional<Q>Stream
  • API的设计是为了使目的声明更容易被理解。

每个容器只有几个基本的方法,比如说这些。

  • 在保存状态的同时,Option<Q> 仍然可以使用地图转换功能来改变数值。因此,最后的结果仍然是有利的。
  • 例如,通过使用flatMap()方法,一个成功的Result<Q> 可能会变成一个失败。这可能会改变应用程序的当前状态。
  • 为了同时处理这两种情况,即当前/空以及成功/失败,Result<Q> 包含了一个 fold() 函数。

总结

通过这篇文章,我们现在对Java编程语言中的实用主义函数有了基本的了解。

它是一种当代的、基于函数式编程的Java编码语言,简单易懂。此外,我们还演示了如何将它付诸行动。