What is OTP?
It's The Open Telecom Platform!

OTP, viết tắt của cụm tư Open Telecom Platform, khi đọc tới cái tên này bạn có thể nghĩ ngay tới viễn thông nhưng có thể là trước đây còn giờ nó không còn liên quan nhiều tới viễn thông nữa ( Thay vào đó giờ nó như một nền tảng để viết các ứng dụng phần mềm cho viên thông ). Nếu như một nửa thành phần quan trọng tạo lên Erlang tới từ concurrency và phân tán, nửa thứ hai tới từ khả năng xử lí lỗi, thì nửa còn lại chính là OTP framework. OTP stands for Open Telecom Platform, although it's not that much about telecom anymore (it's more about software that has the property of telecom applications, but yeah.) If half of Erlang's greatness comes from its concurrency and distribution and the other half comes from its error handling capabilities, then the OTP framework is the third half of it.
Trong các chương trước, chúng ta đã làm quen với một số ví dụ phô biến cũng như cách viết các ứng dụng concurrent với sự trợ giúp của các tiện ích tích hợp sẵn trong ngôn ngữ rồi: liên kết ( link ), màn hình giám sát ( monitor ), servers, timeout, trapping exít, etc. Trong đó chúng ta có một số nguyên tắc 'gotchas' theo thứ tự cần phải thực hiện, như cách để tránh race condition hoặc đừng quên rằng rằng một tiến trình có thể chấm dứt ở bất kỳ thời điểm nào. Chúng ta cũng thấy các trường hợp về hot code loading, đặt tên cho các tiến trình và supervisors. During the previous chapters, we've seen a few examples of common practices on how to write concurrent applications with the languages' built-in facilities: links, monitors, servers, timeouts, trapping exits, etc. There were a few 'gotchas' here and there on the order things need to be done, on how to avoid race conditions or to always remember that a process could die at any time. There was also hot code loading, naming processes and adding supervisors, to name a few.
Nếu bạn làm tất cả những thứ vừa liệt kê trên theo phương pháp thủ công thì chắc chắc bạn sẽ phải mất rất nhiều thời gian và không tránh phải một số sai xót khi thực hiện. Hơn nữa để ghi nhớ tất cả không phải là dễ dàng, sẽ có nhiều trường hợp bạn quên mất và làm bạn dễ dàng gặp lỗi. Do đó OTP framework được tạo ra để giải quyết vấn đề này. Nó sẽ nhóm các nguyên tắc cần thiết thành một tập các thư viện, các thư viện này được thiết kế một cách cẩn thận và trải qua nhiều năm rèn luyện. Vì vậy bất cứ lập trình viên nào sử dụng Erlang đều lên sử dụng chúng. Doing all of this manually is time consuming and sometimes prone to error. There are corner cases to be forgotten about and pits to fall into. The OTP framework takes care of this by grouping these essential practices into a set of libraries that have been carefully engineered and battle-hardened over years. Every Erlang programmer should use them.
Ngoài ra OTP framework cũng là một tập hợp các module và các tiêu chuânt được thiết kế để giúp bạn trong việc xây dụng một ứng dụng dễ dàng hơn. Với phần lớn các lạp trình viên Erlang sử dụng OPT framework, phần lớn các ứng dụng bạn gặp trong tự nhiên sẽ có xu hướng tuân theo chuẩn sau: The OTP framework is also a set of modules and standards designed to help you build applications. Given most Erlang programmers end up using OTP, most Erlang applications you'll encounter in the wild will tend to follow these standards.
The Common Process, Abstracted
Một trong những thứ mà chúng ta làm rát nhiều lần trong các ví dụ trước đó là chia các công việc ra thành từng phần cụ thể. trong hầu hết các trường hợp chúng ta có một hàm phụ trách việc sinh ( spawn ) mới một tiến trình, một hàm phụ trách việc khởi tạo giá trị khi sinh và một hàm lặp ( loop ), etc. One of the things we've done many times in the previous process examples is divide everything in accordance to very specific tasks. In most processes, we had a function in charge of spawning the new process, a function in charge of giving it its initial values, a main loop, etc.
Các phần đó, có thể thấy chúng thường được xuất hiện trong bất cữ các chương trình concurrent mà bạn viết, bất kể là tiến trình dó được sử dụng nhằm mục đích nào đó. These parts, as it turns out, are usually present in all concurrent programs you'll write, no matter what the process might be used for.

Các kỹ sự và các nhà khoa học máy tính xây dựng lên OTP framework đã xác định các mẫu này và đặt chúng vào trong một số thư viện phổ biến. Các thư viện này được xây dưng cùng với các đoạn mã tương tự các khác niệm trừu tượng mà chúng ta đã dùng ( vd như đanh dấu các tin nhắc qua references ). cùng với đó là lợi thế trong nhiều năm sử dụng trong các lĩnh vực và được xây dựng một cách thận trọng hơn nhiều những gì mà chúng ta đã thực hiện với các nguyên tắc trước cho một ứng dụng ổn định. Thư này bao gồm các hàm khởi tạo và sinh ( spawn ) các tiến trình an toàn hơn, quá trình gửi các tin nhắn bao gồm việc phục hồi lại lỗi các tin nhắc và nhiều tính năng hơn nữa. Một điều hài hước ở đây là mặc dù nó cung cấp rất nhiều tính năng kể trên nhưng trái lại bạn hiếm khi phải tự mình sử dụng các thư viện này. Các khái niệm trừu tượng của chúng rất đơn giản và phổ thông tới mức mà có rất nhiều điều thú vị để xây dựng dựa trên chúng. Đó các các thư viện như hình mình họa mà chúng ta sẽ sử dụng: The engineers and computer scientists behind the OTP framework spotted these patterns and included them in a bunch of common libraries. These libraries are built with code that is equivalent to most of the abstractions we used (like using references to tag messages), with the advantage of being used for years in the field and also being built with far more caution than we were with our implementations. They contain functions to safely spawn and initialize processes, send messages to them in a fault-tolerant manner and many other things. Funnily enough, you should rarely need to use these libraries yourself. The abstractions they contain are so basic and universal that a lot more interesting things were built on top of them. Those libraries are the ones we'll use.

Trong các chương sau chúng ta sẽ gặp một mẫu sử dụng tương đối phổ biến của các tiến trình và sau đó xem cách chúng được khái quát hóa, đối với mỗi mẫu chúng ta sẽ xem xét cách thực hiện từng trường hợp đó ứng với các hành vi trong OTP framework và cách sử dụng các hành vi này như thế nào. In the following chapters we'll see a few of the common uses of processes and then how they can be abstracted, then made generic. Then for each of these we'll also see the corresponding implementation with the OTP framework's behaviours and how to use each of them.
The Basic Server
Với mẫu đầu tiên mà tôi sẽ mô tả này, sẽ không có gì ngạc nhiên nếu bạn cảm thấy nó quen thuộc bởi vì đây là mẫu mà chúng ta đã thực hiện ở chương trước. Ở chương trước, chúng ta đã phân tích và viết một event server, khi viết chúng ta đã vân dụng một mô hình gọi là client-server. Trong mô hình này server sẽ nhận được các lời gọi từ client và xử lí, phản hồi lại lời gọi đó nếu nằm trong phạm vi của giao thức mà được yêu cầu để làm vậy. Mẫu đầu tiên mà tối sẽ mô tả ở đây là mẫu mà chúng ta đã làm ở chương trước The first common pattern I'll describe is one we've already used. When writing the event server, we had what could be called a client-server model. The event server would receive calls from the client, act on them and then reply to it if the protocol said to do so.
Trong chương này, chúng sẽ sử dụng một server đơn giản hơn cho phép chúng ta tập trung vào các thuộc tính cơ bản của nó. Chúng ta sẽ tạo ra một file module gọi tên là kitty_server và gõ các đoạn mã dưới đây: For this chapter, we'll use a very simple server, allowing us to focus on the essential properties of it. Here's the kitty_server:
%%%%% Naive version -module(kitty_server). -export([start_link/0, order_cat/4, return_cat/2, close_shop/1]). -record(cat, {name, color=green, description}). %%% Client API start_link() -> spawn_link(fun init/0). %% Synchronous call order_cat(Pid, Name, Color, Description) -> Ref = erlang:monitor(process, Pid), Pid ! {self(), Ref, {order, Name, Color, Description}}, receive {Ref, Cat} -> erlang:demonitor(Ref, [flush]), Cat; {'DOWN', Ref, process, Pid, Reason} -> erlang:error(Reason) after 5000 -> erlang:error(timeout) end. %% This call is asynchronous return_cat(Pid, Cat = #cat{}) -> Pid ! {return, Cat}, ok. %% Synchronous call close_shop(Pid) -> Ref = erlang:monitor(process, Pid), Pid ! {self(), Ref, terminate}, receive {Ref, ok} -> erlang:demonitor(Ref, [flush]), ok; {'DOWN', Ref, process, Pid, Reason} -> erlang:error(Reason) after 5000 -> erlang:error(timeout) end. %%% Server functions init() -> loop([]). loop(Cats) -> receive {Pid, Ref, {order, Name, Color, Description}} -> if Cats =:= [] -> Pid ! {Ref, make_cat(Name, Color, Description)}, loop(Cats); Cats =/= [] -> % got to empty the stock Pid ! {Ref, hd(Cats)}, loop(tl(Cats)) end; {return, Cat = #cat{}} -> loop([Cat|Cats]); {Pid, Ref, terminate} -> Pid ! {Ref, ok}, terminate(Cats); Unknown -> %% do some logging here too io:format("Unknown message: ~p~n", [Unknown]), loop(Cats) end. %%% Private functions make_cat(Name, Col, Desc) -> #cat{name=Name, color=Col, description=Desc}. terminate(Cats) -> [io:format("~p was set free.~n",[C#cat.name]) || C <- Cats], ok.
Vâng đó là server cửa hàng mèo ( kitty server/store ). Để mô tả hành vi hoạt động của cửa hàng này khá đơn giản: bạn nhận được một chú mèo với hình dáng ra sao. Nếu chú mèo được trả về, nó sẽ tự động thêm vào danh sách và chuyển tới thứ tự tiếp theo thay vì phải đợi client đưa ra yêu cầu ( của hàng mèo này phục vụ vì tiền chứ không phải vì tiếng cười ) So this is a kitty server/store. The behavior is extremely simple: you describe a cat and you get that cat. If someone returns a cat, it's added to a list and is then automatically sent as the next order instead of what the client actually asked for (we're in this kitty store for the money, not smiles):
1> c(kitty_server). {ok,kitty_server} 2> rr(kitty_server). [cat] 3> Pid = kitty_server:start_link(). <0.57.0> 4> Cat1 = kitty_server:order_cat(Pid, carl, brown, "loves to burn bridges"). #cat{name = carl,color = brown, description = "loves to burn bridges"} 5> kitty_server:return_cat(Pid, Cat1). ok 6> kitty_server:order_cat(Pid, jimmy, orange, "cuddly"). #cat{name = carl,color = brown, description = "loves to burn bridges"} 7> kitty_server:order_cat(Pid, jimmy, orange, "cuddly"). #cat{name = jimmy,color = orange,description = "cuddly"} 8> kitty_server:return_cat(Pid, Cat1). ok 9> kitty_server:close_shop(Pid). carl was set free. ok 10> kitty_server:close_shop(Pid). ** exception error: no such process or port in function kitty_server:close_shop/1
Quay trở lại mã nguồn của module, Không khó gì để bạn có thể thấy các mẫu mà chúng ta đã sử dụng trước đó. Trong đoạn mã này bạn thấy nơi mà chúng ta thiết lâp một màn hình giám sát ( monitor ) được hoạt động hay ngừng, sử dụng bộ tính thời gian, nhận dữ liệu, sử dụng vòng lặp chính ( main loop ), hàm khởi tạo ( init function ), etc. Chúng không có gì lạ nhẫm cả. Mọi thứ đều giống như trước đó chúng ta đã làm cả, vì thế chúng ta dó thể khái quát hóa lại những thứ mà chúng ta luôn lặp lại. Looking back at the source code for the module, we can see patterns we've previously applied. The sections where we set monitors up and down, apply timers, receive data, use a main loop, handle the init function, etc. should all be familiar. It should be possible to abstract away these things we end up repeating all the time.
Tiếp theo, hãy nhin vào phía Client API, điều đầu tiên gây chú ý tới chúng ta đó là cả hai lời gọi đồng bô ( synchronous calls ) thực sự rất giống nhau. Những cuộc gọi này có khả năng xuất hiện trong các thư viện trừu tượng mà chúng ta đã nói tới trong phần trước rồi. Và giờ, chúng ta sẽ chỉ khái quát hóa chúng với một hàm duy nhất trong một module mới gọi tên là my_server.erl, nó sẽ định nghĩa tất cả các thành phần chung của kitty server. Let's first take a look at the client API. The first thing we can notice is that both synchronous calls are extremely similar. These are the calls that would likely go in abstraction libraries as mentioned in the previous section. For now, we'll just abstract these away as a single function in a new module which will hold all the generic parts of the kitty server:
-module(my_server). -compile(export_all). call(Pid, Msg) -> Ref = erlang:monitor(process, Pid), Pid ! {self(), Ref, Msg}, receive {Ref, Reply} -> erlang:demonitor(Ref, [flush]), Reply; {'DOWN', Ref, process, Pid, Reason} -> erlang:error(Reason) after 5000 -> erlang:error(timeout) end.
Hàm này sẽ có nhiệm vụ nhận một tin nhắn và một định danh ( PID ), sao đó thực hiện chuyển tiếp tin nhắn cho bạn một cách an toàn.
Từ giờ trở đi, chúng ta có thể chỉ cần thay thế các tin nhắn, thông báo gửi đi bằng cách gọi hàm này.
Tiếp theo để kết hợp module kitty server với các hàm khái quát trong module my_server
, chúng ta sẽ làm như sau:
This takes a message and a PID, sticks them into in the function, then forwards the message for you in a safe manner. From now on,
we can just substitute the message sending we do with a call to this function.
So if we were to rewrite a new kitty server to be paired with the abstracted my_server
, it could begin like this:
-module(kitty_server2). -export([start_link/0, order_cat/4, return_cat/2, close_shop/1]). -record(cat, {name, color=green, description}). %%% Client API start_link() -> spawn_link(fun init/0). %% Synchronous call order_cat(Pid, Name, Color, Description) -> my_server:call(Pid, {order, Name, Color, Description}). %% This call is asynchronous return_cat(Pid, Cat = #cat{}) -> Pid ! {return, Cat}, ok. %% Synchronous call close_shop(Pid) -> my_server:call(Pid, terminate).
Đoạn mã tiếp theo mà chúng ta xét sẽ không đơn giản như hàm call/2
. Lưu ý là tất cả tiến trình được tạo ra mà chúng ta viết trước đó
đều bắt đầu với một hàm lăp ( loop function ) và các mẫu tin nhắn bên trong chúng sử dụng để khớp với từng trường hợp. Mặc dù cách làm như vậy không
có vấn đề gì nhưng sẽ có một chút rắc rối khi đọc với một lượng lớn các mẫu tin nhắn, do đó chúng ta sẽ thử thay đổi một chút, tuy hơi mang tính nhạy cảm.
Chúng ta sẽ tách các đoạn mẫu tin nhắc trong hàm lặp này ra. Cách nhanh nhất để thực hiện điều đó là:
The next big generic chunk of code we have is not as obvious as the call/2
function.
Note that every process we've written so far has a loop where all the messages are pattern matched.
This is a bit of a touchy part, but here we have to separate the pattern matching from the loop itself. One quick way to do it would be to add:
loop(Module, State) -> receive Message -> Module:handle(Message, State) end.
Và hàm xử lí module trông sẽ như thế này: And then the specific module can look like this:
handle(Message1, State) -> NewState1; handle(Message2, State) -> NewState2; ... handle(MessageN, State) -> NewStateN.
Nhìn tốt hơn phải không ? Ngoài ra còn vẫn có một số cách để làm cho đoạn mã này trỏ lên rõ nghĩa hơn nữa. Khi đọc module kitty_server
nếu bạn để ý
đọc kỹ chúng (Tôi hi vọng là bạn đã làm được), bạn sẽ thấy chúng ta có một cách khá cụ thể để gọi lời gọi đồng bộ và lời gọi bất đồng bộ.
Sẽ tuyệt vời hơn nếu chúng ta thực hiện một chức năng chung cho module server để cung cấp một cách phân biệt rà ràng với hai loại này.
This is better. There are still ways to make it even cleaner. If you paid attention when reading the kitty_server
module (and I hope you did!), '
you will have noticed we have a specific way to call synchronously and another one to call asynchronously.
It would be pretty helpful if our generic server implementation could provide a clear way to know which kind of call is which.
Để làm điều đó, chúng ta sẽ cần khớp với các loại tin nhắc khác nhau trong hàm my_server:loop/2
, Và cũng sẽ cần phải thay đổi hàm call/2
một chút
đối với lời gọi đồng bộ bằng cách thêm atom sync
vào trong tin nhắn ở trong thứ hai của hàm để xác định rõ lời gọi đồng bộ.
In order to do this, we will need to match different kinds of messages in my_server:loop/2
.
This means we'll need to change the call/2
function a little bit so synchronous calls are made obvious by adding the atom
sync
to the message on the function's second line:
call(Pid, Msg) -> Ref = erlang:monitor(process, Pid), Pid ! {sync, self(), Ref, Msg}, receive {Ref, Reply} -> erlang:demonitor(Ref, [flush]), Reply; {'DOWN', Ref, process, Pid, Reason} -> erlang:error(Reason) after 5000 -> erlang:error(timeout) end.
tiếp đó để xử lí các lời gọi bất đồng bộ chúng ta cần định nghĩa thêm một hàm mới cho việc này,
và nó sẽ thực hiện bởi hàm cast/2
We can now provide a new function for asynchronous calls. The function cast/2
will handle this:
cast(Pid, Msg) -> Pid ! {async, Msg}, ok.
Cuối cùng hàm lặp của chúng ta sẽ có dạng: sau With this done, the loop can now look like this:
loop(Module, State) -> receive {async, Msg} -> loop(Module, Module:handle_cast(Msg, State)); {sync, Pid, Ref, Msg} -> loop(Module, Module:handle_call(Msg, Pid, Ref, State)) end.

Sau đó bạn cũng có thể thêm rất nhiều các mẫu khác để xủ lí các tin nhắn mà không phù hợp với khái niệm sync/async như hai mãu đồng bộ, bất đồng bộ trên hoặc thêm một chức năng cho việc gỡ lõi hoặc một số khác như hot code reloading. And then you could also add specific slots to handle messages that don't fit the sync/async concept (maybe they were sent by accident) or to have your debug functions and other stuff like hot code reloading in there.
tuy nhiên ở đoạn mã trong hàm lặp trên có một điều gây thất vọng đó là abstraction is leaking.
Các lập trình viện trong trường hợp nếu sử dụng module my_server
để gửi các tin nhắc bất đồng bộ và nhận phản hồi sẽ cần phải biết thôn tin về
định danh tham chiếu. Việc làm này khiến cho mô hình khái quát hoá ko có nghĩa.
Dưới đây là cách chúng tôi có thể khắc phục nhanh chóng:
One disappointing thing with the loop above is that the abstraction is leaking.
The programmers who will use my_server
will still need to know about references when sending synchronous messages and replying to them.
That makes the abstraction useless. To use it, you still need to understand all the boring details. Here's a quick fix for it:
loop(Module, State) -> receive {async, Msg} -> loop(Module, Module:handle_cast(Msg, State)); {sync, Pid, Ref, Msg} -> loop(Module, Module:handle_call(Msg, {Pid, Ref}, State)) end.
Bằng cách đặt hay biến Pid, Ref vào trong một bộ, chúng ta có thể chỉ cần truyền chúng như một đối số tới một hàm khác thông qua một biến giả định như biến From Thông qua biến này, người dùng không cần phải biết bất cứ điều gì bên trong biên này. Thay vào đó chúng ta chỉ càn cung cấp một hàm để gưi phản hồi dựa trên nội dung của biến From By putting both variables Pid and Ref in a tuple, they can be passed as a single argument to the other function as a variable with a name like From. Then the user doesn't have to know anything about the variable's innards. Instead, we'll provide a function to send replies that should understand what From contains:
reply({Pid, Ref}, Reply) -> Pid ! {Ref, Reply}.
Việc còn lại giờ là chúng ta sẽ phải xác định cụ thể các hàm cho việc khởi tạo tiến trinh ( start
, start_link
và init
),
các hàm này sẽ được định nghĩa thông qua tên module và một số thứ khác. Một khi chúng được thêm vào module, nó sẽ có dạng như sau:
What is left to do is specify the starter functions (start
, start_link
and init
) that pass around the module names and whatnot.
Once they're added, the module should look like this:
-module(my_server). -export([start/2, start_link/2, call/2, cast/2, reply/2]). %%% Public API start(Module, InitialState) -> spawn(fun() -> init(Module, InitialState) end). start_link(Module, InitialState) -> spawn_link(fun() -> init(Module, InitialState) end). call(Pid, Msg) -> Ref = erlang:monitor(process, Pid), Pid ! {sync, self(), Ref, Msg}, receive {Ref, Reply} -> erlang:demonitor(Ref, [flush]), Reply; {'DOWN', Ref, process, Pid, Reason} -> erlang:error(Reason) after 5000 -> erlang:error(timeout) end. cast(Pid, Msg) -> Pid ! {async, Msg}, ok. reply({Pid, Ref}, Reply) -> Pid ! {Ref, Reply}. %%% Private stuff init(Module, InitialState) -> loop(Module, Module:init(InitialState)). loop(Module, State) -> receive {async, Msg} -> loop(Module, Module:handle_cast(Msg, State)); {sync, Pid, Ref, Msg} -> loop(Module, Module:handle_call(Msg, {Pid, Ref}, State)) end.
Tiếp theo chúng ta sẽ phải thay đổi lại module kitty server cho phù hợp với những hàm mà chúng ta đã viết phía trên.
hay tạo ra một module mới và đặt tên nó là kitty_server2
,
module mới này sẽ hoạt động như một module callback, tương tự các giao tiếp ( interface ) của nó sẽ liên kết hay sử dụng các giao tiếp mà chúng ta đã định nghĩa cho module
kitty_server2
này so với module kitty_server
ngoại trừ nội dung logic bên trong mỗi hàm giớ sẽ xử lí thông qua
module my_server
.
The next thing to do is reimplement the kitty server, now kitty_server2
as a callback module that will respect the interface we defined for my_server
. We'll keep the same interface as the previous implementation,
except all the calls are now redirected to go through my_server
:
-module(kitty_server2). -export([start_link/0, order_cat/4, return_cat/2, close_shop/1]). -export([init/1, handle_call/3, handle_cast/2]). -record(cat, {name, color=green, description}). %%% Client API start_link() -> my_server:start_link(?MODULE, []). %% Synchronous call order_cat(Pid, Name, Color, Description) -> my_server:call(Pid, {order, Name, Color, Description}). %% This call is asynchronous return_cat(Pid, Cat = #cat{}) -> my_server:cast(Pid, {return, Cat}). %% Synchronous call close_shop(Pid) -> my_server:call(Pid, terminate).
Lưu ý: tôi đã thêm thuộc tinh -export()
thứ hay ở đầu module ngày sau thuộc -export()
đầu tiên là bởi vì chúng ta sẽ cần chúng để có thể hoạt động
được với module my_server
:
Note that I added a second -export()
at the top of the module. Those are the functions my_server
will need to call to make everything work:
%%% Server functions init([]) -> []. %% no treatment of info here! handle_call({order, Name, Color, Description}, From, Cats) -> if Cats =:= [] -> my_server:reply(From, make_cat(Name, Color, Description)), Cats; Cats =/= [] -> my_server:reply(From, hd(Cats)), tl(Cats) end; handle_call(terminate, From, Cats) -> my_server:reply(From, ok), terminate(Cats). handle_cast({return, Cat = #cat{}}, Cats) -> [Cat|Cats].
Và tiếp đó để hoàn thành, chúng ta sẽ thêm lại các hàm riêng: And then what needs to be done is to re-add the private functions:
%%% Private functions make_cat(Name, Col, Desc) -> #cat{name=Name, color=Col, description=Desc}. terminate(Cats) -> [io:format("~p was set free.~n",[C#cat.name]) || C <- Cats], exit(normal).
Để chắc chắn server hoạt động, hãy thay thế kết quả trả về ok
trước đó trong hàm ok
bằng exit(normal)
.
Just make sure to replace the ok
we had before by exit(normal)
in terminate/1
, otherwise the server will keep going on.
Cuối cùng hay biên dịch đã mã mà chúng ta vừa viết và kiểm tra nó, không có gì ngạc nhiên khi nó sẽ thực hiện chính xác giống với nhưng gì trước đó chúng ta đã làm, Tuy nhiên chúng ta sẽ xem xét những thay đỏi so với trước. The code should be compilable and testable, and run in exactly the same manner as it was before. The code is quite similar, but let's see what changed.
Specific Vs. Generic
Những gì chúng ta làm ở trên là một cách để hiểu được bản chất, cốt lõi của OTP ( như một khái niệm ). Đây là những gì mà OTP mang lại: bằng cách đưa ra các thành phần phổ biến, trích xuất chúng và đưa chúng vào trong các thư viện, đảm bảo chúng hoạt động chính xác và tái sử dụng các đoạn mã đó ( nếu có thể ). Chúng ta sẽ không phải bận tâm tới các vấn đề đó nữa mà thay vào đó tập trung vào các công việc cụ thể hơn, những thứ luôn thay đổi từ ứng dụng này sang ứng dụng khác. What we've just done is get an understanding the core of OTP (conceptually speaking). This is what OTP really is all about: taking all the generic components, extracting them in libraries, making sure they work well and then reusing that code when possible. Then all that's left to do is focus on the specific stuff, things that will always change from application to application.
Rõ ràng, không có quá ý nghĩa ở đây khi chỉ áp dụng chúng với kitty server. Nó giống như việc chúng ta đang làm ở đây là khái quát hóa dựa trên những gì đã được khái quát trước dó rồi. Nếu ứng dụng của chúng ta cần phải gửi cho khác hàng, thì với phiên bản kitty server đầu chắc chắn sẽ hoạt động tốt, không có bất kỳ vấn đề gì. Obviously, there isn't much to save by doing things that way with only the kitty server. It looks a bit like abstraction for abstraction's sake. Trong trường hợp với một ứng dụng lớn hơn , thì bạn có thể tách các phần chung mã chung từ các phần cụ thể . If the app we had to ship to a customer were nothing but the kitty server, then the first version might be fine. If you're going to have larger applications then it might be worth it to separate generic parts of your code from the specific sections.
Hãy tưởng tượng rằng hiện tại chúng ta đang có một ứng Erlang đang chạy trên server. Ứng dụng này đang chạy cùng trên một số kitty server, cùng với đó là một tiến trình gọi là thú ý ( bạn sẽ gửi các chú mèo bị thương tới và nó sẽ phản hồi lại cho bạn về chú mèo đó kèm với việc đã trị cho chúng ), một salon chăm sóc sắc đẹp cho mèo, một server thức ăn cho động vật, các trang thiết thiết, đồ dụng , etc. Hầu hết tất cả những thứ trên đều có thể triển khai thông qua một mẫu client-server. THeo thơi gian, hệ thống của bạn dần trở lên phức tập và gần như có nhiều các loại server khác nhau đang chạy. Let's imagine for a moment that we have some Erlang software running on a server. Our software has a few kitty servers running, a veterinary process (you send your broken kitties and it returns them fixed), a kitty beauty salon, a server for pet food, supplies, etc. Most of these can be implemented with a client-server pattern. As time goes, your complex system becomes full of different servers running around.
Thêm càng nhiều server sẽ khiến cho các đoạn mã càng trở lên phức tạp , đồng thời quá trình kiểm tra, bảo trì và hiểu được kiến trúc của hệ thống cũng càng phức tạp khó khăn hơn.
mỗi lần triển khai có thể khác nhau, có thể được lập trình theo nhiều cách khác nhau bởi nhiều ngừoi, etc. Tuy nhiên nếu tất cả các server đó cùng tuân theo
một mẫu, mô hình chung, một mẫu được khái quát hóa như trong module my_server
, thì chúng ta sẽ giảm được rất nhiều tính phức tạp trước đó đi.
nhìn vào đó, ngay lập tức bạn hiểu khái niệm cơ bản của module ( oh, nó chỉ là một server mà thôi!), việc triển khai để kiểm tra, tài liệu, etc cũng trở lên dễ dàng
hơn. Phần còn lại của nỗ lực có thể được áp dụng cho các chi tiết cụ thể của từng triển khai cụ thể.
Adding servers adds complexity in terms of code, but also in terms of testing, maintenance and understanding. Each implementation might be different,
programmed in different styles by different people, and so on. However, if all these servers share the same common my_server
abstraction,
you substantially reduce that complexity. You understand the basic concept of the module instantly ("oh, it's a server!"), there's a single generic
implementation of it to test, document, etc. The rest of the effort can be put on each specific implementation of it.

Việc này sẽ giúp bạn giảm được rất nhiều thời gian cho việc dà soát và giải quyết các lỗi phát sinh ( chỉ cần sửa, làm ở một nơi cho tất cả các server ).
Đồng thời bạn cũng có thể giảm được dáng kể lỗi trong chương trình. Nếu mỗi lần bạn viết lại hàm my_server:call/3
hay hàm lặp chính, nó sẽ không chí
tiêu tốn thời gian của bạ mà còn khiến bạn bỏ sót bước này hay bước khác và góp phần gây ra nhiều lỗi khác hơn. ít lỗi hơn cũng tức là ít có những cuộc gọi vào
ban đêm chỉ để sửa những lỗi này, điều đó chắc chắn là rất tốt cho bạn, cho tôi và tất cả mọi người. Có thể nhà bạn gần công tuy, nhưng tôi cá là
bạn không hề muốn tới công ty vào ngày nghỉ chỉ để sửa lỗi.
This means you reduce a lot of time tracking and solving bugs (just do it at one place for all servers). It also means that you reduce the number of bugs you introduce.
If you were to re-write the my_server:call/3
or the process' main loop all the time, not only would it be more time consuming,
but chances of forgetting one step or the other would skyrocket, and so would bugs. Fewer bugs mean fewer calls during the night to go fix something,
which is definitely good for all of us. Your mileage may vary, but I'll bet you don't appreciate going to the office on days off to fix bugs either.
Một điều thú vị khác mà chúng ta đã làm trước đó khi phân tách thành phần chung từ các thành phần cụ thể đó là đơn giản hóa việc thử nghiệm các module riêng lẻ. . Nếu bạn muốn thực hiện một unit test trên kitty server cũ, bạn sẽ cần sinh ra ( spawn ) mỗi tiến trình riêng cho mỗi kiểm tra, truyền vào đó một trạng thái đúng và gửi các đoạn tin nhắn của bạn tới với hi vọng là kết quả phản hồi đúng với những gì bạn mong đợi. Nói một cách khác, kitty server thứ hai của chúng ta chỉ cần yêu cầu gọi tới hàm 'handle_call/3' và 'handle_cast/2' và nhìn nhật kết quả đưa ra như một trạng thái mới. Không yêu cầu chúng ta phải thiết lập các server, thao tác với trạng thái, . Mà thay vào đó chỉ cần sử dụng trạng thái như một tham số truyền vào hàm thôi. Lưu ý là việc này giúp bạn dễ dàng kiểm tra các phương diện phổ biến của server, bạn chỉ cần thực hiện một số hàm rất đơn giản cho phép bạn tập trung vào các hành vi mà bạn muốn quan sát, không cần bận tâm tới các hành vi khác. Another interesting thing about what we did when separating the generic from the specific is that we instantly made it much easier to test our individual modules. If you wanted to unit test the old kitty server implementation, you'd need to spawn one process per test, give it the right state, send your messages and hope for the reply you expected. On the other hand, our second kitty server only requires us to run the function calls over the 'handle_call/3' and 'handle_cast/2' functions and see what they output as a new state. No need to set up servers, manipulate the state. Just pass it in as a function parameter. Note that this also means the generic aspect of the server is much easier to test given you can just implement very simple functions that do nothing else than let you focus on the behaviour you want to observe, without the rest.
Bằng cách sử dụng những lợi ích 'vô hình' của việc sử dụng trừu tượng phổ biến, nếu bất kỳ người nào sử dụng chính xác cùng một backend cho tiến trình của họ, thì khi họ tối ưu hóa hệ thống backend cá nhân này khiến cho nó nhanh hơn, thì mọi tiến trình sử dụng nó cũng sẽ được tăng hiệu suất. Đối với nguyên tắc này để làm việc trong thực tế , nhièu người lên cố gắng sử dụng cùng một mẫu trừu tượng. May mắn thay trong cộng đồng Erlang, OTP framework thực hiện chính xác nhưng điều như vậy. A much more 'hidden' advantage of using common abstractions in that way is that if everyone uses the exact same backend for their processes, when someone optimizes that single backend to make it a little bit faster, every process using it out there will run a little bit faster too. For this principle to work in practice, it's usually necessary to have a whole lot of people using the same abstractions and putting effort on them. Luckily for the Erlang community, that's what happens with the OTP framework.
Quay trở lại với module của chúng ta, có một số vấn đề mà chúng ta vẫn chưa thực sự nhắc tới: như named processes, cấu hình thời chờ ( timeout ), thêm thông tin cho việc gỡ lỗi,
cách giải quyết các trường hợp tin nhắc không mong muốn xảy ra, cách tích hợp với hot code loading, cách xử lí các lỗi đặc biệt, cách viết các đọa mã khái quát hóa đối với
hầu hết các tin nhắn phản hồi, cách xử lí khi server ngừng hoạt động ( shutdown ), đảm bảo việc tích hợp server cùng với supervisor hoạt động tốt, etc. Tất cả các vấn đề liệt
kê trên không cần thiết trong chương này, nhưng nó vẫn rất quan trọng trong dự án thực tế . Một lần nữa, bạn có thể thấy tại sao làm tất cả những vấn đề trên một mình là một việc
làm khá nguy hiểm. May mắn cho bạn ( và những người hỗ trỡ ứng dụng của bạn), đội ngũ phát triển Erlang/OTP sẽ xử lí tất cả những vấn đề trên cùng với mẫu hành vi 'gen_server'. gen_server
thoạt nhìn rất giống với nhưng gì chúng ta đã làm với module my_server
kết hợp với thuốc kích thích ( steroids ), và nó được đảm trong thực tiẽn qua nhiều năm kiểm tra và đươc sử dụng trong
rất nhiều các sản phẩm production.
Back to our modules. There are a bunch of things we haven't yet addressed: named processes, configuring the timeouts, adding debug information,
what to do with unexpected messages, how to tie in hot code loading, handling specific errors, abstracting away the need to write most replies, handling most ways to shut a server down,
making sure the server plays nice with supervisors, etc. Going over all of this is superfluous for this text, but would be necessary in real products that need to be shipped.
Again, you might see why doing all of this by yourself is a bit of a risky task. Luckily for you (and the people who'll support your applications), the Erlang/OTP team managed to handle
all of that for you with the gen_server behaviour. gen_server
is a bit like my_server
on steroids, except it has years and years of testing and production use behind it.