Clients and Servers

Callback to the Future

Weird version of Marty from Back to The Future

Trong chương trước chúng ta đã tìm hiểu về OTP và làm quen với một ví dụ đơn giản cũng như ở cuối chương đã để cập tới một hành vi sử dụng trong OTP. Trong chương này chúng ta sẽ làm quen với gen_server, nó là một trong những hành vi được sử dụng nhiều nhất trong các ứng dụng. interface của gen_server khá giống với những gì chúng ta đã viết trong module my_serverchương trước đó. Nó cung cấp cho bạn một số hàm để sử dụng The first OTP behaviour we'll see is one of the most used ones. Its name is gen_server and it has an interface a bit similar to the one we've written with my_server in last chapter; it gives you a few functions to use it and in exchange, your module has to already have a few functions gen_server will use.

init

Hàm đầu tiên mà chúng ta sẽ nói đến đó là hàm init/1, hàm này thoạt nhìn giống với hàm 'init' mà chúng ta đã sử dụng trong module my_server trước đó, nó được dùng để khởi tạo trạng thái ban đầu của server và thực hiện các công việc khác duy nhất một lần khi khởi tạo. Kêt quả sau khi được khởi tạo sẽ có một trong các dạng sau {ok, State}, {ok, State, TimeOut}, {ok, State, hibernate}, {stop, Reason} hoặc ignore The first one is an init/1 function. It is similar to the one we've used with my_server in that it is used to initialize the server's state and do all of these one-time tasks that it will depend on. The function can return {ok, State}, {ok, State, TimeOut}, {ok, State, hibernate}, {stop, Reason} or ignore.

Giá trị {ok, State} cho biết việc khởi tạo thành công và không cần phải giải thích gì nhiều, bạn chỉ cần hiểu là trạng thái biến State sẽ được truyền trực tiếp vào trong hàm lặp chính ( main loop ) của tiến trình như một trạng thái cần lưu và được sử dụng về sau. Biến TimeOut được thêm vào bộ trong kết quả trả về như một cách để xác định thời gian chờ trước khi server nhận được một tin nhắn thông báo. Trong trường hợp mà server phải đợi quá lâu vượt quá thời gian chờ, một tin nhắc đặc biệt ( có dạng atom timeout) sẽ được gửi tới và được xử lí trong hàm handle_info/2 ( chúng ta sẽ tìm hiểu hàm này một lát nữa). The normal {ok, State}, return value doesn't really need explaining past saying that State will be passed directly to the main loop of the process as the state to keep later on. The TimeOut variable is meant to be added to the tuple whenever you need a deadline before which you expect the server to receive a message. If no message is received before the deadline, a special one (the atom timeout) is sent to the server, which should be handled with handle_info/2 (more on this later.)

Mặt khác, nếu bạn tin rằng nó sẽ mất nhiều thời gian trước khi nhận được phản hồi nhưng lại lo ngại về vấn đề bộ nhớ thì bạn có thể thêm vào bộ một atom hibernate. Bằng cách này nó sẽ giảm kích thước của trạng thái của tiến trình cho tới khi nó nhận được một thông báo. Còn nếu bạn vẫn còn nghi nghờ về việc sử dụng hibernation, thì bạn có thể hoàn toàn bỏ qua. On the other hand, if you do expect the process to take a long time before getting a reply and are worried about memory, you can add the hibernate atom to the tuple. Hibernation basically reduces the size of the process' state until it gets a message, at the cost of some processing power. If you are in doubt about using hibernation, you probably don't need it.

Giá trị {stop, Reason} sữ được đưa ra để thông báo về một sự cố sai sót trong quá trình khởi tạo. Returning {stop, Reason} should be done when something went wrong during the initialization.

Lưu ý: Đây là thông tin kỹ thuật chi tiết hơn của hibernation của tiến trình. Không có gì lo láng nếu một số độc giả không hiểu về tráng thái này hay không quan tâm bởi vì thấy nó không có ích gì cả. Khi một hàm BIF được gọi theo cấu trúc erlang:hibernate(M,F,A), ngay lập tức call stack của tiến trình đang chạy sẽ được loại bỏ ( hàm sẽ không bao được trả về ). bộ dọn rác (garbage collection ) sẽ được kích hoạt, và những gì còn lại là một continuous heap giảm tới bằng kích thước của dữ liệu trong tiến trình. Thực chất đó chỉ là nén tất cả các dữ liệu lại để tiết kiết không gian bộ nhớ trong tiến trình mà thôi. here's a more technical definition of process hibernation. It's no big deal if some readers do not understand or care about it. When the BIF erlang:hibernate(M,F,A) is called, the call stack for the currently running process is discarded (the function never returns). The garbage collection then kicks in, and what's left is one continuous heap that is shrunken to the size of the data in the process. This basically compacts all the data so the process takes less place.

Một khi tiến trình nhận được tin nhắn thông báo, hàm M:F cùng với đối số A sẽ được gọi là thực thi. Once the process receives a message, the function M:F with A as arguments is called and the execution resumes.

Lưu ý: Tại thời điểm hàm init/1 đang trong quá trình khởi tạo, các chức năng thực thi sẽ bị chặn lại đối với tiến trình mà sinh (spawn) ra server. Lí giải việc làm này à bởi vì để đảm bảo mọi thứ diễn ra ổn định, nó cần đợi một tin nhắn thông báo 'ready' được gửi tự động từ module gen_server while init/1 is running, execution is blocked in the process that spawned the server. This is because it is waiting for a 'ready' message sent automatically by the gen_server module to make sure everything went fine.

handle_call

Tiếp theo chúng ta cần biết về hàm handle_call/3 , hàm này được sử dụng để xử lí các tin nhắn đồng bộ ( sớm thôi chúng ta sẽ xem cách sử dụng chúng như thế nào ), hàm này nhận 3 tham số đầu vào: Request, From, và State, vâng nó khá giống với nhưng gì chúng ta đã làm trước đó với hàm handle_call/3 trong module my_server. Chỉ có một điều khác biệt lớn nhất giữa chúng đó là cách mà bạn sẽ phản hồi lại một thông báo tin nhắn ra sao. trong module my_server chúng ta đã sử dụng một mẫu tổng quát chung đó là gọi tới my_server:reply/2 cho việc phản hồi tới tiến trình. Còn đối với gen_server có tới tận 8 cách khác nhau cho việc này dưới dạng các bộ dữ liệu. is used to work with synchronous messages (we'll see how to send them soon). It takes 3 arguments: Request, From, and State. It's pretty similar to how we programmed our own handle_call/3 in my_server. The biggest difference is how you reply to messages. In our own abstraction of a server, it was necessary to use my_server:reply/2 to talk back to the process. In the case of gen_servers, there are 8 different return values possible, taking the form of tuples.

Vì có khá nhiều, lên chúng ta sẽ liệt kê chúng: Because there are many of them, here's a simple list instead:

{reply,Reply,NewState}
{reply,Reply,NewState,Timeout}
{reply,Reply,NewState,hibernate}
{noreply,NewState}
{noreply,NewState,Timeout}
{noreply,NewState,hibernate}
{stop,Reason,Reply,NewState}
{stop,Reason,NewState}

Trong tất cả các mẫu này thì Timeouthibernate tương tự như trong hàm init/1. Tiếp đó đối với các mẫu có Reply, nó sẽ luôn được gửi tới những người mà đã thực hiện lời gọi tới server. Hãy để ý là chúng ta có tới tận 3 mẫu noreply, đối với các mẫu này khi ban sử dụng, phần chung của server sẽ giả định rằng bản thân bạn đang xử lí việc gửi thông báo tin nhắn phản hồi. Và nó có thể được xử lí cùng với hàm gen_server:reply/2, cách dùng hàm này tượng tự như những gì chúng ta đã làm với hàm my_server:reply/2. For all of these, Timeout and hibernate work the same way as for init/1. Whatever is in Reply will be sent back to whoever called the server in the first place. Notice that there are three possible noreply options. When you use noreply, the generic part of the server will assume you're taking care of sending the reply back yourself. This can be done with gen_server:reply/2, which can be used in the same way as my_server:reply/2.

Phần lớn trường hợp, bạn chỉ cần phải sử dụng mẫy reply, tuy nhiên vẫn có một số lí do nhất định để sử dụng mẫu noreply: khi bạn muốn một tién trình khác gửi phản hồi tới cho bạn hay khi bạn muốn gửi một tin nhắn xác nhận ( ví dự như 'hey !Tôi đã nhận được tinh nhắn rồi') nhưng vẫn muốn xử lí nó sau đó ( không phản hồi tại thời điểm này ), etc. Nếu bạn càn thực hiện các hành động như vậy, thì chắc chắn việc sử dụng gen_server:reply/2 là cần thiết vì nếu bạn gọi một hàm khác, nó sẽ gây phải chờ và có thể dẫn tới crash server. Most of the time, you'll only need the reply tuples. There are still a few valid reasons to use noreply: whenever you want another process to send the reply for you or when you want to send an acknowledgement ('hey! I received the message!') but still process it afterwards (without replying this time), etc. If this is what you choose to do, it is absolutely necessary to use gen_server:reply/2 because otherwise the call will time out and cause a crash.

handle_cast

Không có gì ngạc nhiên , nếu như hàm handle_cast/2 giống với nhưng gì chúng ta đã làm trong module gen_server:reply/2, nó vẫn sẽ nhận 2 tham số đầu vào là 2 biến cho MessageState, và sử dụng cho việc gửi các tin nhắn bất đồng bộ. Sau cùng những gì bạn cần làm còn lại khá giống với những gì bạn vừa thấy trong hàm handle_call/3. Chỉ có một callback works a lot like the one we had in my_server: it takes the parameters Message and State and is used to handle asynchronous calls. You do whatever you want in there, in a manner quite similar to what's doable with handle_call/3. Tuy nhiên mặc khác có một sư khác biệt đó là nó sẽ bạn chỉ trả về các bộ dữ liệu mà không kèm với thông tin phản hồi: , only tuples without replies are valid return values:

{noreply,NewState}
{noreply,NewState,Timeout}
{noreply,NewState,hibernate}
{stop,Reason,NewState}

handle_info

Tôi đã từng đề cập tới vân để server của chúng ta đã bỏ qua các tin nhắn mà không khớp với mẫu interface của chúng ta đúng không ? Đúng vậy, tuy nhiên giờ chúng ta có thể dễ dàng giải quyết chúng với hàm handle_info/2 . Cách dùng hàm này khá giống với những gì chúng ta dùng với hàm handle_cast/2 trước đó cùng với việc trả về bộ dữ liệu tương tự, chỉ khác một điều đó là callback chỉ hoạt động đối với các thông báo tin nhắc mà được gửi trực tiếp cùng với toán tử ! và một số hàm đặc biệt giống như timeout trong init/1, hay monitors' notifications, 'EXIT' signals. You know how I mentioned our own server didn't really deal with messages that do not fit our interface, right? Well handle_info/2 is the solution. It's very similar to handle_cast/2 and in fact returns the same tuples. The difference is that this callback is only there for messages that were sent directly with the ! operator and special ones like init/1's timeout, monitors' notifications and 'EXIT' signals.

terminate

hàm callback terminate/2 này sẽ được sử dụng khi một trong các hàm handle_Something trả về bộ dữ liệu có dạng {stop, Reason, NewState}, {stop, Reason, Reply, NewState}. Như bạn thấy, nó sẽ chấp nhận 2 tham số đầu vào đó là ReasonState tương ứng với các giá trị từ bộ dữ liệu stop. is called whenever one of the three handle_Something functions returns a tuple of the form {stop, Reason, NewState} or {stop, Reason, Reply, NewState}. It takes two parameters, Reason and State, corresponding to the same values from the stop tuples.

hàm terminate/2 cũng được gọi khi tiến trình cha ( tiến trình mà sinh ra tiên trình hiện tại ) châm dứt, Nhưng chỉ trong trường hợp mà gen_server trapping exits will also be called when its parent (the process that spawned it) dies, if and only if the gen_server is trapping exits.

Lưu ý: Nếu có bất kỳ lí do nào khác ngoài normal, shutdown hay {shutdown, Term} , thì hàm terminate/2 cũng sẽ được gọi và OTP framework sẽ nhìn nhận nó như một trường hợp lỗi và tiến hành bắt đầu ghi nhận một số thông tin cho bạn. if any reason other than normal, shutdown hay {shutdown, Term} is used when terminate/2 is called, the OTP framework will see this as a failure and start logging a bunch of stuff here and there for you.

Có thể nói rằng hàm terminate/2 là một sự nghịch đảo đối với hàm init/1, mỗi hành vi diễn ra trong init/1 sẽ có một hành vi trái ngược với nó trong terminate/2. Hình dung nó như một người vệ cho server của bạn, nó sẽ có nhiệm vụ giúp khóa cửa lại sau khi đảm bảo rằng tất cả mọi người đã đi ra ngoài, không còn một ai ở bên trong nữa. Dĩ nhiên là bản thân máy ảo cũng thực hiện chức năng này qua việc xóa bỏ tất cả các ETS tables và đóng tất cả các ports lại ,etc. Một lưu ý nữa đó là giá trị trả về của hàm này không nhất thiết phải quan tâm tới bởi vì chương trình sẽ dừng thực thi mã sau khi gọi hàm này. This function is pretty much the direct opposite of init/1 so whatever was done in there should have its opposite in terminate/2. It's your server's janitor, the function in charge of locking the door after making sure everyone's gone. Of course, the function is helped by the VM itself, which should usually delete all ETS tables, close all ports, etc. for you. Note that the return value of this function doesn't really matter, because the code stops executing after it's been called.

code_change

như trong chương trước đó chúng ta đã học cách viết một hàm dùng cho việc nâng cấp chương trình, tương tự trong gen_server cũng hỗ trợ hành vi này, bằng cách sử dụng hàm code_change/3. Hàm này được sử dụng dưới dạng code_change(PreviousVersion, State, Extra), trong đó biến PreviousVersion là một thuật ngữ dùng để chỉ phiên bản trong trường hợp nâng cấp (nếu bạn không nhớ hãy xem lại chương More About Modules), hay {down, Version} tương ứng với trường hợp hạ cấp ( chỉ cần tải nạp đoạn mã ở bản trước đó vào ). Biến State được dùng để lưu trữ tất cả các trang thái của server do đó nó luôn sẵn có để bạn có thể chuyển đổi. is there to let you upgrade code. It takes the form code_change(PreviousVersion, State, Extra). Here, the variable PreviousVersion is either the version term itself in the case of an upgrade (read More About Modules again if you forget what this is), or {down, Version} in the case of a downgrade (just reloading older code). The State variable holds all of the current's server state so you can convert it.

Hãy hình dùng rằng tại một thời điểm chúng ta sử dụng kiểu cấu trúc orddict để lưu trữ tất cả các dữ liệu. tuy nhiên, Một thời gian sau đó, khi lượng dữ liệu tăng lên, cấu trúc orddict sẽ càng trở lên chậm hơn so chúng ta buộc phải quyết dịnh thay đổi sang một kiểu cấu trúc từ điển phổ thông, và để an toàn hơn, tránh gây ra lỗi crash server trong lần gọi hàm tiếp theo khi chuyển đổi kiểu giữa hai kiểu cấu trúc, tất cả những gì bạn phải làm đó là trả về một trạng tháu mới cùng với dạng {ok, NewState}. Imagine for a moment that we used an orddict to store all of our data. However, as time goes on, the orddict becomes too slow and we decide to change it for a regular dict. In order to avoid the process crashing on the next function call, the conversion from one data structure to the other can be done in there, safely. All you have to do is return the new state with {ok, NewState}.

a cat with an eye patch

Còn lại biến Extra , nó thực sự không quan trọng khiến chúng ta phải bận tâm, hầu hết trong các trường hơp nó được sử dụng để hỗ trợ triển khai trong các ứng dựng OTP lớn, nới mà bạn sẽ sử dụng các công cụ đặc biệt để nâng cấp toàn bộ máy ảo, do đó trong phạm vi của cuốn sách này chúng ta không thực sự phải dùng tới nó. The Extra variable isn't something we'll worry about for now. It's mostly used in larger OTP deployment, where specific tools exist to upgrade entire releases on a VM. We're not there yet.

Bây giờ chúng ta đã xem xét hết tất cả các hàm callback cần phải định nghĩa rồi. Đừng lo lắng nếu bạn cảm thấy có chút bối rối, khó hiểu, bởi vì đôi lúc chúng ta phải đi đường vòng khi tìm hiểu về OTP framework, khi mà muốn hiểu được A của nó thì bạn phải hiểu B, nhưng để để sử dụng được trong B, bạn phải quay trở lại để nhìn A, thật sự rất vòng vèo gây khó chịu do đó cách tốt nhất để tránh những nhầm lẫn này đó là thực hiện ví dụ về gen_server. So now we've got all the callbacks defined. Don't worry if you're a bit lost: the OTP framework is a bit circular sometimes, where to understand part A of the framework you have to understand part B, but then part B requires to see part A to be useful. The best way to get over that confusion is to actually implement a gen_server.

.BEAM me up, Scotty!

Chúng ta sẽ thực hiện ví dụ qua một module được gọi là kitty_gen_server. Module này sẽ thực hiện các chức năng tương tự giống với module kitty_server2 mà chúng ta đã làm trước đó cùng với một ít thay đổi trong API. Nào hãy bắt đầu tao mới một module đã viết những đoạn mã sau vào: This is going to be the kitty_gen_server. It's going to be mostly similar to kitty_server2, with only minimal API changes. First start a new module with the following lines in it:

-module(kitty_gen_server).
-behaviour(gen_server).

Hãy thử biện dịch và ngay lập tức bạn sẽ nhin thấy gióng như dưới đây: And try to compile it. You should get something like this:

1> c(kitty_gen_server).
./kitty_gen_server.erl:2: Warning: undefined callback function code_change/3 (behaviour 'gen_server')
./kitty_gen_server.erl:2: Warning: undefined callback function handle_call/3 (behaviour 'gen_server')
./kitty_gen_server.erl:2: Warning: undefined callback function handle_cast/2 (behaviour 'gen_server')
./kitty_gen_server.erl:2: Warning: undefined callback function handle_info/2 (behaviour 'gen_server')
./kitty_gen_server.erl:2: Warning: undefined callback function init/1 (behaviour 'gen_server')
./kitty_gen_server.erl:2: Warning: undefined callback function terminate/2 (behaviour 'gen_server')
{ok,kitty_gen_server}

Ok, công cụ biện dịch hoạt động nhưng kèm theo đó là một số cảnh báo về một số hàm callback không được dịnh nghĩa. Lí giải điều này đó là bởi vì đó là những hàm yêu cầu hay nói một cách cụ thể là những hành vi bắt buộc trong gen_server. Hành vi trong gen_server là một cách cơ bản để xác định các hàm, chức năng mà một module mong muốn một module khác phải có. Hành vi cũng là một hợp đồng gắn kết giữa các phần đúng và phần lỗi trong đoạn mã của bạn. The compilation worked, but there are warnings about missing callbacks. This is because of the gen_server behaviour. A behaviour is basically a way for a module to specify functions it expects another module to have. The behaviour is the contract sealing the deal between the well-behaved generic part of the code and the specific, error-prone part of the code (yours).

Lưu ý: cả hai cách khai báo là 'behavior' và 'behaviour đều được chấp nhận trong Erlang. both 'behavior' and 'behaviour' are accepted by the Erlang compiler.

Để khai báo hành vi rất đơn giản, chỉ cần xuất hàm bằng tên behaviour_info/1 và làm như sau: Defining your own behaviours is really simple. You just need to export a function called behaviour_info/1 implemented as follows:

-module(my_behaviour).
-export([behaviour_info/1]).

%% init/1, some_fun/0 and other/3 are now expected callbacks
behaviour_info(callbacks) -> [{init,1}, {some_fun, 0}, {other, 3}];
behaviour_info(_) -> undefined.

Đó là những gì mà chúng ta phải làm, bạn chỉ cần đặt đoạn mã -behaviour(my_behaviour). trong module của bạn và thực hiện chúng, nếu bạn quên thì có thể dựa trên những cảnh bảo từ trình biên dịch để thêm vào. Bây giờ chúng ta sẽ quay trở lại module kitty server. And that's about it for behaviours. You can just use -behaviour(my_behaviour). in a module implementing them to get compiler warnings if you forgot a function. Anyway, back to our third kitty server.

hàm đầu tiên chúng ta cần thực hiện đó là hàm start_link/0, nó có thể viết như sau: The first function we had was start_link/0. This one can be changed to the following:

start_link() -> gen_server:start_link(?MODULE, [], []).

Trong đó hãy để ý thám số đầu tiên của là một callback module, và tham số thứ hai truyền vào là một danh sách các tham số sẽ truyền vào hàm init/1 , và tham số tiếp theo như một lựa chọn để sử dụng cho mục đích gỡ lỗi vì vậy tôi sẽ không đề cập chi tiết lúc này. Ngoài ra bạn cũng có thể thêm một tham số thứ tư vào đầu vị trí để sử dụng như một tên dịnh danh đăng ký cho server. Một Lưu ý nữa là trong bản trước đó của kitty server, chúng ta chỉ đơn giản trả về một đinh danh tiến trình ( pid ), tuy nhiên với gen_server, chúng ta phải trả về dưới dạng {ok, Pid}. The first parameter is the callback module, the second one is the list of parameters to pass to init/1 and the third one is about debugging options that won't be covered right now. You could add a fourth parameter in the first position, which would be the name to register the server with. Note that while the previous version of the function simply returned a pid, this one instead returns {ok, Pid}.

Các hàm tiếp theo chúng ta sẽ xét đến là: Next functions now:

%% Synchronous call
order_cat(Pid, Name, Color, Description) ->
   gen_server:call(Pid, {order, Name, Color, Description}).

%% This call is asynchronous
return_cat(Pid, Cat = #cat{}) ->
    gen_server:cast(Pid, {return, Cat}).

%% Synchronous call
close_shop(Pid) ->
    gen_server:call(Pid, terminate).

Lưu ý là đối với tham số thứ ba trong gen_server:call/2-3 được sử dụng cho việc xác định thời gian chờ trước khi có tín hiệu phản hồi, trong trường hợp bạn không thiết lập thời gian ( hoặc truyền vào đó một atom infinity), nó sẽ được mặc là thời gian là 5 giây. . Nếu sau thời gian chờ mà không có bất kỳ một phản hồi nào lại, lời gọi sẽ tụ động chấm dứt bằng cách đưa ra một crash. All of these calls are a one-to-one change. Note that a third parameter can be passed to gen_server:call/2-3 to give a timeout. If you don't give a timeout to the function (or the atom infinity), the default is set to 5 seconds. If no reply is received before time is up, the call crashes.

Bây giờ chúng ta đã có thể thêm các hàm callback trong gen_server rồi, dựa trên cấy trúc bảng sau sẽ đưa ra mối quan hệ giữa các lời gọi hàm và hàm callback trong gen_server: Now we'll be able to add the gen_server callbacks. The following table shows the relationship we have between calls and callbacks:

gen_server YourModule
start/3-4 init/1
start_link/3-4 init/1
call/2-3 handle_call/3
cast/2 handle_cast/2

Ngoải ra vẫn còn một số hàm callback nữa, chúng sẽ được sử dụng cho các trường hợp đặc biêt: And then you have the other callbacks, those that are more about special cases:

Nào, hãy bắt đầu điều chỉnh lại các đoạn mã trong module để phù hợp với mô hình: init/1, handle_call/3handle_cast/2 Let's begin by changing those we already have to fit the model: init/1, handle_call/3 and handle_cast/2.

%%% Server functions
init([]) -> {ok, []}. %% no treatment of info here!

handle_call({order, Name, Color, Description}, _From, Cats) ->
    if Cats =:= [] ->
        {reply, make_cat(Name, Color, Description), Cats};
       Cats =/= [] ->
        {reply, hd(Cats), tl(Cats)}
    end;
handle_call(terminate, _From, Cats) ->
    {stop, normal, ok, Cats}.

handle_cast({return, Cat = #cat{}}, Cats) ->
    {noreply, [Cat|Cats]}.

Một lần nữa, không có nhiều thay đổi ở đây, may mắn hơn là đoạn mã giờ trông ngắn gọn hơn nhờ vào sự trừu tượng thông minh. Bây giờ chúng ta sẽ xét một số hàm callback mới, hàm đàu tiên chúng ta xét đó là hàm handle_info/2. Trong module kitty server này, chức năng tương đối nhỏ và mục đích của chúng ta chỉ để mình họa do đó sẽ không sử dụng các hệ thống ghi log mà thay vào đó chỉ đơn giản xuất ra màn hình các dòng thông báo tin nhắc không biết trước là đủ. Again, very little has changed there. In fact, the code is now shorter, thanks to smarter abstractions. Now we get to the new callbacks. The first one is handle_info/2. Given this is a toy module and we have no logging system pre-defined, just outputting the unexpected messages will be enough:

handle_info(Msg, Cats) ->
    io:format("Unexpected message: ~p~n",[Msg]),
    {noreply, Cats}.

Hàm tiếp theo chúng ta sẽ xét đó là terminate/2, cơ bản không có gì thay đổi nhiều so vơi hàm riêng terminate/1 mà chúng ta đã có từ trước đó: The next one is the terminate/2 callback. It will be very similar to the terminate/1 private function we had:

terminate(normal, Cats) ->
    [io:format("~p was set free.~n",[C#cat.name]) || C <- Cats],
    ok.

Hàm cuối cùng chúng ta xét đó là code_change/3: And then the last callback, code_change/3:

code_change(_OldVsn, State, _Extra) ->
    %% No change planned. The function is there for the behaviour,
    %% but will not be used. Only a version on the next
    {ok, State}. 

Hãy nhớ đừng công khai hàm make_cat/3 để sử dụng bên ngoài module: Just remember to keep in the make_cat/3 private function:

%%% Private functions
make_cat(Name, Col, Desc) ->
    #cat{name=Name, color=Col, description=Desc}.

Tât cả định nghĩa cho module này đã xong, giờ chúng ta có thể kiểm tra nó: And we can now try the brand new code:

1> c(kitty_gen_server).
{ok,kitty_gen_server}
2> rr(kitty_gen_server).
[cat]
3> {ok, Pid} = kitty_gen_server:start_link().
{ok,<0.253.0>}
4> Pid ! <<"Test handle_info">>.
Unexpected message: <<"Test handle_info">>
<<"Test handle_info">>
5> Cat = kitty_gen_server:order_cat(Pid, "Cat Stevens", white, "not actually a cat").
#cat{name = "Cat Stevens",color = white,
     description = "not actually a cat"}
6> kitty_gen_server:return_cat(Pid, Cat).
ok
7> kitty_gen_server:order_cat(Pid, "Kitten Mittens", black, "look at them little paws!").
#cat{name = "Cat Stevens",color = white,
     description = "not actually a cat"}
8> kitty_gen_server:order_cat(Pid, "Kitten Mittens", black, "look at them little paws!").
#cat{name = "Kitten Mittens",color = black,
     description = "look at them little paws!"}
9> kitty_gen_server:return_cat(Pid, Cat).
ok       
10> kitty_gen_server:close_shop(Pid).
"Cat Stevens" was set free.
ok
pair of wool mittens

Oh , tuyệt vời nó hoạt động rất chính xác! Oh and hot damn, it works!

Vậy tổng kết lại chúng ta đã học được bài học gì từ hanh trình này? Có lẽ nó giống với lúc trước đó là sự phân tách từ các phần cụ thể ra thành phần chung là một ý tưởng tuyệt vời. Việc làm này sẽ giúp cho công việc bảo trì các đoạn mã, ứng dụng trở lên đỡ phức tạp, đơn giản và an toàn hơn, nó cũng giúp giảm lỗi gặp phải trong quá trình kiểm tra. Kể cả trong trường vẫn còn lỗi thì việc sửa chúng cũng dễ dàng hơn. Mặc dù các server chung chỉ là một trong nhưng mâu trừu tượng có sẵn nhưng chúng là một trong các mẫu được sử dụng nhiều nhất, và chúng ta sẽ thường xuyên bắt gặp các mẫy trừ tượng này và hành vi của chúng trong các chương tiếp theo. So what can we say about this generic adventure? Probably the same generic stuff as before: separating the generic from the specific is a great idea on every point. Maintenance is simpler, complexity is reduced, the code is safer, easier to test and less prone to bugs. If there are bugs, they are easier to fix. Generic servers are only one of the many available abstractions, but they're certainly one of the most used ones. We'll see more of these abstractions and behaviours in the next chapters.