有趣的Code Poster

Pete Corey的博客《Build your own code poster with Elixir》展示了如何通过Elixir实现一个类似Commits.io一样的功能,即可以将自己的代码融合到一张LOGO图片中,例如这样的效果:

左侧是原图,右侧则将代码重叠到图中有颜色的部分,形成一种颇有极客范儿的图片。这很有趣。

实现代码并不复杂,不到200行代码。虽然简单,代码却很好地体现了Elixir编程的风格。这种风格提倡运用|>管道符以流的形式传递数据,体现函数组合子的强大威力。针对问题域,我们的解决方案是思考数据流动的方向,以及处理数据的各个阶段,并将其组合成一个顺序的执行流程。

以Code Poster为例,其执行流程如下所示:

  • 读取代码文件
  • 读取图片文件
  • 生成Text元素集(这个过程会完成代码字符与颜色的融合)
  • 生成SVG
  • 保存SVG文件

Elixir的管道运算符,会自然地让我们想起Unix中实现的Pipe and Filter模式,每个Filter均被定义为一个统一的接口:输入为stdin,输出为stdout或者stderr。

在Unix中,这种统一接口可以被隐喻为文件(file),上图中的stdin、stdout与stderr都属于file descriptor,可以像操作文件那样读或写字节流(stream of bytes)。这里所谓的文件是一个宽泛的概念,可以是一个文件系统,也可以通过管道(pipe)将字节流传递到另一个进程,可以是Unix Socket,设备驱动,内核API以及TCP连接。

那么,在Code Poster场景下,这个统一接口是什么呢?或者说我们需要抽象的数据究竟是什么呢?分析前面的执行流程,读取一个code文件与读取一个image文件,返回的结果其实完全不同。那么,如这般不同的数据结构如何才能够像Stream一样通过管道连接起来呢?

在Elixir中,我们通常通过定义一个struct来完成对数据的抽象。整个管道处理中需要的数据会作为一个“并集”被定义到struct的属性中。例如,load code file操作需要一个code path,返回文件中的代码code;load image file操作则需要一个image path,返回一个image结构体。image结构体包含了图片的width和所有像素(pixels)。这些数据都是construct text element步骤需要的,在对这些数据进行处理后,该步骤又会返回生成的text elements集合;然后再交给construct svg,最后生成一个svg对象并保存。这个过程如下图所示:

整个过程传递的数据为PosterData,它好像是一个数据容器一般,各个执行步骤都会对该数据容器传来的相关数据进行处理,然后再将获得的结果塞到该容器中,再接力传递给下一个。PosterData的定义如下:

  defstruct code_path: nil,
            image_path: nil,
            ratio: nil,
            final_width: nil,
            final_height: nil,
            out_path: nil,
            code: nil,
            image: nil,
            pixels: nil,
            text_elements: nil,
            svg: nil

Elixir使用管道操作符|>非常好地改善了代码的阅读体验。阅读如下代码,与阅读自然语言没有太大区别:

  def go(ratio, final_width, final_height, code_path, image_path, out_path) do
    %PosterData{
      ratio: ratio,
      final_width: final_width,
      final_height: final_height,
      code_path: code_path,
      image_path: image_path,
      out_path: out_path
    }
    |> load_code
    |> load_image
    |> construct_text_elements
    |> construct_svg
    |> save_svg
  end

在编写这样的方法时,我们一定要注意方法的抽象层次,即遵循所谓的单一抽象层次原则。该原则要求一个函数中的所有操作都处于相同的抽象层。只有如此,才不会让函数表达的意思失衡,有的隐藏了细节,有的又暴露了不必要的内容。

我们可以通过对需求的任务逐层拆分来保证这一点。前面提到的流程是最高抽象层的任务分解,而针对load code file,又可以拆分为如下的子任务:

  • 读取代码文件
  • 去除文件内容中的空格、tab、换行符(表现的业务含义就是拼接这些代码字符)
  • 转换为字符集合

load_code函数体现了这样的流程:

  def load_code(data = %PosterData{code_path: code_path}) do
    Logger.debug("Loading code from '#{code_path}'...")
    code = code_path
    |> File.read!
    |> join_code
    |> String.codepoints
    %{data | code: code}
  end

其中join_code接收的是File.read!函数读取的文件内容,它又可以继续拆分子任务:

  def join_code(code) do
    Logger.debug("Joining code...")
    code
    |> String.trim
    |> String.replace(~r/\s*\n+\s*/, " ")
    |> String.replace(~r/\s/," ")
  end

多么美妙的代码表现力!又是多么清晰的任务分解层次!管道操作符将整个业务盘活了,就好像赋予了代码灵魂一般。

原文发布于微信公众号 - 逸言(YiYan_OneWord)

原文发表时间:2017-03-09

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏云飞学编程

怎么让代码更Pythonic?光有技巧可不行,你还需要看这些

写代码如同写文章,好的文章是反复修改出来的,代码也同样是反复的重构出来的。今天给大家分享下,怎么从一个编程学习者变为一个程序猿(程序媛)!起码不要让别人一看你的...

853
来自专栏前端知识分享

第136天:Web前端面试题总结(理论)

  a. 请求数量:合并脚本和样式表,CSS Sprites,拆分初始化负载,划分主域

572
来自专栏java一日一条

50个常见的 Java 错误及避免方法(第一部分)

在开发Java软件时可能会遇到许多类型的错误,但大多数是可以避免的。为此我们罗列了50个最常见的Java编码错误,其中包含代码示例和教程,以帮助大家解决常见的编...

683
来自专栏韩伟的专栏

反射,框架的利器

通用型软件框架的难题 假设我们希望开发一套通用型的软件框架,这个框架允许用户自定义大量不同的情况下的回调函数(方法),用来实现丰富多彩的业务逻辑功能,例如一个...

3409
来自专栏IMWeb前端团队

web系统性能及规范检测笔记

静态检查 关于javascript的语言特性,Douglas Crockford在蝴蝶书里面写过:“一些特性因为规范的不完善而可能导致可移植性问题,一些特性会导...

1796
来自专栏流媒体

Linux下Socket编程(五)——http容器(支持php)简介

为了方便操作以及功能模块的拆分,这里我们将请求内容和响应内容进行封装。这里我们只列出头文件,封装类中我们仅仅做了关系字段的提取。

341
来自专栏大数据钻研

HTML meta标签总结与属性使用介绍

之前学习前端中,对meta标签的了解仅仅只是这一句。 <metacharset="UTF-8"> 但是打开任意的网站,其head标签内都有一列的meta标签。比...

2656
来自专栏gaoqin31

设计模式之 UML

今年接手的一个计费项目让我痛苦不堪,里面到处充斥着重复冗余的代码,一个简单的需求往往需要改n个地方,而且很多改动牵一发动全身,这个项目涉及到支付,出问题就是损失...

512
来自专栏生信技能树

两个神奇的R包介绍,外加实用小抄

认识Tidy Data1.Reshape Data2.Handle Missing Values3.Expand Tables4.split cells一、测...

1034
来自专栏张善友的专栏

Cocoa编程学习笔记之MVC

Cocoa 使用了一种修改版本的MVC模式来处理GUI的显示。MVC模式(自1979年以来)已经出现很长时间了,它皆在分离显示用户界面所需的大量任务,并处理用户...

1727

扫描关注云+社区