Fork me on GitHub

Organizacja kodu

Some contents of this translation may be outdated.
Several major changes were applied to the original lesson since the last update.

Doświadczenie podpowiada, że bardzo ciężko jest trzymać cały nasz kod w jednym pliku. W tej lekcji przyjrzymy się, jak grupować nasze funkcje w moduły oraz jak za pomocą wyspecjalizowanych map, zwanych strukturami, można efektywnie zorganizować nasz kod.

Spis treści

Moduły

Moduły to najlepsza metoda na zorganizowanie naszego kodu w ramach przestrzeni nazw. Dodatkowo poza grupowaniem funkcji moduły pozwalają na definiowanie funkcji nazwanych oraz prywatnych, które poznaliśmy w poprzedniej lekcji.

Przyjrzyjmy się prostemu przykładowi:

defmodule Example do
  def greeting(name) do
    "Hello #{name}."
  end
end

iex> Example.greeting "Sean"
"Hello Sean."

W Elixirze możliwe jest zagnieżdżanie się modułów, pozwala to na lepszą organizację w naszej przestrzeni nazw:

defmodule Example.Greetings do
  def morning(name) do
    "Good morning #{name}."
  end

  def evening(name) do
    "Good night #{name}."
  end
end

iex> Example.Greetings.morning "Sean"
"Good morning Sean."

Atrybuty modułów

Atrybuty modułów są najczęściej wykorzystywane do reprezentowania stałych. Przyjrzyjmy się przykładowym atrybutom:

defmodule Example do
  @greeting "Hello"

  def greeting(name) do
    ~s([email protected]} #{name}.)
  end
end

W Elixirze istnieją zarezerwowane nazwy dla atrybutów. Trzy najpopularniejsze to:

Struktury

Struktury to wyspecjalizowane mapy, które zawierają zbiór kluczy i domyślnych wartości. Muszą być zdefiniowane w module i mają taką samą jak on nazwę. Nierzadko struktura jest jedynym elementem zdefiniowanym w module.

By zdefiniować strukturę, używamy słowa kluczowego defstruct wraz z listą asocjacyjną zawierającą nazwy pól i wartości domyślne:

defmodule Example.User do
  defstruct name: "Sean", roles: []
end

Stwórzmy zatem kilka struktur:

iex> %Example.User{}
%Example.User{name: "Sean", roles: []}

iex> %Example.User{name: "Steve"}
%Example.User{name: "Steve", roles: []}

iex> %Example.User{name: "Steve", roles: [:admin, :owner]}
%Example.User{name: "Steve", roles: [:admin, :owner]}

Struktury można aktualizować tak jak zwykłe mapy:

iex> steve = %Example.User{name: "Steve", roles: [:admin, :owner]}
%Example.User{name: "Steve", roles: [:admin, :owner]}
iex> sean = %{steve | name: "Sean"}
%Example.User{name: "Sean", roles: [:admin, :owner]}

I najważniejsze. Struktury można dopasowywać tak jak zwykłe mapy:

iex> %{name: "Sean"} = sean
%Example.User{name: "Sean", roles: [:admin, :owner]}

Komponenty

Skoro już wiemy jak tworzyć moduły oraz struktury przyjrzyjmy się jak wykorzystywać je w kodzie z pomocą komponentów. Elixir pozwala na współpracę pomiędzy modułami na kilka sposobów. Przyjrzyjmy się, z czego możemy skorzystać.

alias

Pozwalana tworzenie aliasów nazw modułów, co jest bardzo często wykorzystywane w kodzie Elixira:

defmodule Sayings.Greetings do
  def basic(name), do: "Hi, #{name}"
end

defmodule Example do
  alias Sayings.Greetings

  def greeting(name), do: Greetings.basic(name)
end

# Without alias

defmodule Example do
  def greeting(name), do: Sayings.Greetings.basic(name)
end

Jeżeli pojawi się konflikt w nazwach aliasów, to za pomocą :as możemy lokalnie zmienić nazwę jednego z nich:

defmodule Example do
  alias Sayings.Greetings, as: Hi

  def print_message(name), do: Hi.basic(name)
end

Można też utworzyć alias do wielu modułów naraz:

defmodule Example do
  alias Sayings.{Greetings, Farewells}
end

import

Jeżeli zamiast aliasu chcemy dołączyć, zaimportować, funkcje i makra z modułu do naszego kodu to możemy użyć import/:

iex> last([1, 2, 3])
** (CompileError) iex:9: undefined function last/1
iex> import List
nil
iex> last([1, 2, 3])
3

Filtrowanie

Domyślnie importowane są wszystkie funkcje i makra, ale możemy odfiltrować tylko część z nich za pomocą opcji :only i
:except.

By zaimportować wskazane funkcje i makra, musimy podać nazwę/ilość argumentów jako parametry :only i :except.
Zaimportujmy tylko funkcję last/1:

iex> import List, only: [last: 1]
iex> first([1, 2, 3])
** (CompileError) iex:13: undefined function first/1
iex> last([1, 2, 3])
3

Jeżeli zaimportujemy wszystkie funkcje poza last/1 i uruchomimy kod z poprzedniego przykładu:

iex> import List, except: [last: 1]
nil
iex> first([1, 2, 3])
1
iex> last([1, 2, 3])
** (CompileError) iex:3: undefined function last/1

Poza podaniem pary nazwa/liczba argumentów możemy też użyć dwóch atomów :functions i :macros, dzięki którym zaimportujemy odpowiednio tylko funkcje lub tylko makra:

import List, only: :functions
import List, only: :macros

require

Choć nie jest to zbyt często stosowana funkcja to require/2 jest też bardzo ważna. Pozwala ona na wymuszenie kompilacji i załadowania wskazanego modułu. Jest to szczególnie przydatne, jeżeli chcemy korzystać z makr:

defmodule Example do
  require SuperMacros

  SuperMacros.do_stuff
end

Jeżeli spróbujemy wywołać makro, które jeszcze nie zostało załadowane, to otrzymamy błąd.

use

Pozwala na użycie modułu w aktualnym kontekście. Jest to szczególnie użyteczne, gdy moduł potrzebny jest do konfiguracji. Wywołując use odwołujemy się do zaczepu __using__ wewnątrz modułu, pozwalając modułowi na zmiany w aktualnym kontekście:

defmodule MyModule do
  defmacro __using__(opts) do
    quote do
      import MyModule.Foo
      import MyModule.Bar
      import MyModule.Baz

      alias MyModule.Repo
    end
  end
end

Podziel się