---
title: "مقدمة إلى OTP في Elixir"
description: "في هذا الجزء من سلسلتنا التمهيدية عن Elixir، نحوّل خادم FizzBuzz الذي بنيناه في المرة السابقة إلى تطبيق OTP."
canonical: https://oozou.com/ar/blog/an-intro-to-otp-in-elixir-20
locale: ar
date: 2015-02-13
author: "OOZOU Team"
---

# مقدمة إلى OTP في Elixir

> في هذا الجزء من سلسلتنا التمهيدية عن Elixir، نحوّل خادم FizzBuzz الذي بنيناه في المرة السابقة إلى تطبيق OTP.

Published: 2015-02-13 · Author: OOZOU Team (Engineering) · 6 min read

**ما هو OTP؟**

بحسب [التوثيق الرسمي](http://www.erlang.org/doc/)، فإن OTP، أي [منصة الاتصالات المفتوحة (Open Telecom Platform)](https://en.wikipedia.org/wiki/Open_Telecom_Platform)، هي "بيئة تطوير متكاملة للبرمجة المتزامنة". وتضم مترجمًا ومفسّرًا للغة Erlang، وخادم قاعدة بيانات ([Mnesia](http://en.wikipedia.org/wiki/Mnesia))، وأداة تحليل ([Dyalizer](http://www.erlang.org/doc/man/dialyzer.html))، إضافة إلى عدد كبير من المكتبات. وهذه المكتبات تحديدًا هي ما يقصده الناس عادة عندما يتحدثون عن OTP.

**السلوكيات (Behaviours)**

من أهم [مبادئ التصميم في Erlang/OTP](http://www.erlang.org/doc/design_principles/des_princ.html) أنماط التطبيقات، أو ما يُسمّى "behaviours" بلغة OTP. تحدد هذه الأنماط تطبيقات عامة للمهام الشائعة، مثل وحدة الخادم العام ([gen_server](http://elixir-lang.org/getting_started/mix_otp/3.html)). يضيف مطوّر التطبيق الكود الخاص به في وحدة callback تصدّر مجموعة محددة من الدوال. وعلى خلاف Erlang، توفر Elixir تطبيقات افتراضية لهذه الدوال، فلا تحتاجون إلا إلى إعادة تعريف ما يلزمكم منها.

**خادم FizzBuzz باستخدام GenServer**

يكفي حديثًا عن النظرية، لنكتب بعض الكود!

نستخدم [mix](http://elixir-lang.org/docs/stable/mix/) لإنشاء مشروع جديد. ولأننا نريد أن يكون اسم الوحدة بصيغة [CamelCase](https://en.wikipedia.org/wiki/CamelCase)، علينا تمرير الخيار module (كما في الأمر أدناه)، وإلا فسيكون الاسم الافتراضي Fizzbuzz.

```
$ mix new fizzbuzz --module FizzBuzz
lang-bash
```

ينشئ هذا الأمر قالب تطبيق كاملًا، لكننا في هذا المقال سنعمل على الملف lib/fizzbuzz.ex فقط.

لنبدأ بإخبار التطبيق بأننا نطبّق سلوك GenServer من OTP:

```
use GenServer
require Logger
```

استدعينا أيضًا [وحدة Logger](http://elixir-lang.org/docs/master/logger/Logger.html) لأننا نستخدمها لأغراض التتبع وتصحيح الأخطاء. بعد ذلك نعرّف واجهة API الخاصة بالعملاء.

```
def start_link do
   GenServer.start_link(__MODULE__, :ok, name: __MODULE__)
end

def get(n) do
  GenServer.call(__MODULE__, {:print, n})
end

def print(n) do
  GenServer.cast(__MODULE__, {:print, n})
end
```

الدالة FizzBuzz.start_link/0 مجرد غلاف حول [GenServer.start_link/3](http://elixir-lang.org/docs/stable/elixir/GenServer.html#start_link/3) التي تشغّل الخادم كعملية مرتبطة (linked process)، وغالبًا ما يكون ذلك ضمن [شجرة إشراف (supervision tree)](http://elixir-lang.org/docs/stable/elixir/Supervisor.html). لاحظوا أننا نشغّل الخادم مع الخيار name ونعطيه اسم الوحدة (__MODULE__). بهذه الطريقة لا تحتاج الدوال الأخرى إلى الإشارة إلى الخادم عبر PID.

الدالتان FizzBuzz.get/1 وFizzBuzz.print/1 هما الواجهة الرئيسية للخادم. تستقبل كل منهما رقمًا وتعيد قيمة FizzBuzz المقابلة له أو تطبعها. يدعم GenServer نوعين من الطلبات: [calls](http://elixir-lang.org/docs/stable/elixir/GenServer.html#call/3) و[casts](http://elixir-lang.org/docs/stable/elixir/GenServer.html#cast/2). النوع الأول متزامن ويُفترض أن يعيد شيئًا إلى العميل (دالة get لدينا)، أما الثاني فغير متزامن ولا يعيد بالضرورة أي شيء إلى العميل (دالة print لدينا).

من الشائع تغليف واجهة GenServer بواجهات API خاصة بنا للعملاء، لذا يستحق هذا النمط أن تتذكروه.

بعد ذلك ننتقل إلى تطبيق دوال callback اللازمة في GenServer.

```
def init(:ok) do
  Logger.debug "FizzBuzz server started"
  {:ok, %{}}
end

def handle_call({:print, n}, _from, state) do
  {:ok, fb, state} = fetch_or_calculate(n, state)
  {:reply, fb, state}
end

def handle_cast({:print, n}, state) do
  {:ok, fb, state} = fetch_or_calculate(n, state)
  IO.puts fb
  {:noreply, state}
end
```

تستدعي GenSever.start_link/3 الدالة init/1، وتعيد هذه الأخيرة tuple بالشكل `{:ok, state}`. وفي حالتنا هذه تكون الحالة (state) مجرد map بسيطة في Elixir (`%{}`).

الدالتان handle_call/2 وhandle_cast/2 هما قلب التطبيق. نستخدم pattern matching على الوسيط الأول لنحدد أننا نعالج الرسائل التي تأتي بالشكل `{:print, n}`. تسترجع الدالة الخاصة fetch_or_calculate/2 القيمة أو تحسبها، ثم نعيد الاستجابة المناسبة. في حالة call تكون الاستجابة بالشكل `{:reply, response, state}`، أما في حالة cast فتكون `{:noreply, state}`. هذه أعراف GenServer، ويسرد [التوثيق](http://elixir-lang.org/docs/stable/elixir/GenServer.html) جميع أنواع الردود الممكنة لكل نوع من الطلبات.

هكذا تبدو الدالة المساعدة الخاصة:

```
defp fetch_or_calculate(n, state) do
  if Dict.has_key?(state, n) do
    Logger.debug "Fetching #{n}"
    {:ok, fb} = Dict.fetch(state, n)
  else
    Logger.debug "Calculating #{n}"
    fb = fizzbuzz(n)
    state = Dict.put(state, n, fb)
  end
  {:ok, fb , state}
end
```

تتحقق fetch_or_calculate/2 مما إذا كانت قيمة FizzBuzz للرقم n قد حُسبت من قبل. إن كانت كذلك، نجلبها من القاموس. وإن لم تكن، نحسبها ونحدّث الحالة بإضافة القيمة الجديدة إليها (Dict.put(state, n, fb)). وفي النهاية نعيد tuple بالشكل `{:ok, value, state}` نطابقه في دوال المعالجة.

هذا كل شيء، أصبح خادم FizzBuzz جاهزًا للعمل! وإليكم الكود كاملًا:

```
defmodule FizzBuzz do
  use GenServer
  require Logger

  ## Client API

  def start_link do
     GenServer.start_link(__MODULE__, :ok, name: __MODULE__)
  end

  def get(n) do
    GenServer.call(__MODULE__, {:print, n})
  end

  def print(n) do
    GenServer.cast(__MODULE__, {:print, n})
  end

  ## Server Callbacks

  def init(:ok) do
    Logger.debug "FizzBuzz server started"
    {:ok, %{}}
  end

  def handle_call({:print, n}, _from, state) do
    {:ok, fb, state} = fetch_or_calculate(n, state)
    {:reply, fb, state}
  end

  def handle_cast({:print, n}, state) do
    {:ok, fb, state} = fetch_or_calculate(n, state)
    IO.puts fb
    {:noreply, state}
  end

  defp fetch_or_calculate(n, state) do
    if Dict.has_key?(state, n) do
      Logger.debug "Fetching #{n}"
      {:ok, fb} = Dict.fetch(state, n)
    else
      Logger.debug "Calculating #{n}"
      fb = fizzbuzz(n)
      state = Dict.put(state, n, fb)
    end
    {:ok, fb , state}
  end

  defp fizzbuzz(n) do
    case {rem(n, 3), rem(n, 5)} do
      {0, 0} -> :FizzBuzz
      {0, _} -> :Fizz
      {_, 0} -> :Buzz
      _      -> n
    end
  end
end
```

حان وقت استخدام الكود. من داخل مجلد المشروع، شغّلوا جلسة iex في سياق التطبيق بالأمر التالي:

```
iex -S mix
```

الآن يمكننا تشغيل الخادم وحساب بعض القيم:

```
FizzBuzz.start_link
4..6 |> Enum.map(&FizzBuzz.print/1)
```

هذا هو الناتج الذي يولّده الأمر. وبسبب الطبيعة غير المتزامنة للطلبات، يتداخل الناتج ورسائل السجل والقيمة التي تعيدها الدالة:

```
4

10:54:58.026 [debug] Calculating 4
Buzz
[:ok, :ok, :ok]

10:54:58.028 [debug] Calculating 5
Fizz
```

لاحظوا أن ناتج IO.puts ظهر في جلسة iex رغم أن الخادم يعمل في عملية منفصلة، لأن هذه الجلسة هي [قائد المجموعة (group leader)](http://elixir-lang.org/getting-started/io-and-the-file-system.html#processes-and-group-leaders) الحالي.

وإذا حاولنا الآن جلب قيمة سبق حسابها (FizzBuzz.get(5))، فسنرى أنها تُسترجع فعلًا من الـ cache:

```
10:58:56.774 [debug] Fetching 5
:Buzz
```

**الخلاصة**

استعرضنا في هذا المقال مدى سهولة بناء تطبيقات الخوادم بالاستفادة من OTP. أصبح خادم FizzBuzz الآن جاهزًا للنشر كجزء من تطبيق Elixir أكبر، مع ميزات مثل الإشراف (supervision) والاستبدال الحي للكود (hot code swapping).

**مصادر**

- [توثيق OTP الرسمي](http://www.erlang.org/doc/)
- [دليل OTP في Elixir](http://elixir-lang.org/getting_started/mix_otp/1.html)
- [The Little Elixir and OTP Guidebook](http://www.exotpbook.com/)

---

Contact OOZOU: hello@oozou.com · +66 2544 0000 · https://oozou.com/ar/contact

More for AI agents: https://oozou.com/llms.txt · every page has a markdown twin at its URL plus ".md"
