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. CSS 子选择器(六)

    一.子选择器 子选择器中前后部分之间用一个大于号隔开,前后两部分选择符在结构上属于父子关系. 子选择器是根据左侧选择符指定的父元素,然后在该父元素下寻找匹配右侧选择符的子元素. 二.简单例子 < ...

  2. IOS SDWebImage实现原理详解

    在之前我写过SDWebImage的使用方法,主要是用与获取网络图片,没有看过的朋友可以看看. 这篇文章将主要介绍SDWebImage的实现原理,主要针对于获取网络图片的原理,如果没有第三方我们该怎么去 ...

  3. 蓝牙防丢器原理、实现与Android BLE接口编程

    本文是对已实现的蓝牙防丢器项目的总结,阐述蓝牙防丢器的原理.实现与android客户端的蓝牙BLE接口编程.在这里重点关注如何利用BLE接口来进行工程实现,对于BLE的协议.涉及到JNI的BLE接口内 ...

  4. OC小实例关于init方法不小心的错误

    *:first-child { margin-top: 0 !important; } body > *:last-child { margin-bottom: 0 !important; } ...

  5. SAM4E单片机之旅——20、DMAC之使用Multi-buffer进行内存拷贝

    这次使用这个DMAC的Multi-buffer传输功能,将两个缓冲区的内容拷贝至一个连续的缓冲区中. 一. DMAC 在M4中,DMA控制器(DMAC)比外设DMA控制器(PDC)要复杂,但是功能更加 ...

  6. Effective Java 44 Write doc comments for all exposed API elements

    Principle You must precede every exported class, interface, constructor, method, and field declarati ...

  7. Eclipse 一直提示 loading descriptor for 的解决方法

    启动eclipse之后,进行相关操作时,弹出界面,提示:loading descriptor for xxx 解决方法: 在Eclipse左侧的Project Explorer 最右上角有一个小钮,鼠 ...

  8. PHP模拟发送POST请求之三、用Telnet和fsockopen()模拟发送POST信息

    了解完了HTTP头信息和URL信息的具体内容,我们开始尝试自己动手写一段头信息发送到服务器.Windows内置命令Telnet可以帮助我们发送简单的HTTP请求. 并且TELNET是一个特别灵活的工具 ...

  9. windows 下my.ini的配置优化

    线上有若干WIN环境下的MySQL,需要进行优化配置,列出以下参数方便查阅 # For advice on how to change settings please see # http://dev ...

  10. solr 导入数据

    从sqlserver导入数据到solr, solr 采用的版本6.0.1,并且本机解压到:F:\Tool\solr-6.0.1: 1. 命令启动solr,创建core 启动,进入solr文件目录下,执 ...