欢迎浏览我的博客 获取更多精彩文章
从命令式编程到函数式编程(一)
函数式编程有什么好处?
为什么要将代码从命令式编程转到函数式编程?
请参见下文,此处不再赘述,本文的重点是如何将命令式编程的代码转化为函数式编程
一个邮箱验证的程序
最开始的命令式编程
final Pattern emailPattern = = Pattern.compile("^[a-z0-9A-Z._-]+@[a-z0-9.-]+\\.[a-z]{2,4}$");
void testMain(String email){
if(emailPattern.matcher(email).matches()){
System.out.println("Send");
}else {
System.out.println("Error")
}
}
//在此处为了表示方便,用简单的输出语句表示匹配结果
这样看来,程序虽然很简单,但是却有着很低的可拓展性和健壮性,对错误的邮箱地址应该有不同的处理以及可以拓展对不同结果的处理方法.而且在耦合的角度看来,匹配字符串是一个计算的操作,而对匹配结果的表示是一个作用,应该要将操作和作用互相分离,来降低耦合度
我们在这里可以用一个函数来表示匹配邮箱地址这个操作
final Pattern emailPattern = = Pattern.compile("^[a-z0-9A-Z._-]+@[a-z0-9.-]+\\.[a-z]{2,4}$");
final Function<String,Boolean> emailChecker = s->emailPattern.matcher(email).matches();
void testMain(String email){
if(emailChecker.apply(email)){
System.out.println("Send");
}else {
System.out.println("Error")
}
}
增加程序的健壮性
接下来我们处理如何去增加程序健壮性的问题,首先,我们要思考一个问题,在匹配结果中,我们只对成功与否做出了匹配,但是这样是不够的,我们还要对字符串不同的情况做出处理,如
String s1 = "";
String s2 = null;
String s3 = "123456";
为了处理这些结果,我们可以定义一个组件来表示计算的结果
public interface Result<T> {
static <T> Result<T> failure(String message) {
return new Failure<T>(message);
}
static <T> Result<T> success(T value) {
return new Success<>(value);
}
/**
* 表示计算成功
*/
public class Success<T> implements Result {
private final T value;
private Success(T t) {
value = t;
}
}
/**
* 表示计算失败
*/
public class Failure<T> implements Result {
private final String errorMessage;
private Failure(String m) {
errorMessage = m;
}
}
}
通过这样的方法,我们就可以方便地且有拓展性地处理错误
final Pattern emailPattern = = Pattern.compile("^[a-z0-9A-Z._-]+@[a-z0-9.-]+\\.[a-z]{2,4}$");
final Function<String,Boolean> emailChecker = s->{
if(s==null){
return Result.failure("email must not be null");
}else if(s.length==0){
return Result.failure("email must not be empty");
}else if(!emailPattern.matcher(email).matches()){
return Result.failure("email "+s+" is invaild")
}else {
return Result.success();
}
}
static void validate(String email){
Result result = emailChecker.apply(s);
if(result instanceof Result.Success){
System.out.println("Email send")
}else {
System.out.println("Error: "+((Result.Failure)result).getMessage());
}
}
这样,我们不仅创建了一个有良好拓展性的程序,还令其能够方便地处理错误.
但是,这样子是远远不够的,这个程序只能够被成为比较好看的命令式编程,而不是函数式编程
消除函数的副作用
在下一步,我们要在validate函数中动手脚,消除例如instanceof和类型转换这样的代码,并且消除这个函数的副作用(具体来说,要将输出函数抽象成一个行为)
为了消除副作用,我们要定义这样一个可执行代码的函数式接口
@FunctionalInterface
public interface Executable {
void exec();
}
于是,validate函数就被修改成如下这样:
static Executable validate(String email){
Result result = emailChecker.apply(s);
(result instanceof Result.Success)
?
return ()->System.out.println("Email send")
:
return ()->System.out.println("Error: "+((Result.Failure)result).getMessage());
}
只要我们调用Executable的exec函数,就可以用来执行结果了.并且,将ifelse表达式用三目运算符进行表示,让其看起来更加"函数式",但是,实际上,如果非要用三目运算符,建议不要嵌套多个使用,这样不仅看起来逻辑凌乱,而且修改也不方便.
将验证逻辑与行为解耦
接下来,我们要做的是将验证逻辑从应用的作用中解耦.现在的validate函数可重用性比较低,当我们验证了一个结果后,只能选择执行与否,而且验证方法不应该局限于表示电子邮件是否有效,我们也可以随时选择成功与否产生的作用.
具体来说,我们应该创建一个表示作用的类,将验证后要做的事情与这个类绑定在一起,由于emailChecker方法会返回一个结果Result,其信息量已经足够我们对validate进行解耦了,所以我们不再需要validate函数,而是将Result与作用绑定(bind)起来.
我们需要创建一个表示作用的接口
public interface Effect<T>{
void apply(T t);
}
这个接口只是我们的一个命名,事实上,它和Java8中定义的Consumer接口作用是一样的.
上面说过了,我们知道Result返回的结果抽象起来只有两个,就是验证不通过或者验证通过,但是不同结果导致的行为我们是可以自由定义并进行更换的,所以我们要更改Result接口,将成功和失败的行为和Result的对象绑定在一起
更改Result接口后的代码如下:
/**
* @author Boyn
* @date 2019/8/18
* @description 表示计算的结果
*/
public interface Result<T> {
static <T> Result<T> failure(String message) {
return new Failure<T>(message);
}
static <T> Result<T> success(T value) {
return new Success<>(value);
}
/**
* bind函数用于绑定验证之后,要进行的行为
* @param success 绑定验证成功后的行为
* @param failure 绑定验证失败后的行为
*/
void bind(Effect<T> success, Effect<String> failure);
/**
* 表示计算成功
*/
public class Success<T> implements Result {
private final T value;
private Success(T t) {
value = t;
}
@Override
public void bind(Effect success, Effect failure) {
success.apply(value);//执行行为
}
}
/**
* 表示计算失败
*/
public class Failure<T> implements Result {
private final String errorMessage;
private Failure(String m) {
errorMessage = m;
}
@Override
public void bind(Effect success, Effect failure) {
failure.apply(errorMessage);
}
}
}
在这个代码中,我们看到Result接口新增了一个bind方法,参数就是成功和失败后的行为,是由用户自定义的.
可以通过这样的方式绑定行为并运行
static Effect<String> success = s -> System.out.println("Mail send to " + s);
static Effect<String> failure = s -> System.out.println("Error: " + s);
public static void main(String[] args) {
emailChecker.apply("adfsf@gmail.com").bind(success, failure);
}
将if-else语句消灭
现在程序已经显得非常"函数式"了,剩下的一个问题,就是在emailChecker中大量的if-else语句,如果我们简单地将if-else替换成三目运算符来让他显得函数式,会显得十分凌乱,并且不利于拓展.
我们如果将if-else语句当作一个函数来看的话,相当于判断一个条件后,执行一个行为,将这个行为抽象出来,我们可以创建一个表示条件的类,条件可以通过Supplier(提供者)来指定
/**
* @author Boyn
* @date 2019/8/18
* @description Supplier类,即无中生有
*/
public interface Supplier<T> {
T get();
}
同时,我们还需要一个对Result的信息的提供者,将这两个Supplier的接口实现用一个元组存储起来.
Tuple<Supplier<Boolean>, Supplier<Result<T>>>
并且,我们的Case类可以定义如下三个方法,用于定义不同的条件
/**
* 表示是否匹配条件 match_case
* 接受一个条件和返回一个结果
*/
public static <T> Case<T> mcase(Supplier<Boolean> condition, Supplier<Result<T>> value) {
return new Case<T>(condition, value);
}
/**
* 条件永远为真,返回一个结果
*/
public static <T> DefaultCase<T> mcase(Supplier<Result<T>> value) {
return new DefaultCase<T>(() -> true, value);
}
/**
* 选择一种情况,在使用Case类时,用match作为条件的入口类,先指定一个默认的条件
* 通常是在if...else...语句中最后的那个兜底条件
* matchers参数就是各个条件,通过上面的两个mcase方法进行指定,
*/
public static <T> Result<T> match(DefaultCase<T> defaultCase, Case<T>... matchers) {
for (Case<T> aCase : matchers) {
if (aCase.t_1.get()) return aCase.t_2.get();
}
return defaultCase.t_2.get();
}
mcase表示的是match_case,即匹配的case,第一个mcase方法定义了一种正常情况,接受一个条件和一个结果,在match函数中,当这个条件为真,则返回这个结果,第二个mcase方法定义了一种默认情况,只接受结果,而条件默认为真,作为当前面的条件都不匹配时,最后返回的默认结果.
Case类的总体实现如下:
/**
* @author Boyn
* @date 2019/8/18
* @description 一个表示条件的Case类
* 这个类相等于在命令式编程中的switch case语句
*/
public class Case<T> extends Tuple<Supplier<Boolean>, Supplier<Result<T>>> {
private Case(Supplier<Boolean> booleanSupplier, Supplier<Result<T>> resultSupplier) {
super(booleanSupplier, resultSupplier);
}
/**
* 表示是否匹配条件 match_case
* 接受一个条件和返回一个结果
*/
public static <T> Case<T> mcase(Supplier<Boolean> condition, Supplier<Result<T>> value) {
return new Case<T>(condition, value);
}
/**
* 条件永远为真,返回一个结果
*/
public static <T> DefaultCase<T> mcase(Supplier<Result<T>> value) {
return new DefaultCase<T>(() -> true, value);
}
/**
* 选择一种情况,在使用Case类时,用match作为条件的入口类,先指定一个默认的条件
* 通常是在if...else...语句中最后的那个兜底条件
* matchers参数就是各个条件,通过上面的两个mcase方法进行指定,
*/
public static <T> Result<T> match(DefaultCase<T> defaultCase, Case<T>... matchers) {
for (Case<T> aCase : matchers) {
if (aCase.t_1.get()) return aCase.t_2.get();
}
return defaultCase.t_2.get();
}
private static class DefaultCase<T> extends Case<T> {
private DefaultCase(Supplier<Boolean> booleanSupplier, Supplier<Result<T>> resultSupplier) {
super(booleanSupplier, resultSupplier);
}
}
}
这样,我们就可以将emailChecker中的if-else语句消除了:
static Function<String, Result<String>> emailChecker = s -> match(
mcase(() -> Result.success(s)),
mcase(() -> s == null, () -> Result.failure("email can not be null")),
mcase(() -> s.length() == 0, () -> Result.failure("email can not be blank")),
mcase(() -> !emailPattern.matcher(s).matches(), () -> Result.failure("email " + s + " is invaild"))
);
但是,这个只是权宜之计,我们只是在明面上消除了这些判断,实际的实现中,仍然会存在,只是不会显式地调用.