functional-programming - Java 8 函数式 VS 命令式方法

标签 functional-programming java-8 refactoring naming-conventions

我创建了一种基于 Bean 属性动态构建 rest URI 的方法,最初是命令式的,然后我将其重构为函数式风格,这是我第一次进行函数式编程。 命令式和函数式都按预期工作,但我对函数式可读性不满意,函数式接缝对这种方法来说太过分了,或者可能是因为我还是一个新手函数式程序员!

您如何将此方法重构为更简洁的功能方式?

还是保持命令式?

import java.beans.PropertyDescriptor;
import java.lang.reflect.InvocationTargetException;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.lang.reflect.Method;

import org.springframework.beans.BeanUtils;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.web.util.UriComponentsBuilder;

public String functionalBuildRestUri() throws Exception {

    final UriComponentsBuilder uriBuilder = UriComponentsBuilder.newInstance().scheme("https")
            .host("foo.com").path("/offers");
    //here is the functional 
    List<PropertyDescriptor> propDescList = Arrays.asList(BeanUtils.getPropertyDescriptors(getClass()));

    //this part is readable and precis, but to enable it had to add 4 methods 
    propDescList.stream().filter(notClassProp())
                         .filter(notNullPropValue())
                         .collect(Collectors.toMap(PropertyDescriptor::getName, propValue()))//conversion to map doesn't feel good to me how can I avoid it?
                         .forEach(buildRestParam(uriBuilder));

    return uriBuilder.build().toUriString();
}


public String imperativeBuildRestUri() throws Exception {
     final UriComponentsBuilder uriBuilder = UriComponentsBuilder.newInstance().scheme("https")
                .host("foo.com").path("/offers");



    PropertyDescriptor[] propDescArray = BeanUtils.getPropertyDescriptors(getClass());
    for (PropertyDescriptor propDesc : propDescArray) {

        String propName = propDesc.getName();
        if (!propName.equals("class")) {
            Method getPropMethod = propDesc.getReadMethod();
            Object propValue = getPropMethod.invoke(this);
            if (propValue != null) {
                if(propValue instanceof Date){
                    String dateStr = new SimpleDateFormat(DATE_FORMAT).format((Date)propValue);
                    uriBuilder.queryParam(propName, ":"+dateStr);
                }else{
                    uriBuilder.queryParam(propName, propValue);
                }
            }
        }
    }

    return uriBuilder.build().toUriString();
}

所有这些方法都是在功能重构后添加的

// I couldn't avoid being imperative here, how can we refactor it to more functional style
 private BiConsumer<String, Object> buildRestParam(final UriComponentsBuilder uriBuilder) {
    return (propName, propValue) -> {
        if (propValue instanceof Date) {
            String dateStr = new SimpleDateFormat(DATE_FORMAT).format((Date) propValue);
            uriBuilder.queryParam(propName, ":" + dateStr);
        } else {
            uriBuilder.queryParam(propName, propValue);
        }
    };
}

private Predicate<? super PropertyDescriptor> notNullPropValue() {
    return propDesc -> {

        return propValue().apply(propDesc) != null;

    };
}


private Predicate<? super PropertyDescriptor> notClassProp() {
    return propDesc -> {
        return !propDesc.getName().equals("class");
    };
}

private Function<? super PropertyDescriptor, ? extends Object> propValue() {
    return (propDesc) -> {
        try {
            return propDesc.getReadMethod().invoke(HotelOfferSearchCommand.this);
        } catch (IllegalAccessException e) {
            e.printStackTrace();
            throw new RuntimeException(e);
        } catch (IllegalArgumentException e) {
            e.printStackTrace();
            throw new RuntimeException(e);
        } catch (InvocationTargetException e) {
            e.printStackTrace();
            throw new RuntimeException(e);
        }
    };
}

最佳答案

新代码的大部分冗长与函数式编程无关。您重构了代码,将每个 lambda 表达式放入它自己的方法中,这当然破坏了 lambda 表达式的主要优点之一,即紧凑性。即使代码足够复杂以证明创建方法是合理的,该方法也应该执行实际工作,然后,您可以在需要函数的地方使用方法引用。

这些方法还受到不必要(甚至不鼓励,因为在返回类型中)使用通配符的困扰。您还使用了冗长的语法 parameter -> { return expression; }其中 parameter -> expression有可能。

还有其他问题,比如不必要地创建一个独特的 catch每个异常类型的子句,当所有异常类型都做相同或将数组包装到 List 中时在创建 Stream 之前不是直接流过数组或有代码重复,最后一点适用于命令式变体和函数式变体。

你可以这样写:

public String functionalBuildRestUri() throws Exception {
    final UriComponentsBuilder uriBuilder = UriComponentsBuilder.newInstance()
        .scheme("https").host("foo.com").path("/offers");
    Function<PropertyDescriptor, Object> propValue = propDesc -> {
        try { return propDesc.getReadMethod().invoke(HotelOfferSearchCommand.this); }
        catch(ReflectiveOperationException e) { throw new RuntimeException(e); }
    };
    Arrays.stream(BeanUtils.getPropertyDescriptors(getClass()))
          .filter(propDesc -> !propDesc.getName().equals("class"))
          .filter(propDesc -> propValue.apply(propDesc) != null)
          .forEach(propDesc -> {
              Object value = propValue.apply(propDesc);
              if (value instanceof Date)
                  value = ":"+new SimpleDateFormat(DATE_FORMAT).format(value);
              uriBuilder.queryParam(propDesc.getName(), value);
          });
    return uriBuilder.build().toUriString();
}

没有任何额外的方法。

这可能不是最佳选择,因为确实存在一个缺陷,即缺少元组或对类型来保存要通过流传递的两个值。通过使用 Map.Entry作为替身,但不填充 Map ,我们可以将操作表示为

public String functionalBuildRestUri() throws Exception {
    final UriComponentsBuilder uriBuilder = UriComponentsBuilder.newInstance()
        .scheme("https").host("foo.com").path("/offers");
    Function<PropertyDescriptor, Object> propValue = propDesc -> {
        try { return propDesc.getReadMethod().invoke(HotelOfferSearchCommand.this); }
        catch(ReflectiveOperationException e) { throw new RuntimeException(e); }
    };
    Arrays.stream(BeanUtils.getPropertyDescriptors(getClass()))
          .filter(propDesc -> !propDesc.getName().equals("class"))
          .map(propDesc -> new AbstractMap.SimpleImmutableEntry<>(
                               propDesc.getName(), propValue.apply(propDesc)))
          .filter(entry -> entry.getValue() != null)
          .forEach(entry -> {
              Object value = entry.getKey();
              if (value instanceof Date)
                  value = ":"+new SimpleDateFormat(DATE_FORMAT).format(value);
              uriBuilder.queryParam(entry.getKey(), value);
          });
    return uriBuilder.build().toUriString();
}

或者,或者,

    Arrays.stream(BeanUtils.getPropertyDescriptors(getClass()))
          .filter(propDesc -> !propDesc.getName().equals("class"))
          .map(propDesc -> new AbstractMap.SimpleImmutableEntry<>(
                               propDesc.getName(), propValue.apply(propDesc)))
          .filter(entry -> entry.getValue() != null)
          .map(e -> e.getValue() instanceof Date?
                  new AbstractMap.SimpleImmutableEntry<>(e.getKey(),
                        ":"+new SimpleDateFormat(DATE_FORMAT).format(e.getValue())):
                  e)
          .forEach(entry -> uriBuilder.queryParam(entry.getKey(), entry.getValue()));

有了这两个变体,propValue每个元素只对函数求值一次,而不是像在第一个变体和您的原始代码中那样求值两次,两者都检查 null。属性值和终端操作对其进行评估。

请注意,仍有改进的余地,例如没有理由添加 ":"format之后当您首先使冒号成为格式模式字符串的一部分时的操作。

这是否是对循环的改进,您必须自己决定。并非每个代码都必须重写为函数式风格。至少,如上面的示例所示,它不必比命令式代码更大......

关于functional-programming - Java 8 函数式 VS 命令式方法,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/44890043/

相关文章:

list - SML:如何确定列表索引是否为空?

oop - 帮助设计树结构 - 函数式和 OOP 之间的张力

haskell - 在 Haskell 中推导是什么/意味着什么?

java - 从列表中的任何值获取键

java - IntStream.boxed() 与 for 循环 |表现

class - 你如何重构一个上帝类?

python - 如何减小 5 层 for 循环的大小

clojure - 在 clojure 的循环中改变数据是否可以?

java - 构造函数引用 - 创建泛型数组时没有警告

user-interface - 你应该在 UI 类中放入多少逻辑?