diff --git a/lib/elixir/pages/anti-patterns/design-anti-patterns.md b/lib/elixir/pages/anti-patterns/design-anti-patterns.md index 5e955ce0327..8680e5691fc 100644 --- a/lib/elixir/pages/anti-patterns/design-anti-patterns.md +++ b/lib/elixir/pages/anti-patterns/design-anti-patterns.md @@ -279,4 +279,59 @@ A common practice followed by the community is to make the non-raising version t ## Using application configuration for libraries -TODO +#### Problem + +The *[Application Environment](https://hexdocs.pm/elixir/1.15.5/Config.html)* is a mechanism that can be used to parameterize global values that will be used in several different places in a system implemented in Elixir. This parameterization mechanism can be very useful and therefore is not considered an anti-pattern by itself. However, when *Application Environments* are used as a mechanism for configuring a library's functions, this can make these functions less flexible, making it impossible for a library-dependent application to reuse their functions with different behaviors in different places in the code. Libraries are created to foster code reuse, so this limitation imposed by this parameterization mechanism can be problematic in this scenario. + +#### Example + +The `DashSplitter` module represents a library that configures the behavior of its functions through the global *Application Environment* mechanism. These configurations are concentrated in the *config/config.exs* file, shown below: + +```elixir +import Config + +config :app_config, + parts: 3 + +import_config "#{config_env()}.exs" +``` + +One of the functions implemented by the `DashSplitter` library is `split/1`. This function aims to separate a string received via a parameter into a certain number of parts. The character used as a separator in `split/1` is always `"-"` and the number of parts the string is split into is defined globally by the *Application Environment*. This value is retrieved by the `split/1` function by calling `Application.fetch_env!/2`, as shown next: + +```elixir +defmodule DashSplitter do + def split(string) when is_binary(string) do + parts = Application.fetch_env!(:app_config, :parts) # <= retrieve parameterized value + String.split(string, "-", parts: parts) # <= parts: 3 + end +end +``` + +Due to this parameterized value used by the `DashSplitter` library, all applications dependent on it can only use the `split/1` function with identical behavior about the number of parts generated by string separation. Currently, this value is equal to 3, as we can see in the use examples shown below: + +```elixir +iex> DashSplitter.split("Lucas-Francisco-Vegi") +["Lucas", "Francisco", "Vegi"] +iex> DashSplitter.split("Lucas-Francisco-da-Matta-Vegi") +["Lucas", "Francisco", "da-Matta-Vegi"] +``` + +#### Refactoring + +To remove this anti-pattern and make the library more adaptable and flexible, this type of configuration must be performed via parameters in function calls. The code shown below performs the refactoring of the `split/1` function by adding a new optional parameter of type *Keyword list*. With this new parameter, it is possible to modify the default behavior of the function at the time of its call, allowing multiple different ways of using `split/2` within the same application: + +```elixir +defmodule DashSplitter do + def split(string, opts \\ []) when is_binary(string) and is_list(opts) do + parts = Keyword.get(opts, :parts, 2) # <= default config of parts == 2 + String.split(string, "-", parts: parts) + end +end +``` + +```elixir +iex> DashSplitter.split("Lucas-Francisco-da-Matta-Vegi", [parts: 5]) +["Lucas", "Francisco", "da", "Matta", "Vegi"] +iex> DashSplitter.split("Lucas-Francisco-da-Matta-Vegi") #<= default config is used! +["Lucas", "Francisco-da-Matta-Vegi"] +```