Перейти до основного вмісту

Модулі в Elixir

Модулі — це основний спосіб організації коду в Elixir. Вони групують пов'язані функції разом і створюють простори імен, що дозволяє уникнути конфліктів імен та структурувати програму.

Базове визначення модуля

Модулі визначаються за допомогою ключового слова defmodule:

# Простий модуль
defmodule Greeting do
  def hello do
    "Привіт, світ!"
  end
  
  def hello(name) do
    "Привіт, #{name}!"
  end
end

# Виклик функцій модуля
Greeting.hello()
# "Привіт, світ!"

Greeting.hello("Олексій")
# "Привіт, Олексій!"

Публічні та приватні функції

Функції можуть бути публічними (def) або приватними (defp):

defmodule Calculator do
  # Публічна функція - доступна ззовні
  def calculate(a, b, operation) do
    case operation do
      :add -> add(a, b)
      :subtract -> subtract(a, b)
      :multiply -> multiply(a, b)
      :divide -> divide(a, b)
    end
  end
  
  # Приватні функції - доступні лише всередині модуля
  defp add(a, b), do: a + b
  defp subtract(a, b), do: a - b
  defp multiply(a, b), do: a * b
  
  defp divide(a, b) when b != 0, do: a / b
  defp divide(_a, 0), do: {:error, "Ділення на нуль"}
end

# Можна викликати
Calculator.calculate(10, 5, :add)
# 15

# Не можна викликати напряму
Calculator.add(10, 5)
# ** (UndefinedFunctionError)
Примітка: Приватні функції (defp) можна викликати тільки з того ж модуля. Це допомагає інкапсулювати внутрішню логіку.

Вкладені модулі

Модулі можуть бути вкладеними для кращої організації коду:

# Вкладені модулі
defmodule MyApp do
  defmodule User do
    defstruct [:name, :email, :age]
    
    def new(name, email, age) do
      %__MODULE__{name: name, email: email, age: age}
    end
    
    def adult?(%__MODULE__{age: age}) do
      age >= 18
    end
  end
  
  defmodule Post do
    defstruct [:title, :content, :author]
    
    def new(title, content, author) do
      %__MODULE__{title: title, content: content, author: author}
    end
  end
end

# Використання
user = MyApp.User.new("Іван", "ivan@example.com", 25)
MyApp.User.adult?(user)
# true

post = MyApp.Post.new("Заголовок", "Контент", user)
Порада: __MODULE__ — це спеціальна директива, що повертає ім'я поточного модуля. Вона корисна для уникнення дублювання імен.

Атрибути модуля

Атрибути модуля визначаються за допомогою @ і використовуються для метаданих та констант:

defmodule Config do
  # Константи
  @api_version "v1"
  @timeout 5000
  @max_retries 3
  
  # Документація
  @moduledoc """
  Модуль для роботи з конфігурацією API.
  """
  
  @doc """
  Повертає базову URL для API.
  """
  def api_url do
    "https://api.example.com/#{@api_version}"
  end
  
  def timeout, do: @timeout
  def max_retries, do: @max_retries
end

Config.api_url()
# "https://api.example.com/v1"

Config.timeout()
# 5000

Директива alias

Директива alias дозволяє створювати короткі імена для модулів:

defmodule MyApp.Services.UserService do
  # Без alias
  def create_user(params) do
    MyApp.Schemas.User.changeset(%MyApp.Schemas.User{}, params)
  end
end

defmodule MyApp.Services.UserService do
  # З alias
  alias MyApp.Schemas.User
  
  def create_user(params) do
    User.changeset(%User{}, params)
  end
  
  # Можна задати власне ім'я
  alias MyApp.Schemas.User, as: UserSchema
  
  def validate_user(params) do
    UserSchema.validate(params)
  end
  
  # Можна використати кілька alias
  alias MyApp.Schemas.{User, Post, Comment}
  
  def get_user_content(user_id) do
    user = User.get(user_id)
    posts = Post.by_user(user_id)
    comments = Comment.by_user(user_id)
    {user, posts, comments}
  end
end

Директива import

Директива import дозволяє викликати функції без префікса модуля:

defmodule StringHelper do
  # Імпорт усіх функцій
  import String
  
  def process(text) do
    text
    |> upcase()      # Замість String.upcase()
    |> trim()        # Замість String.trim()
    |> split(",")    # Замість String.split()
  end
  
  # Імпорт тільки певних функцій
  import List, only: [first: 1, last: 1]
  
  def get_boundaries(list) do
    {first(list), last(list)}
  end
  
  # Імпорт усього окрім певних функцій
  import Enum, except: [map: 2]
end
Увага: Використовуйте import обережно, оскільки він може ускладнити розуміння, звідки приходять функції. Краще використовувати alias або повні імена модулів.

Директива require

Директива require використовується для макросів:

defmodule LoggerExample do
  # require потрібен для використання макросів
  require Logger
  
  def process_data(data) do
    Logger.info("Обробка даних: #{inspect(data)}")
    
    result = do_processing(data)
    
    Logger.debug("Результат: #{inspect(result)}")
    result
  end
  
  defp do_processing(data) do
    # Обробка даних
    data
  end
end

Директива use

Директива use викликає макрос __using__ з іншого модуля:

# Визначення модуля з __using__
defmodule Validator do
  defmacro __using__(_opts) do
    quote do
      def validate_presence(value, field) do
        if is_nil(value) or value == "" do
          {:error, "#{field} обов'язкове"}
        else
          :ok
        end
      end
      
      def validate_length(value, field, min, max) do
        len = String.length(value)
        cond do
          len < min -> {:error, "#{field} занадто коротке"}
          len > max -> {:error, "#{field} занадто довге"}
          true -> :ok
        end
      end
    end
  end
end

# Використання
defmodule UserValidator do
  use Validator
  
  def validate_user(%{name: name, email: email}) do
    with :ok <- validate_presence(name, "Ім'я"),
         :ok <- validate_length(name, "Ім'я", 2, 50),
         :ok <- validate_presence(email, "Email") do
      :ok
    end
  end
end

Поведінки (Behaviours)

Поведінки визначають набір функцій, які повинен реалізувати модуль:

# Визначення поведінки
defmodule Storage do
  @callback save(key :: String.t(), value :: any()) :: :ok | {:error, term()}
  @callback get(key :: String.t()) :: {:ok, any()} | {:error, term()}
  @callback delete(key :: String.t()) :: :ok | {:error, term()}
end

# Реалізація поведінки
defmodule FileStorage do
  @behaviour Storage
  
  @impl Storage
  def save(key, value) do
    File.write("storage/#{key}", :erlang.term_to_binary(value))
  end
  
  @impl Storage
  def get(key) do
    case File.read("storage/#{key}") do
      {:ok, binary} -> {:ok, :erlang.binary_to_term(binary)}
      error -> error
    end
  end
  
  @impl Storage
  def delete(key) do
    File.rm("storage/#{key}")
  end
end

# Ще одна реалізація
defmodule MemoryStorage do
  @behaviour Storage
  
  def start_link do
    Agent.start_link(fn -> %{} end, name: __MODULE__)
  end
  
  @impl Storage
  def save(key, value) do
    Agent.update(__MODULE__, &Map.put(&1, key, value))
    :ok
  end
  
  @impl Storage
  def get(key) do
    case Agent.get(__MODULE__, &Map.get(&1, key)) do
      nil -> {:error, :not_found}
      value -> {:ok, value}
    end
  end
  
  @impl Storage
  def delete(key) do
    Agent.update(__MODULE__, &Map.delete(&1, key))
    :ok
  end
end

Структури (Structs)

Структури — це спеціальні карти з фіксованим набором полів, що визначаються в модулях:

defmodule User do
  # Визначення структури
  defstruct [:id, :name, :email, age: 0, active: true]
  
  # Функції для роботи зі структурою
  def new(name, email) do
    %__MODULE__{
      id: generate_id(),
      name: name,
      email: email
    }
  end
  
  def activate(%__MODULE__{} = user) do
    %{user | active: true}
  end
  
  def deactivate(%__MODULE__{} = user) do
    %{user | active: false}
  end
  
  def adult?(%__MODULE__{age: age}) when age >= 18, do: true
  def adult?(%__MODULE__{}), do: false
  
  defp generate_id do
    :crypto.strong_rand_bytes(16) |> Base.encode64()
  end
end

# Використання
user = User.new("Марія", "maria@example.com")
# %User{id: "...", name: "Марія", email: "maria@example.com", age: 0, active: true}

user = %{user | age: 25}
User.adult?(user)
# true

user = User.deactivate(user)
# %User{..., active: false}

Протоколи

Протоколи дозволяють визначати поліморфні функції:

# Визначення протоколу
defprotocol Renderable do
  @doc "Перетворює дані в рядок для відображення"
  def render(data)
end

# Реалізація для різних типів
defimpl Renderable, for: User do
  def render(%User{name: name, email: email}) do
    "Користувач: #{name} (#{email})"
  end
end

defimpl Renderable, for: List do
  def render(list) do
    items = Enum.map_join(list, ", ", &Renderable.render/1)
    "[#{items}]"
  end
end

defimpl Renderable, for: Integer do
  def render(number) do
    "Число: #{number}"
  end
end

# Використання
user = %User{name: "Іван", email: "ivan@example.com"}
Renderable.render(user)
# "Користувач: Іван (ivan@example.com)"

Renderable.render([1, 2, 3])
# "[Число: 1, Число: 2, Число: 3]"

Організація файлів та модулів

# Структура проєкту
lib/
  my_app/
    accounts/
      user.ex           # MyApp.Accounts.User
      session.ex        # MyApp.Accounts.Session
    blog/
      post.ex           # MyApp.Blog.Post
      comment.ex        # MyApp.Blog.Comment
    repo.ex             # MyApp.Repo
  my_app.ex             # MyApp

# Приклад user.ex
defmodule MyApp.Accounts.User do
  use Ecto.Schema
  import Ecto.Changeset
  
  alias MyApp.Accounts.Session
  
  schema "users" do
    field :name, :string
    field :email, :string
    field :password_hash, :string
    
    has_many :sessions, Session
    
    timestamps()
  end
  
  def changeset(user, attrs) do
    user
    |> cast(attrs, [:name, :email, :password_hash])
    |> validate_required([:name, :email])
    |> validate_format(:email, ~r/@/)
    |> unique_constraint(:email)
  end
end

Модульна документація

defmodule MathHelper do
  @moduledoc """
  Допоміжні математичні функції.
  
  ## Приклади
  
      iex> MathHelper.factorial(5)
      120
      
      iex> MathHelper.fibonacci(7)
      13
  """
  
  @doc """
  Обчислює факторіал числа.
  
  ## Параметри
  
    - n: невід'ємне ціле число
  
  ## Приклади
  
      iex> MathHelper.factorial(0)
      1
      
      iex> MathHelper.factorial(5)
      120
  """
  @spec factorial(non_neg_integer()) :: pos_integer()
  def factorial(0), do: 1
  def factorial(n) when n > 0, do: n * factorial(n - 1)
  
  @doc """
  Обчислює n-не число Фібоначчі.
  """
  @spec fibonacci(non_neg_integer()) :: non_neg_integer()
  def fibonacci(0), do: 0
  def fibonacci(1), do: 1
  def fibonacci(n), do: fibonacci(n - 1) + fibonacci(n - 2)
  
  @doc false
  def internal_helper do
    # Ця функція не буде в документації
  end
end

Порівняння директив

Директива Призначення Приклад
alias Скорочує імена модулів alias MyApp.User
import Дозволяє викликати функції без модуля import String
require Потрібен для макросів require Logger
use Викликає __using__ макрос use GenServer

Кращі практики

  • Один модуль — один файл: назва файлу повинна відповідати імені модуля
  • Іменування: використовуйте CamelCase для модулів (MyApp.UserService)
  • Організація: групуйте пов'язані модулі в папки
  • Розмір: уникайте надто великих модулів (більше 300-400 рядків)
  • Відповідальність: кожен модуль повинен мати одну чітку відповідальність
  • Документація: документуйте всі публічні функції
  • Приватні функції: використовуйте defp для внутрішньої логіки
  • alias vs import: віддавайте перевагу alias над import для кращої читабельності

Модулі — це фундамент структурування коду в Elixir. Правильна організація модулів робить код зрозумілим, підтримуваним та легким для тестування. Дотримуйтесь принципів одиничної відповідальності та чіткого розділення публічного та приватного API.

Коментарі

Популярні публікації

Створення нового Elixir-проєкту

Для створення новго Elixir-проєкту можна використати команду mix new first_project --sup Зрозуміло, що Elixir має бути встановлений раніше. Пояснення команди: mix — це вбудований інструмент для управління проєктами в Elixir (аналог maven у Java чи npm у JavaScript ). new — підкоманда mix, яка створює новий проєкт. first_project — назва твого нового проєкту. Папка з цією назвою буде створена у поточному каталозі. --sup — опціональний прапорець, який додає шаблон структури з Supervision Tree. Це означає, що створений проєкт одразу буде мати структуру, яка підтримує супервізор (супервізор керує життєвим циклом процесів у системі, перезапускаючи їх при падінні). Щоб створити файл з тестом, можна запустити команду із директорії проєкту mix test Приблизний вигляд структури проєкту:

Агрегати в DDD

Domain-Driven Design (DDD, предметно-орієнтоване проєктування) — це підхід до розробки програмного забезпечення, який зосереджується на моделюванні бізнес-логіки на основі реального домену (предметної області). Його запропонував Ерік Еванс у своїй книзі "Domain-Driven Design: Tackling Complexity in the Heart of Software". Основні принципи DDD Фокус на домені – головна увага приділяється предметній області, а не технічним деталям. Єдина мова (Ubiquitous Language) – розробники, бізнес-аналітики та інші учасники проєкту використовують спільну термінологію, щоб уникнути непорозумінь. Бізнес-логіка відокремлена від технічної реалізації – код моделюється так, щоб він чітко відображав реальний бізнес-процес. Основні концепції DDD Entity (Сутність) – об’єкт з унікальним ідентифікатором, що зберігається в системі (наприклад, Користувач, Замовлення). Value Object (Об’єкт-значення) – об’єкт, який не має унікального ідентифікатора та є незмінним (наприклад, Адреса або Гроші)...

Основи Elixir

Elixir — це функційна мова програмування, яка працює на віртуальній машині Erlang (BEAM). Вона призначена для створення масштабованих і відмовостійких систем. Elixir успадкував багато переваг Erlang, таких як легкість паралельного програмування та висока доступність, але також додав сучасний синтаксис та інструменти для розробки. Основні концепції Elixir Elixir є функційною мовою, тому вона орієнтована на використання функцій та незмінних даних. Ось декілька ключових концепцій: Незмінність даних. Усі дані в Elixir є незмінними, що спрощує роботу з паралельними процесами. Функції. Функції є основним будівельним блоком програми. Вони можуть бути анонімними або іменованими. Паттерн-матчинг. Elixir використовує паттерн-матчинг для роботи з даними, що дозволяє легко розбирати структури даних. Процеси. Elixir використовує легкі процеси для паралельного виконання завдань. Ці процеси ізольовані та спілкуються через передачу повідомлень. Синтаксис Elixir Синтаксис Elixir є прос...

Стратегії ребалансування в Kafka

Стратегії ребалансування в Kafka Ребалансування (Rebalancing) — це процес перерозподілу партицій між споживачами (сonsumer) у групі (Consumer Group). Kafka має кілька стратегій ребалансування: RangeAssignor. Ця стратегія розподіляє партиції на основі діапазонів, які створюються відповідно до сортування топіків і партицій. Наприклад, якщо є два консюмери і 6 партицій (P0–P5), перший консюмер отримає P0–P2, а другий — P3–P5. Особливості: Простий алгоритм. Може призводити до нерівномірного розподілу, якщо кількість партицій не ділиться порівну між консюмерами. RoundRobinAssignor. Ця стратегія рівномірно розподіляє партиції між консюмерами за круговим принципом. Наприклад, якщо є два консюмери і 6 партицій, перший отримає P0, P2, P4, а другий — P1, P3, P5. Особливості: Гарантує більш рівномірний розподіл партицій. Використовується в багатотопікових сценаріях. StickyAssignor. Ця стратегія намагається мінімізувати кількість змін у розподілі партицій між консюмерами при ре...

Angular CLI

CLI (command-line interface) – інтерфейс командного рядка. Перед початком роботи має бути встановлений Node.js Встановлення: npm install -g @angular/cli Отримання допомоги: ng help Буде приблизно такий результат: add Adds support for an external library to your project. analytics Configures the gathering of Angular CLI usage metrics. See https://angular.io/cli/usage-analytics-gathering. build (b) Compiles an Angular app into an output directory named dist/ at the given output path. Must be executed from within a workspace directory. deploy Invokes the deploy builder for a specified project or for the default project in the workspace. config Retrieves or sets Angular configuration values in the angular.json file for the workspace. doc (d) Opens the official Angular documentation (angular.io) in a browser, and searches for a given keyword. e2e (e) Builds and serves an Angular app, then runs end-to-end tests. extract-i18n (i18n-extract, xi18n) Extracts i18n mes...