1.Behaviour介绍

Erlang/Elixir的Behaviour类似于其它语言中的接口(interfaces),本质就是在指定behaviours的模块中强制要求导出一些指定的函数,否则编译时会warning。

其中Elixir中使用到behaviour的典范就是GenServer, GenEvent

曾经Elixir有一个叫Behaviour的模块,但是在1.1时就已被deprecated掉了,现在你并不需要用一个Behaviour模块才能定义一个behaviour啦。

让我们一步步实现一个自定义的behaviour吧。

2. Warehouse和Warehouse.Apple测试用例

我们来定义一个仓库,它只要进货和出货,它的状态就是库存量和类型,然后再在上一层封装一个具体的apple仓库,它基于仓库,但它对外只显示库存量。

mix new behaviour_play
cd behaviour_play

先写测试,搞明白我们希望的效果

# test/behaviour_play_test.exs
defmodule BehaviourPlayTest do
use ExUnit.Case
doctest BehaviourPlay test "warehouse working" do
:ok = Warehouse.new(%{category: :fruit, store: 0})
assert Warehouse.query(:fruit) == %{category: :fruit, store: 0}
:ok = Warehouse.increase(:fruit, 10)
assert Warehouse.query(:fruit) == %{category: :fruit, store: 10}
:ok = Warehouse.decrease(:fruit, 2)
assert Warehouse.query(:fruit) == %{category: :fruit, store: 8}
assert {:not_enough, 8} = Warehouse.decrease(:fruit, 9)
end
# 隐藏了是什么类型的水果这个参数
test "add a apple warehouse" do
:ok = Warehouse.Apple.new
assert Warehouse.Apple.query == 0
:ok = Warehouse.Apple.increase(5)
assert Warehouse.Apple.query == 5
assert {:not_enough, 5} == Warehouse.Apple.decrease(6)
:ok = Warehouse.Apple.decrease(4)
assert Warehouse.Apple.query == 1
end
end

我们现在运行测试肯定是失败的,因为我们还没写warehouse.ex,但是先写测试是一个好习惯~

3. 构建Warehouse和Warehouse.Apple

# lib/warehouse.ex
defmodule Warehouse do
use GenServer
def new(%{category: category, store: store} = init_state) when is_integer(store) and store >= 0 do
{:ok, _pid} = GenServer.start(__MODULE__, init_state, name: category)
:ok
end
def query(pid) do
GenServer.call(pid, :query)
end
def increase(pid, num) when is_integer(num) and num > 0 do
GenServer.call(pid, {:increase, num})
end
def decrease(pid, num)when is_integer(num) and num > 0 do
GenServer.call(pid, {:decrease, num})
end
# GenServer callback
def handle_call(:query, _from, state) do
{:reply, state, state}
end
def handle_call({:increase, num}, _from, state) do
{:reply, :ok, %{state| store: state.store + num}}
end
def handle_call({:decrease, num}, _from, state = %{store: store})when store >= num do
{:reply, :ok, %{state| store: store - num}}
end
def handle_call({:decrease, _num}, _from, state) do
{:reply, {:not_enough, state.store}, state}
end
end

以上我们为把每一个warehouse都定义成新建立的一个GenServer进程。这时我们运行一下测试(mix test),会发现测试1已通过,但是我们具体指定到某一种类型apple的仓库还没有建出来。

# lib/apple.ex
defmodule Warehouse.Apple do
def new do
Warehouse.new(%{category: __MODULE__, store: 0})
end
def query do
state = Warehouse.query(__MODULE__)
state.store
end
def increase(num) do
Warehouse.increase(__MODULE__, num)
end
end

上面我们故意少定义了decrease/1这个函数,但是执行mix compile, 我们居然可以无任何warning的通过啦,我们只有到运行test时才能发现这个错。

可是希望的结果是在编译期间就能检查出这种低级失误来,而不是要到使用时才发现(如果我们没有完备的test,就发现不了啦)

所以我们加入behaviour。

4.使用behaviour包装Apple

# lib/warehouse.ex的最前面加上@callback属性
defmodule Warehouse do
@callback new() :: :ok
@callback query() :: number
@callback increase(num :: number) :: :ok
@callback decrease(num :: number) :: :ok| {:not_enough, number}
#接原来的内容...

然后在apple仓库中引入这个behaviour

# lib/apple.ex
defmodule Warehouse.Apple do
@behaviour Warehouse
# 接原来的内容

这时再compile就可以看到对应的warning啦。

> mix compile
Compiled lib/warehouse.ex
lib/apple.ex:1: warning: undefined behaviour function decrease/1 (for behaviour Warehouse)

我们再把按指示把decrease/1补全就

# lib/apple.ex
def decrease(num) do
Warehouse.decrease(__MODULE__, num)
end

mix compile & mix test就全过啦。

6.几个小细节

5.1 use GenServer后的callback并没有doc,不会显示在help里面

我们use GenServer后,会自动生成GenServer对应的6个callback函数。但是当我们使用

iex(1)> h Warehouse. #按下tab自动补全
Apple decrease/2 increase/2 new/1 query/1

并没有看到这些callback函数的doc...

6.2 use GenServer后为什么可以不必定义全部的callback.

可以在这里看看use GenServer时发生了什么,它先使用@behaviour GenServer, 希望定义这6个函数的缺省行为,最后再把他们defoverridable

这就是为什么我们在Warehouse里面没有定义init/1时它却没有warning的原因。这如果在Erlang中就是会warning的,因为他们没有这么flexible 的 Macro系统。

5.3 use实现的原理

可以参照看elixir的源代码, 你需要在原模块定义一个宏__using__,所以我们的终极版本应该是

# lib/warehouse.ex 添加
defmacro __using__(options) do
quote location: :keep do # 如果出错,把错误的error trace打到本模块来
@behaviour Warehouse
def new, do: Warehouse.new(%{category: unquote(options)[:category], store: 0})
def query, do: Warehouse.query(unquote(options)[:category]).store
def increase(num), do: Warehouse.increase(unquote(options)[:category], num)
def decrease(num), do: Warehouse.decrease(unquote(options)[:category], num)
defoverridable [new: 0, query: 0, increase: 1, decrease: 1]
end
end

然后把apple.ex里面只需要use Warehouse一句话就搞定啦。

所以我们可以如果想定义很多的水果apple, banana,  pear,基本就是一句话的事~

#lib/apple.ex
defmodule Warehouse.Apple do
use Warehouse, category: __MODULE__
end
#lib/banana.ex
defmodule Warehouse.Banana do
use Warehouse, category: __MODULE__
end
# lib/pear.ex
defmodule Warehouse.Pear do
use Warehouse, category: __MODULE__
end

这样的封装就很简化了很多代码,是不是感觉写elixir很爽呀~~~


show my Elixir apps around

[Elixir009]像GenServer一样用behaviour来规范接口的更多相关文章

  1. J2EE规范标准

    J2EE是一个很大的平台体系,提供了很多服务.程序接口.协议等.这么庞大的体系必须要由一系列的标准进行规范,不然将会一片混乱.通过这些规范好的接口来开发程序将会使程序更加强壮.更加有生命力.总的来说, ...

  2. Django restful 规范

    一.REST Frame Work REST与技术无关,代表的是一种软件架构风格,REST是Representational State Transfer的简称,中文翻译为"表征状态转移&q ...

  3. RESTful规范(一)

    一.学习restframework之前准备 1.json格式若想展示中文,需要ensure_ascii=False import json dic={'name':'你好'} print(json.d ...

  4. h5 plus/h5+规范使用,模块索引,教你如何去看h5+的手册

    最近看了下h5+规范的官网,开始觉得晦涩难懂,确实很乱,不过这也是基于我不理解的情况,终于艰难读完了,现在来分享下心得吧,基本看完文章,按我的方法,应该可以直接上手项目. 我准备的工具 hbuilde ...

  5. JAVAEE 是什么,如何获取各种规范jar包及各种规范的jar包源码

    1.什么是JAVA EE JAVA EE是由一系列规范组成的,规范是由JCP制定的,并且提供了参考实现.规范(Specification)是一系列接口,不包含具体实现 有以下常见的JAVA EE实现, ...

  6. API 接口开发规范

    整体规范建议采用RESTful 方式来实施. 协议 API与用户的通信协议,总是使用HTTPs协议,确保交互数据的传输安全. 域名 应该尽量将API部署在专用域名之下.https://api.exam ...

  7. 正确使用Spring Data JPA规范

    在优锐课的学习分享中探讨了关于,Spring Data JPA的创建主要是为了通过按方法名称生成查询来轻松创建查询. 但是,有时我们需要创建复杂的查询,而无法利用查询生成器.码了很多知识笔记分享给大家 ...

  8. HTML5+规范:Geolocation(管理设备位置信息) 定位

    Geolocation模块管理设备位置信息,用于获取地理位置信息,如经度.纬度等.通过plus.geolocation可获取设备位置管理对象.虽然W3C已经提供标准API获取位置信息,但在某些平台存在 ...

  9. vue 前后端分离 接口及result规范 及drf安装使用方法

    接口 # 接口:url链接,通过向链接发送不同的类型请求与参数得到相应的响应数据​# 1.在视图层书写处理请求的 视图函数# 2.在路由层为视图函数配置 url链接 => 产生接口# 3.前台通 ...

随机推荐

  1. Oracle EBS在编码方式为AL32UTF8时的注意事项

    现如今的EBS系统中,为了推进国际化的进程,以及系统向全球化的扩展,在Oracle数据库的编码方式上渐渐从支持中国本土简体中文的ZHS16GBK转向了更趋于国际化的AL32UTF8编码方式.但随之而来 ...

  2. 安卓开发-问题集-Description Resource Path Location TypeUnparsed aapt error(s)! Check the console for output.

    今天在安卓项目中 res-drawable-hdpi 替换图片的时候出现这个问题 问题现象为项目显示一个叉号,但是在项目内容的任何文件都不显示叉号, 搞了半天没发现是什么问题,然后就去项目的文件夹下 ...

  3. JAVA基础学习day24--Socket基础一UDP与TCP的基本使用

    一.网络模型 1.1.OIS参考模型 1.2.TCP/IP参考模型 1.3.网络通讯要素 IP地址:IPV4/IPV6 端口号:0-65535,一般0-1024,都被系统占用,mysql:3306,o ...

  4. Android官方多媒体API Mediacodec翻译(一)

    因近期工作调整,关于Mediacodec部分的翻译会暂停,后续有时间一定补上,非常抱歉. 本文章为根据Android Mediacodec官方英文版的原创翻译,转载请注明出处:http://www.c ...

  5. 保持listview当前位置

    保持listview滑动的位置,一般用在增加listview子item中布局的评论或者退出当前活动,再次进入继续阅读时. 利用ListView.getFirstVisiblePosition()来获取 ...

  6. python idle 清屏问题的解决

    在学习和使用python的过程中,少不了要与python idle打交道.但使用python idle都会遇到一个常见而又懊恼的问题——要怎么清屏?   我在stackoverflow看到这样两种答案 ...

  7. Jmeter代理录制脚本

    录制的原理: 1.LR/Jmeter录制是针对网络通讯协议层面的,它只关心客户端与服务器端的通讯包2.LR/Jmeter的并发测试实际上就是并发客户端与服务器端的通讯过程3.压力是通过多进程/多线程方 ...

  8. android中的坐标系以及获取坐标的方法

    android中有两种坐标系,分别称之为Android坐标系和视图坐标系.而对应的也有一些相关的方法可以获取坐标系中的 坐标值.只有搞清楚这些区别,才能在实现的时候不至于出错或者得不到你想要的效果. ...

  9. Oracle instr函数与SqlServer charindex的区别

    INSTR(C1,C2[,I[,J]]) [功能]在一个字符串中搜索指定的字符,返回发现指定的字符的位置; [说明]多字节符(汉字.全角符等),按1个字符计算 [参数] C1 被搜索的字符串      ...

  10. java基础-在dos控制台编写简易 的java程序

    第一步:在文件夹中修改隐藏的文件扩展名,让其文件的扩展名全部显示:————win7系统在文件的组织下方的文件夹和搜索选项,选择查看,将里面的隐藏选项取消: 第二步:在文件夹中新建一个text文件,将其 ...