Elixir 从零 · 第 28 课(展开目录)
01 · 先让代码说话02 · 先认六种值03 · 把值装起来04 · 从旧 list 生成新 list05 · 让形状对上06 · 用 case 做选择07 · 把文字安全变成整数08 · 拆开 &1 和管道09 · 把代码放进项目10 · 真假不只靠 true11 · 走进嵌套数据12 · 一字不一定一字节13 · 让函数按形状接活14 · 把一列数收成一个值15 · 给不同问题选不同路16 · 给 map 一张名片17 · 让项目自己验答案18 · 让成功和失败长得一样19 · 读写文件先管好路20 · 只算眼前需要的21 · 同一句话,不同做法22 · 先约好模块会做什么23 · 把数据形状写在门口24 · 测试不只看一条好路25 · 把项目磨成一个命令26 · 定下小项目边界27 · 把真实文字洗干净28 · 只开三扇门29 · 把约定钉在代码旁30 · 打包,也留下脚印31 · 照任务书交卷
ELIXIR · 作品 · LESSON 2845 分钟

只开三扇门

设计小而稳的公开 API,把分词细节藏进私有函数。

01 · 先看完整例子

让公开入口只接收行

本项目约定空白分隔词,字符数不包含换行。

Elixir
defmodule TrailStats.Stats do
  @enforce_keys [:lines, :words, :chars]
  defstruct [:lines, :words, :chars]
end

defmodule TrailStats do
  alias TrailStats.Stats

  # 对外只公开 count/1
  def count(lines) do
    %Stats{
      lines: length(lines),
      words: Enum.reduce(lines, 0, &(&2 + word_count(&1))),
      chars: lines |> Enum.join() |> String.length()
    }
  end

  defp word_count(line), do: line |> String.split() |> length()
end
先对照结果
  • TrailStats.count(["长安 春风", "又一程"]) 返回行 2、词 3、字符 8 的 struct。
02 · 回头拆代码

从第一行往下读

  1. struct 固定三项输出,避免 map 键随手变化。
  2. count/1 只接内部数据,不关心路径。
  3. 分词规则藏在 word_count/1,以后可单独替换。
03 · 认清新符号

符号不是暗号

def

定义公开函数。

defp

定义只供当前模块使用的私有函数。

%Stats{}

返回固定字段的统计结果。

04 · 代码里的概念

把名字和意思对上

01

公开 API

模块外可以依赖的函数和数据形状。

02

实现细节

模块内部可修改而不影响调用者的步骤,通常放在私有函数。

05 · 再说清楚

这些写法为什么有用

模块对外开的门越多,未来越难改。项目只公开调用者真正需要的函数。

统计模块接收已经清洗的行,不再读文件。字符、词、行的规则写在同一处。

内部 helper 使用 defp。调用者看见稳定结果,不需要知道中间 list 怎样生成。

06 · 自己改一次

合上答案,动手

补全空输入测试。

练习起点
assert TrailStats.count([]) == %TrailStats.Stats{
  lines: ____,
  words: ____,
  chars: ____
}

目标结果: 三项都为 0。

卡住了,再看提示

空输入没有行、词和字符。

运行过以后,再看一种答案
一种答案
# 空输入是公开 API 的重要边界
assert TrailStats.count([]) == %TrailStats.Stats{
  lines: 0,
  words: 0,
  chars: 0
}
想一想:为什么 `word_count/1` 使用 `defp`?

它只是 count/1 的实现细节,调用者不需要依赖它。

带走

记住三句

  1. 1

    公开 API 要小而明确。

  2. 2

    模块之间传稳定数据,不传隐含状态。

  3. 3

    实现细节用私有函数收好。

本课结束代码也跑过一次,就把这一课收好。