[Elixir009]像GenServer一样用behaviour来规范接口

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.exsdefmodule 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.exdefmodule 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

时间: 2024-11-16 00:34:11

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

贡献一份精心整理的RBAC规范接口及其解说

这是花费了我半天时间从国标GBT 25062-2010 RBAC上整理得到的IRBACService接口.不要感觉惊奇,GBT 25062-2010 RBAC是和美国的NIST RBAC标准完全一样的.如果您希望阅读GBT 25062-2010 RBAC国标文档的话可以从这里下载http://git.oschina.net/anycmd/anycmd/tree/master/Docs. 国家标准文档是用z语言书写的,一下子读不懂没关系,不要灰心,把丢掉的知识拾起来就可以了.咱们都是在同样的教育体

仓储规范接口

/// <summary> /// 基础的数据操作规范 /// </summary> /// <typeparam name="TEntity"></typeparam> public interface IRepository<TEntity> where TEntity : class { /// <summary> /// 添加实体(单个) /// </summary> /// <param

Android研发规范

/* Style Definitions */ table.MsoNormalTable {mso-style-name:普通表格; mso-tstyle-rowband-size:0; mso-tstyle-colband-size:0; mso-style-noshow:yes; mso-style-priority:99; mso-style-parent:""; mso-padding-alt:0cm 5.4pt 0cm 5.4pt; mso-para-margin:0cm;

J2EE规范标准

J2EE是一个很大的平台体系,提供了很多服务.程序接口.协议等.这么庞大的体系必须要由一系列的标准进行规范,不然将会一片混乱.通过这些规范好的接口来开发程序将会使程序更加强壮.更加有生命力.总的来说,规范是一种抽象思维的体现,它的好处就是达到了约束所有厂商的效果,抽象出一个统一的规范接口,使我们在编程时使用统一的接口,兼容性得到保证,与底层具体实现达到高度隔离解耦.我们知道,J2EE有十三个规范,每个标准提供不同的服务,应用的场合也不同,这十三个规范并非全部都是标准的,只有标准的规范才会在JDK

微服务架构实战:Swagger规范RESTful API

转载本文需注明出处:EAII企业架构创新研究院,违者必究.如需加入微信群参与微课堂.架构设计与讨论直播请直接回复公众号:"EAII企业架构创新研究院".(微信号:eaworld)   导读:本文是EAII微服务系列文章之一.随着微服务架构的流行,REST风格也是大势所趋.那么,什么是REST?如何规范我们的RESTFUL API 文档?本文中,作者主要基于以上两个话题进行讨论并探讨在数字化企业云平台实践中如何规范RESTful文档. REST的引入 随着微服务架构的广泛流行,REST风

Web Service相关规范

Web Service概述 Web Service是一个平台独立的.低耦合的.自包含的.基于可编程的Web应用程序,可使用开放的XML(标准通用标记语言下的一个子集)标准来描述.发布.发现.协调和配置这些应用程序,用于开发分布式的互操作的应用程序. 在Web Service的体系架构中有三个角色:服务提供者(Service Provider),也叫服务生产者:服务请求者(Service Requester),也叫服务消费者:服务注册中心(Service Register),也叫服务代理,服务提供

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

Geolocation模块管理设备位置信息,用于获取地理位置信息,如经度.纬度等.通过plus.geolocation可获取设备位置管理对象.虽然W3C已经提供标准API获取位置信息,但在某些平台存在差异或未实现,为了保持各平台的统一性,定义此规范接口获取位置信息. 1.方法 1.1.getCurrentPosition: 获取当前设备位置信息 void plus.geolocation.getCurrentPosition( successCB, errorCB, option ); 说明:位

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

1.什么是JAVA EE JAVA EE是由一系列规范组成的,规范是由JCP制定的,并且提供了参考实现.规范(Specification)是一系列接口,不包含具体实现 有以下常见的JAVA EE实现,包括JBOSS.GLASSFISH等.而tomcat是一个servlet容器,实现了servlet规范.jsp规范.但是它并没有实现EJB.JMS.JPA等规范,所以tomcat不是一个完整的JAVA EE实现 在oracle网站上,下载JAVA EE SDK时,会同时下载GLASSFISH,也就是

JCA——一个名不见经传却重要的JavaEE规范

JCA(Java EE Connector Architecture)规范可以说是JavaEE规范集合里最"默默无闻"的,在JavaEE1.3规范发布时就加入了,比现在重要成员JPA, CDI等都早了很多.从应用开发角度来看,开发一个很普通的Web应用程序,只有几个页面,使用Servlet就可以完成,用JDBC API保存信息到数据库中,部署这个应用到JavaEE应用服务器中时,就会用到JCA技术.这个很简单的应用程序只用了庞大的JavaEE规范集30多项中的Servlet和JCA两项