乐趣区

关于oop:函数式编程如何高效简洁地对数据查询与变换

摘要:一提到编程范式,很容易联想到宗教的虔诚,每种宗教所表白信条都有肯定合理性,但如果始终只遵循一种教条,可能也被让本人痛苦不堪,编程范式也是如此。

案例 1

案例一,代码摘抄来自一企业培训资料,次要代码逻辑是打印每课问题,并找出学生非 F 级别课程统计均匀分数:

class CourseGrade {
 public String title;
 public char grade;
}

public class ReportCard {
 public String studentName;
 public ArrayList<CourseGrade> cliens;

 public void printReport() {System.out.println("Report card for" + studentName);
        System.out.println("------------------------");
        System.out.println("Course Title       Grade");
        Iterator<CourseGrade> grades = cliens.iterator();
        CourseGrade grade;
 double avg = 0.0d;
 while (grades.hasNext()) {grade = grades.next();
            System.out.println(grade.title + " " + grade.grade);
 if (!(grade.grade == 'F')) {avg = avg + grade.grade - 64;}
        }
        avg = avg / cliens.size();
        System.out.println("------------------------");
        System.out.println("Grade Point Average =" + avg);
    }
}

下面的代码有哪些问题呢:

  • 成员变量采纳 public,短少数据封装性
  • 没有判断 cliens 是否为空,可能除以 0 值。注:假设它不会为空,另外逻辑可能有问题,为什么统计总分是非 F 课程,除数却是所有课程 Size,先疏忽这个问题
  • avg 这个变量多个用处,即是总分,又是平均分
  • cliens 变量名难以了解
  • !(grade.grade == ‘F’) 有点反直觉
  • while 循环干了两件事,打印每课的问题,也统计了分数

培训资料并未给规范解题,尝试优化一下代码,采纳 Java8 的 Stream 来简化计算过程,并对代码进行了分段:

 public void printReport2() {System.out.println("Report card for" + studentName);
        System.out.println("------------------------");

        System.out.println("Course Title       Grade");
        cliens.forEach(it -> System.out.println(it.title + " " + it.grade));

 double total = cliens.stream().filter(it -> it.grade != 'F')
                .mapToDouble(it -> it.grade - 64).sum();
        System.out.println("------------------------");
        System.out.println("Grade Point Average ="  + total / cliens.size());
    }

进一步优化,把各类打印抽取各自函数:

 private void printHeader() {System.out.println("Report card for" + studentName);
        System.out.println("------------------------");   
    }

 private void printGrade() {System.out.println("Course Title       Grade");
        cliens.forEach(it -> System.out.println(it.title + " " + it.grade));
    }

 private void printAverage() {double total = cliens.stream().filter(it -> it.grade != 'F')
                .mapToDouble(it -> it.grade - 64).sum();
        System.out.println("------------------------");
        System.out.println("Grade Point Average ="  + total / cliens.size());
    }

 public void printReport3() {printHeader();
        printGrade();
        printAverage();} 

注:如果只算非 F 的平均分,能够一行搞定:

double avg = cliens.stream().filter(it -> it.grade != 'F').mapToDouble(it -> it.grade - 64).average().orElse(0.0d);

案例二:再看一段代码:

List<Integer> tanscationsIds = transcations.parallelStream()
        .filter(it -> it.getType() == Transcation.GROCERY)
        .sorted(comparing(Transcation::getValue).resersed())
        .map(Transcation::getId)
        .collect(Collectors::toList());

代码十分清晰:

  • 过滤出类型为 GROCERY 的交易记录
  • 按其 value 值进行倒排序
  • 各自取其 Id 字段
  • 输入 Id 列表

这看起来是不是像这样一条 SQL 语句:select t.id from tanscations t where t.type == ‘GROCERY’ order by t.value desc

1 背地的常识

目前 Java8 已宽泛应用,对于 Stream 与 Lambda 应司空见惯了,而不再是一种炫技。网上也有十分多的教程,若有同学还不相熟他们的用法,能够多找找资料相熟一下。

Stream 正如其名,像一条数据生产流水线,逐渐叠加两头操作(算法和计算),把数据源转换为另一个数据集。

笔者很早以前学过 C#,接触过 LINQ(Language Integrated Query),它比 Java 的 Stream 和 Lambda 用法更为清晰简洁,先给个简略示例:

var result = db.ProScheme.OrderByDescending(p => p.rpId).Where(p => p.rpId > 10).ToList();

LINQ 为数据查问而生,能够算是 DSL(Domain Specific Language)了,背地也是函数式编程 (FP) 一套理念,先记住其中两点:

  • Monad 是一种设计模式,示意将一个运算过程,通过函数拆解成相互连贯的多个步骤
  • Lambda 表达式 是一个匿名函数,Lambda 表达式基于数学中的 λ 演算得名

FP 还有其它的个性:模式匹配,柯里化,偏函数,闭包,尾递归等。对 FP 感觉趣味的同学无妨找找资料学习一下。

当初的支流语言,都引入一些 FP 个性来晋升语言在数据上的表达能力。

C++11 引入 Lambda 表达式,并提供 <algorithm>,<functional> 两个根底库,一个简略示例:

int foo[] = { 10, 20, 5, 15, 25};
std::sort(foo, foo+5, [](int a,int b){return a > b;});

Python 提供 functools 库来简化一些函数式编程(还是相当的弱),一个简略示例:

foo = ["A", "a", "b", "B"]
sorted(foo, key=functools.cmp_to_key(locale.strcoll))

2 函数式编程

当然,面向对象语言中减少 lambda 这类特色不能就称为函数式编程了,大部分只不过是语法糖。是采纳什么编程范式不在于语言的语法,而是在于思维形式。

面向对象编程 (OOP) 在过来 20 多年十分胜利,而函数式编程 (FP) 也一直地倒退,他们相生相息,各自解决不同的场景问题:

  • 面向对象能够了解为是对数据的形象,比方把一个事物形象成一个对象,关注的是数据。
  • 函数式编程是一种过程形象的思维,就是对以后的动作去进行形象,关注的是动作。

事实业务需要往往体现为业务流动,它是面向过程的,即先输出数据源,在肯定条件下,进行一系列的交互,再输入后果。那面向过程与函数式的的区别是什么:

  • 面向过程能够了解是把做事件的动作进行合成多个步骤,所以有 if/while 这类语法撑持,走不同的分支步骤。
  • 函数式相比面向过程式,它更加地强调执行后果而非执行过程,利用若干个简略的执行单元让计算结果一直渐近,逐层推导简单的运算,而不是像面向过程设计出简单的执行过程,所以纯函数式编程语言中不须要 if/while 这类语法,而是模式匹配,递归调用等。

面向对象的编程通过封装可变的局部来结构可能让人读懂的代码,函数式编程则是通过最大水平地缩小可变的局部来结构出可让人读懂的代码。

咱们从 Java 的 Stream 实现也看到函数式的另一个特点:

  • 函数不保护任何状态,上下文的数据是不变的,传入的参数据处理实现之后再扔出来。

联合下面的了解,咱们能够先把世界事物通过 OOP 形象为对象,再把事物间的分割与交互通过 FP 形象为执行单元,这种联合或者是对业务流动的实现一种较好的解决形式。

3 防止繁多范式

一提到编程范式,很容易联想到宗教的虔诚,每种宗教所表白信条都有肯定合理性,但如果始终只遵循一种教条,可能也被让本人痛苦不堪。编程范式也是如此,正如 Java 在 1.8 之前是纯面向对象式,你就会感觉它十分繁琐。也如 Erlang 是纯函数式,你就会发现有时简略的逻辑解决会非常复杂。

近些年来,因为数据分析、科学计算和并行计算的衰亡,让人意识到函数式编程解决数据畛域的魅力,它也越来越受欢迎。在这些畛域,程序往往比拟容易用数据表达式来表白,采纳函数式能够用很少代码来实现。

事实的业务软件,很多的逻辑其实也是对数据的解决,最简略是对数据的 CURD,以及数据的组合、过滤与查问。所以函数式编程在许多语言中都失去反对,晋升了对数据处理的表达能力。

理解新的编程范式在适当的时候应用它们,这会使你事倍功半。无论什么编程范式,他们都是工具,在你的工具箱中,可能有锤子,螺丝刀…,这个工具在什么时候应用,取决待解决的问题。

4 结语

本文的案例只是一个引子,次要是想给你带来函数式编程的一些理念,函数式给咱们解决业务问题提供了另一种思维形式:如何高效简洁地对数据查问与变换。许多语言都反对函数式一些能力,须要咱们一直地学习,在正当的场景下应用他们。

本文分享自华为云社区《飞哥讲代码 16:函数式让数据处理更简洁》,原文作者:华为云专家。

点击关注,第一工夫理解华为云陈腐技术~

退出移动版