作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
迈克尔·拉塔的头像

迈克尔·瑞斯

Michael是一名软件工程师,拥有深厚的技术和领导能力. 作为一名企业家,他既是一名领导者,也是一名团队成员.

工作经验

44

分享

人们喜欢将编程语言分类为范式. 有面向对象(OO)语言、命令式语言、函数式语言等. 这有助于找出哪些语言可以解决类似的问题, 以及一门语言要解决什么类型的问题.

在每种情况下,范式通常都有一个“主要”焦点和技术,这是该语言家族的驱动力:

  • 在OO语言中,它是 类或对象 作为一种用状态(方法)操作来封装状态(数据)的方式.

  • 在函数式语言中,它可以是 函数的操作 他们自己还是 不可变数据 从一个函数传递到另一个函数.

长生不老药 (和 Erlang 在此之前,它们通常被归类为函数式语言,因为它们展示了函数式语言常见的不可变数据, 我认为它们代表a 从许多函数式语言中分离范式. 它们的存在和被采用是因为OTP的存在,所以我将它们归类为 面向流程的语言.

在这篇文章中, 我们将在使用这些语言时了解面向过程编程的含义, 探索与其他范例的异同, 请参阅培训和采用的含义, 并以一个简短的面向过程的编程示例作为结束.

什么是面向过程的编程?

让我们从定义开始: 面向流程的编程 范式的基础是什么 通信顺序进程,原出自一篇论文 Tony Hoare in 1977. 这也被普遍称为 演员 并发模型. 其他一些与原著相关的语言包括Occam、Limbo和Go. The formal paper deals only with synchronous communication; most 演员 models (including OTP)也使用异步通信. 总是可以在异步通信之上构建同步通信, 而OTP支持这两种形式.

关于这段历史, OTP通过通信顺序进程创建了一个容错计算系统. 容错功能来自一种“让它失败”的方法,它以监督器的形式进行可靠的错误恢复,并使用参与者模型支持的分布式处理. “让它失败”可以与“防止它失败”形成对比,,因为前者更容易适应,并且在OTP中被证明比后者可靠得多. 原因是防止故障所需的编程工作(如Java检查异常模型中所示)涉及的内容和要求要高得多.

因此,面向过程的编程可以定义为 以系统的过程结构和过程之间的通信为主要关注点的范式.

面向对象与. 面向流程的编程

在面向对象编程中,数据和函数的静态结构是主要关注的问题. 需要哪些方法来操作所包含的数据, 对象和类之间应该有什么联系. 因此,UML的类图就是这个重点的一个主要例子,如图1所示.

面向过程的编程:示例UML类图

可以注意到,对面向对象编程的一个常见批评是没有可见的控制流. 因为系统是由大量单独定义的类/对象组成的, 对于经验不足的人来说,可视化系统的控制流程是很困难的. 对于具有大量继承的系统尤其如此, 哪些使用抽象接口或没有强类型. 在大多数情况下,它变得很重要 开发人员 要记住大量的系统结构才能有效(哪些类有哪些方法,哪些以什么方式使用).

面向对象开发方法的优势在于,系统可以扩展以支持新类型的对象,而对现有代码的影响有限, 只要新的对象类型符合现有代码的期望.

功能与. 面向流程的编程

许多函数式编程语言确实以各种方式解决并发问题, 但它们主要关注的是函数之间传递的不可变数据, 或者从其他函数创建函数(生成函数的高阶函数). 在很大程度上, 该语言的重点仍然是单个地址空间或可执行文件, 这些可执行文件之间的通信以特定于操作系统的方式处理.

例如, Scala是一种函数式语言 构建在Java虚拟机上. 虽然它可以访问Java工具进行通信,但它不是该语言的固有部分. 虽然它是Spark编程中常用的语言, 它也是一个与语言结合使用的库.

功能范式的优势在于能够可视化给定顶层功能的系统的控制流. 控制流是显式的,因为每个函数调用其他函数, 并将所有数据从一个传递到下一个. 在函数式范式中,没有副作用,这使得确定问题更容易. 纯功能系统的挑战在于持久状态需要“副作用”. 在架构良好的系统中, 状态的持久化是在控制流的顶层处理的, 允许系统的大部分是无副作用的.

长生不老药/OTP和面向过程的编程

在长生不老药/Erlang和OTP, 通信原语是执行该语言的虚拟机的一部分. 进程之间和机器之间的通信能力是内置的,也是语言系统的核心. 这强调了在这种范式和这些语言系统中交流的重要性.

而长生不老药语言占主导地位 功能 就语言表达的逻辑而言,它的用法是 面向过程的.

面向过程意味着什么?

本文所定义的面向过程是指首先以存在什么过程以及它们如何通信的形式设计一个系统. 其中一个主要问题是哪些进程是静态的, 这些都是动态的, 哪些是按需生成的, 哪些服务于长期运行的目的, 哪些持有系统的共享状态或部分共享状态, 以及系统的哪些特性本质上是并发的. 就像OO有对象类型一样, 函数有函数的类型, 面向过程的编程有不同类型的过程.

因此, 面向过程的设计是识别解决问题或满足需求所需的一组过程类型.

时间方面很快进入到设计和需求工作中. 系统的生命周期是什么? 什么样的需求是偶尔的,什么样的需求是恒定的? 系统中的载荷在哪里,预期的速度和体积是多少? 只有在理解了这些类型的考虑之后,面向过程的设计才开始定义每个过程的功能或要执行的逻辑.

训练的影响

这种分类对训练的含义是,训练不应该从语言语法或“Hello World”示例开始, 但随着 系统工程思维和设计的重点是过程分配.

编码问题是次要的,过程设计和分配最好在更高的级别上解决, 并涉及到生命周期的跨职能思考, QA, DevOps, 以及客户的业务需求. 长生不老药或Erlang的任何培训课程都必须(通常也确实)包括OTP, 从一开始就应该有一个过程导向, 而不是“现在你可以在长生不老药中编写代码了, 让我们使用并发类型的方法.

采用影响

采用的含义是语言和系统可以更好地应用于需要通信和/或计算分布的问题. 在这个领域中,单个计算机上的单个工作负载问题不那么有趣, 也许用另一种语言来表达会更好. 长期的连续处理系统是这种语言的主要目标,因为它从头开始就内置了容错功能.

用于文档和设计工作, 使用图形化表示法(如图1中的OO语言)可能会非常有帮助。. 对于长生不老药和UML面向过程的编程,建议使用序列图(图2中的示例)来显示过程之间的时间关系,并确定哪些过程涉及到为请求提供服务. 没有用于捕获生命周期和过程结构的UML图类型, 但是它可以用一个简单的方框和箭头图来表示过程类型及其关系. 例如,图3:

面向过程的编程示例UML序列图

面向过程编程示例过程结构图

过程导向的一个例子

最后,我们将介绍一个将过程导向应用于问题的简短示例. 假设我们的任务是提供一个支持全球选举的系统. 选择这个问题是因为许多单独的活动是在爆发中执行的, 但是对结果的聚合或汇总是实时的,并且可能会看到很大的负载.

初始工艺设计和分配

我们最初可以看到,每个人的投票是来自许多离散输入的系统流量的爆发, 没有时间顺序, 并且可以有高负荷. 为了支持这项活动, 我们希望大量的流程都收集这些输入,并将它们转发给更中心的流程进行制表. 这些程序可以设在每个国家将要产生选票的人口附近, 从而提供低延迟. 他们将保留当地的结果, 立即记录他们的输入, 并将它们分批转发以进行制表,以减少带宽和开销.

我们最初可以看到,将需要有跟踪每个司法管辖区选票的程序,必须在这些程序中提交结果. 在本例中,假设我们需要跟踪每个国家的结果, 在每个国家按省/州划分. 为了支持这项活动, 我们希望每个国家至少有一个进程执行计算, 并保留当前的总数, 每个国家的每个州/省都有一组. 这假设我们需要能够实时或低延迟地回答国家和州/省的总数. 如果结果可以从数据库系统中获得, 我们可以选择一个不同的进程分配,其中总数由瞬时进程更新. 使用专用进程进行这些计算的优点是,结果以内存的速度出现,并且可以以低延迟获得.

最后,我们可以看到很多很多的人将会看到结果. 这些进程可以通过多种方式进行划分. 我们可能希望通过在每个负责本国结果的国家设置进程来分配负载. 进程可以缓存来自计算进程的结果,以减少计算进程的查询负载, 并且/或者计算过程可以周期性地将它们的结果推送到适当的结果过程中, 当结果发生显著变化时, 或者在计算过程变得空闲时,表明变化速度减慢.

在所有三种流程类型中, 我们可以独立地扩展进程, 按地理位置分布, 通过主动确认进程之间的数据传输,确保结果永远不会丢失.

如前所述, 我们以独立于每个流程中的业务逻辑的流程设计开始了该示例. 在业务逻辑对数据聚合或地理有特定需求的情况下,可能会迭代地影响流程分配. 到目前为止我们的流程设计如图4所示.

面向过程的开发示例:初始过程设计

使用单独的流程来接收投票,使得每一张投票都可以独立于任何其他投票而被接收, 收货登记, 然后批处理到下一组进程, 大大减少了这些系统的负载. 对于消耗大量数据的系统, 通过使用流程层来减少数据量是一种常见且有用的模式.

通过在一组孤立的进程中执行计算, 我们可以管理这些进程的负载,并确保它们的稳定性和资源需求.

通过将结果表示置于一组孤立的进程中, 我们既可以控制系统其余部分的负载,也可以根据负载动态缩放进程集.

额外的需求

现在,让我们添加一些复杂的需求. 让我们假设在每个辖区(国家或州), 计票可以产生按比例计算的结果, 赢家通吃的结果, 或者如果相对于该司法管辖区的人口投票不足,则没有结果. 每个司法管辖区对这些方面都有控制权. 有了这个变化, 那么,各国的投票结果就不是原始投票结果的简单汇总, 而是汇总了州/省的结果. 这将流程分配从最初的要求更改为将州/省流程的结果提供给国家流程. 如果在投票收集和州/省以及省到国家的过程之间使用的协议是相同的, 然后可以重用聚合逻辑, 但是保存结果的不同过程是需要的,它们的通信路径是不同的, 如图5所示.

面向过程的开发示例:修改的过程设计

的代码

为了完成这个示例,我们将回顾长生不老药 OTP中该示例的实现. 为了简化事情, 这个例子假设使用像Phoenix这样的web服务器来处理实际的web请求, 这些web服务向上面标识的流程发出请求. 这样做的好处是简化了示例并将重点放在长生不老药/OTP上. 在生产系统中, 将它们作为独立的过程具有一些优势,并且可以分离关注点, 允许灵活部署, 分配负载, 减少了延迟. 完整的源代码和测试可以在 http://github.com/technomage/voting. 为了便于阅读,本文对源代码进行了缩写. 下面的每个进程都适合OTP监督树,以确保在出现故障时重新启动进程. 有关示例这方面的更多信息,请参阅源代码.

投票记录器

这个过程接受投票, 将它们记录到持久存储中, 并将结果批量发送给聚合器. VoteRecoder模块使用任务.主管管理短期任务,记录每次投票.

defmodule投票.VoteRecorder做
  @moduledoc”“”
  该模块接收投票并将其发送给适当的人
  聚合器. 本模块使用监督任务来确保
  任何失败都可以恢复,而投票却不能
  失去了.
  """

  @doc”“”
  启动一个任务来跟踪向系统提交投票的情况
  聚合器. 这是一个有监督的任务
  完成.
  """
  Def cast_vote where, who do
    任务.主管.async_nolink(投票.Vote任务主管,
      fn ->
        投票.聚合器.Submit_vote where, who
      结束)
    |> 任务.等待
  结束
结束

投票聚合器

这个过程在一个管辖范围内聚集选票, 计算该辖区的结果, 并将投票摘要转发给下一个更高级别的流程(更高级别的管辖权), 或结果展示者).

defmodule投票.聚合器做
  使用GenStage
  ...

  @doc”“”
  向聚合器提交一票
  """
  defsubmit_vote id,候选人做
    pid = __MODULE__.via_tuple (id)
    :ok = GenStage.调用pid, {:submit_vote, candidate}
  结束

  @doc”“”
  响应请求
  """
  Def handle_call {:submit_vote, candidate}, _from, state do
    N =状态.选票[候选人]|| 0
    州= %{州|投票:地图.把(国家.投票,候选人,n+1)}
    {:回复,:ok, [%{state].id => state.票}),状态}
  结束

  @doc”“”
  处理来自下级聚合器的事件
  """
  Def handle_events事件,_from, state做
    投票= 枚举.减少事件、状态.票, fn e, 票 ->
      枚举.reduce e, 票, fn {k,v}, 票 ->
        Map.Put (票, k, v) #替换下属的所有条目
      结束
    结束
    任何司法管辖区的特定政策都会在这里

    #对已发布事件的候选人的投票求和
    merged = 枚举.reduce 票, %{}, fn {j, jv}, 票 ->
      每个选区对每个候选人进行求和
      枚举.reduce jv, 票, fn {candidate, tot}, 票 ->
        日志记录器.调试"@@@@在#{inspect j}中为#{inspect candidate}投票:#{inspect tot}"
        N = 票[候选人]|| 0
        Map.投(票,候选人,n + tot)
      结束
    结束
    #返回发布的事件和保留的状态
    #按司法管辖区投票
    {: noreply[%{状态.id => merged}], %{state | 票: 票}}
  结束
结束

结果节目主持人

该流程接收来自聚合器的投票,并将这些结果缓存到服务请求以显示结果.

defmodule投票.ResultPresenter做
  使用GenStage
  …

  @doc”“”
  处理对结果的请求
  """
  Def handle_call:get_票, _from, state do
    {:回复,{:ok,状态.{, [], state}
  结束

  @doc”“”
  从演示者处获取结果
  """
  Def get_票 id
    pid =投票.ResultPresenter.via_tuple (id)
    {:ok, 票} = GenStage.调用pid,:get_票
    票
  结束

  @doc”“”
  从聚合器接收投票
  """
  Def handle_events事件,_from, state做
    日志记录器.调试“@@@@演示者收到:#{inspect events}”
    投票= 枚举.减少事件、状态.票, fn v, 票 ->
      枚举.reduce v, 票, fn {k,v}, 票 ->
        Map.投(票,k, v)
      结束
    结束
    {:noreply, [], %{state | 票: 票}}
  结束
结束

外卖

这篇文章从长生不老药/OTP作为面向过程的语言的潜力出发进行了探讨, 将其与面向对象和函数范例进行比较, 并回顾了这对培训和采用的影响.

这篇文章还包含了一个将这个方向应用于一个示例问题的简短示例. 如果您想查看所有代码,这里有一个到我们的示例的链接 GitHub 再说一遍,这样你就不用回去找了.

关键的收获是将系统视为通信过程的集合. 首先从流程设计的角度规划系统,然后从逻辑编码的角度规划系统.

了解基本知识

  • 什么是长生不老药和OTP?

    长生不老药是一种基于Erlang VM的函数式编程语言. OTP是Erlang和长生不老药集成的面向过程的编程框架.

  • 什么是面向过程的开发?

    面向过程的开发首先关注系统的过程结构, 其次是系统的功能逻辑.

  • 最好地采用长生不老药/OTP和面向过程的开发需要什么?

    从培训或探索开始,重点关注OTP和流程管理,然后关注长生不老药的语法和功能方面. 避免以hello world编码示例开始并且只进行了一半就进入OTP的培训.

  • 为什么采用长生不老药/OTP和面向过程的开发?

    长生不老药/OTP在可靠性和并发性方面比竞争的堆栈要好得多, 需要较少的程序员技能才能精通, 并且具有比Ruby on Rails或Node更好的开箱即用性能.

  • 何时选择长生不老药/OTP?

    长生不老药/OTP适用于长时间运行的进程或需要多核性能的进程. 他们更关注低延迟而不是高吞吐量. 对于要求很高的单核吞吐量应用程序来说,它们不是很强大, 或者用于不经常在批处理或命令行环境中运行的应用程序.

聘请Toptal这方面的专家.
现在雇佣
迈克尔·拉塔的头像
迈克尔·瑞斯

位于 格里利,科罗拉多州,美国

成员自 2015年10月1日

作者简介

Michael是一名软件工程师,拥有深厚的技术和领导能力. 作为一名企业家,他既是一名领导者,也是一名团队成员.

Toptal作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.

工作经验

44

世界级的文章,每周发一次.

订阅意味着同意我们的 隐私政策

世界级的文章,每周发一次.

订阅意味着同意我们的 隐私政策

Toptal开发者

加入总冠军® 社区.