Russell-X-Shanso
大家好,我是狼夜行(嫌不好记叫我哈士奇也成)。仅以此系列随笔跟大家来讨论一下erlang和设计模式。
本文背景:
(1)设计模式在面向对象语言编程中广泛受到关注;
(2)对于非面向对象语言而言,各领域的朋友也有总结一些设计模式,整体受众及受关注度不大;
(3)国内erlang圈不大,玩票的游击队朋友较多,新手引导少;
本文目标:
(1)按面向对象中设计模式的套路生搬硬套过来,借用erlang的形式向erlang为主要语言的朋友们进行介绍,希望对两者更熟悉的朋友能出来指正及修改我可能存在的一些理解的错误,来给朋友们增加了解;
(2)帮助erlang新手了解erlang的一些小技巧;
(3)给大家开一些脑洞,想出更多更好的结构与系统;
本文不可用于:
(1)较真;
(2)完全照搬;
(3)强行装逼;
注意事项:
(1)模式不是形式,请忘掉文中的具体代码实现,感受模式的意义与方式,并在实际应用时以最贴合实际的方式加以利用。基本上所有的模式都可以用其它非模式的方式实现,切勿为了模式而模式;
(2)市面上设计模式的文档与书都很多(多以面向对象语言描述),有兴趣的朋友可以找一些来细细研究,并以书中的描述与理解为主,本文一家之言,在强行仅供参考;
(3)文中代码为了简洁易懂的需要,基本上未做trycatch异常处理、容错或速错对应处理,请在实际应用模式时根据自己的需要考虑到这些内容;
Gof书中,传统面向对象设计模式分为三种类型,共23种。
-
创建型模式:单例模式、抽象工厂模式、建造者模式、工厂模式、原型模式。
-
结构型模式:适配器模式、桥接模式、装饰模式、组合模式、外观模式、享元模式、代理模式。
-
行为型模式:模版方法模式、命令模式、迭代器模式、观察者模式、中介者模式、备忘录模式、解释器模式、状态模式、策略模式、职责链模式、访问者模式。
1、单例模式(Singleton Method)
(1)意图(面向对象):保证一个类仅有一个实例,并提供一个访问它的全局访问点。
(注:可以通俗地理解为一个全局可用且唯一的对象或资源,加以良好的管理与封装,达到很方便使用的目标即可;)
(2)类型:创建类模式;
(3)面向对象中的实现方式:通常使用一个管理类的静态成员对象实现,通过全局统一的静态变量进行访问,访问时若发现对象未实例化,则立即进行创建,否则直接返回已实例化的对象;
(4)优点及意义:将对象资源进程 进行统一管理,并加以封装,使得各处通过统一接口来调用同一个全局实例;
Erlang中的逻辑:
otp中命名gen_server等行为均采用了单例模式的理念,在这里我们为说明其思想,使用进程的方式编写;
(使用单例作为第一个模式来讲解,一方面是由于它足够直白容易理解,另一方面也是由于erlang源码中有大量的实现可供例举;)
以下是一段用erlang描述单例模式的示例代码,使用一个递归进程来实现单例:
1 %% sington.erl 2 -module(singleton). 3 -author("Russell-X-Shanso"). 4 5 %% API 6 -export([ get_singleton/0 ]). 7 8 -define( SINGLETONNAME, singleton_example ). 9 10 %% 对外接口,获取单例进程 11 get_singleton( ) -> 12 case whereis( ?SINGLETONNAME ) of 13 undefined -> 14 Singleton = spawn( fun singleton_loop/0 ), 15 register( ?SINGLETONNAME, Singleton ), 16 Singleton; 17 Pid -> Pid; 18 end. 19 20 %% 单例实体循环 21 singleton_loop( ) -> 22 receive 23 Msg -> 24 do_msg( Msg ), 25 singleton_loop( ) 26 end. 27 28 %% 单例消息处理函数 29 do_msg( Msg ) -> 30 %% 执行所需动作 31 io:format( "Message:~p~n", [ Msg ]).
而在全局使用时,我们就可以:
1 %% sington_user.erl 2 -module(singleton_user). 3 -author("Russell-X-Shanso"). 4 5 %% API 6 -export([test/0]). 7 8 test( ) -> 9 Sington = singleton:get_singleton(), 10 Sington ! "This is test1!", 11 ok.
测试效果很简单:

以上的代码中,spawn( fun singleton_loop/0 )一行派生了一个进程,并在后面通过register方法进行注册形成全局唯一的实例;
我们结合OTP中的源码来看一下实际应用单例的情况,我们都知道gen_server等行为都是可以以命名方式来进行start或start_link,并且可以通过gen_server:call的方式根据命名进行调用的,这里是一处典型的单例模式:
gen_server的start中,涉及命名的启动方式会调用gen:start/6,见下面代码中start及start_link方法中带有Name参的部分:
%% gen_server.erl line 157-167 of erl_ver_17.3 start(Mod, Args, Options) -> gen:start(?MODULE, nolink, Mod, Args, Options). start(Name, Mod, Args, Options) -> gen:start(?MODULE, nolink, Name, Mod, Args, Options). start_link(Mod, Args, Options) -> gen:start(?MODULE, link, Mod, Args, Options). start_link(Name, Mod, Args, Options) -> gen:start(?MODULE, link, Name, Mod, Args, Options).
而在gen.erl的gen:start/6中:
%% gen.erl line 69-83 of erl_ver_17.3 -spec start(module(), linkage(), emgr_name(), module(), term(), options()) -> start_ret(). start(GenMod, LinkP, Name, Mod, Args, Options) -> case where(Name) of undefined -> do_spawn(GenMod, LinkP, Name, Mod, Args, Options); Pid -> {error, {already_started, Pid}} end.
其中where/1方法封装了globalvialocal三种启动情况,local方法中封装了whereis/1方法,三种情况的意义近似,均为根据命名来寻找到单例的实例(pid);
而do_spawn/6方法通过proc_lib:start方法间接调用了gen:init_it/7,此方法中调用的name_register也封装了globalvialocal三种启动情况下的命名注册分支,其中local分支中直接使用register/2,与前面例子中更为明显近似一些。
%% gen.erl line 282-304 of erl_ver_17.3 where({global, Name}) -> global:whereis_name(Name); where({via, Module, Name}) -> Module:whereis_name(Name); where({local, Name}) -> whereis(Name). name_register({local, Name} = LN) -> try register(Name, self()) of true -> true catch error:_ -> {false, where(LN)} end; name_register({global, Name} = GN) -> case global:register_name(Name, self()) of yes -> true; no -> {false, where(GN)} end; name_register({via, Module, Name} = GN) -> case Module:register_name(Name, self()) of yes -> true; no -> {false, where(GN)} end.
要点:
(1)全局唯一进程(面向对象语言中用静态成员变量实现; erlang中用命名进程实现);
(2)统一接口(面向对象语言中用静态成员函数访问静态成员变量;erlang中可以用消息、方法、行为等方式来实现,事实上由于otp里面已经实现了单例,我们可以通过对命名gen_server的使用来间接享受单例带来的便利 )
2、原型模式(Prototype)
(1)意图(面向对象):用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。
(2)类型:创建类模式;
(3)面向对象中的实现方式:为类实现一个clone方法,构建新对象时可通过clone方法实现对现有对象的内容复制;
(4)优点及意义:实现对一个对象瞬时状态的复制,将一个对象的仿照建立的过程全部隐藏起来;或者用于对一个频繁变化的数值的快照性复制;
Erlang中的逻辑:
由于Erlang中没有面向对象时较正式的类-对象概念,因此我借用gen_server的数据复制来表达原型模式的理念,这个模式较为简单,我们用下面代码中的小技巧说明一下:
1 %% prototype_server.erl 2 -module(prototype_server). 3 -author("Russell-X-Shanso"). 4 5 -behaviour(gen_server). 6 7 %% API 8 -export([start/1, clone/1]). 9 -export([get/1, set/2]). 10 11 %% gen_server callbacks 12 -export([init/1, 13 handle_call/3, 14 handle_cast/2, 15 handle_info/2, 16 terminate/2, 17 code_change/3]). 18 19 %% Server 启动 20 start( InitValue ) -> 21 gen_server:start( ?MODULE, InitValue, []). 22 23 %% 克隆方法 24 clone( OtherServer ) when is_pid( OtherServer ) -> 25 gen_server:start( ?MODULE, OtherServer, [] ); 26 clone( _OtherServer ) -> 27 { error, <<"It's not a prototype">> }. 28 29 %% 方便检验做的辅助接口 30 get( ServerPid ) when is_pid( ServerPid ) -> 31 gen_server:call( ServerPid, { get } ). 32 set( ServerPid, Value ) when is_pid( ServerPid ) andalso is_integer( Value ) -> 33 gen_server:call( ServerPid, { set, Value } ). 34 35 36 init( OtherServer ) when is_pid( OtherServer ) -> 37 %% 克隆核心数据,这里应根据需要实现全部克隆或部分克隆 38 CloneValue = gen_server:call( OtherServer, { get } ), 39 { ok, #{ test_value => CloneValue } }; 40 init(InitValue) -> 41 {ok, #{ test_value => InitValue }}. 42 43 handle_call( { get }, _From, State) -> 44 {reply, maps:get( test_value, State ), State }; 45 handle_call( { set, Value }, _From, State) -> 46 {reply, ok, State#{ test_value => Value } }; 47 handle_call(_Request, _From, State) -> 48 {reply, ok, State}. 49 50 51 handle_cast(_Request, State) -> 52 {noreply, State}. 53 54 handle_info(_Info, State) -> 55 {noreply, State}. 56 57 terminate(_Reason, _State) -> 58 ok. 59 60 code_change(_OldVsn, State, _Extra) -> 61 {ok, State}.
测试代码:
1 %% prototype_test.erl 2 -module(prototype_test). 3 -author("Russell-X-Shanso"). 4 5 %% API 6 -export([ test/0 ]). 7 8 9 test() -> 10 %% 启动 Server_A 11 { ok, Server_A } = prototype_server:start( 1 ), 12 %% 为 Server_A 的 State 赋新值 13 ok = prototype_server:set( Server_A, 2 ), 14 %% 根据 Server_A 克隆 Server_B, 实质为将ServerA中 State的值复制给ServerB 15 { ok, Server_B } = prototype_server:clone( Server_A ), 16 %% 获取ServerB的数值检验复制成功 17 ValueB1 = prototype_server:get( Server_B ), 18 %% 对Server再次赋值,分别检验ServerA与ServerB的值, 来验证两者的独立性 19 ok = prototype_server:set( Server_A, 3 ), 20 ok = prototype_server:set( Server_B, 4 ), 21 ValueA = prototype_server:get( Server_A ), 22 ValueB2 = prototype_server:get( Server_B ), 23 io:format( "ValueB1 = ~p~nValueA = ~p~nValueB2= ~p~n", [ ValueB1, ValueA, ValueB2 ] ).
测试效果:
可以看到,ServerA初始化并修改过值后,其State中test_value的值变为2;
而通过clone方法,ServerB拷贝了ServerA中State的关键值(根据实际需要,也可以选择直接进行State的拷贝,或者选择复制State中保存的多个值中的一部分),核心数据成为2;
clone之后,ServerA与ServerB的生命线不再有交集,可以分别接受新的赋值操作并保持不同的数据指标;