Improve examples and docs
This commit is contained in:
@@ -27,8 +27,8 @@ defmodule Registry do
|
||||
`Registry.start_link/1`, it can be used to register and access named
|
||||
processes using the `{:via, Registry, {registry, key}}` tuple:
|
||||
|
||||
{:ok, _} = Registry.start_link(keys: :unique, name: Registry.ViaTest)
|
||||
name = {:via, Registry, {Registry.ViaTest, "agent"}}
|
||||
{:ok, _} = Registry.start_link(keys: :unique, name: MyApp.Registry)
|
||||
name = {:via, Registry, {MyApp.Registry, "agent"}}
|
||||
{:ok, _} = Agent.start_link(fn -> 0 end, name: name)
|
||||
Agent.get(name, & &1)
|
||||
#=> 0
|
||||
@@ -39,22 +39,22 @@ defmodule Registry do
|
||||
In the previous example, we were not interested in associating a value to the
|
||||
process:
|
||||
|
||||
Registry.lookup(Registry.ViaTest, "agent")
|
||||
Registry.lookup(MyApp.Registry, "agent")
|
||||
#=> [{self(), nil}]
|
||||
|
||||
However, in some cases it may be desired to associate a value to the process
|
||||
using the alternate `{:via, Registry, {registry, key, value}}` tuple:
|
||||
|
||||
{:ok, _} = Registry.start_link(keys: :unique, name: Registry.ViaTest)
|
||||
name = {:via, Registry, {Registry.ViaTest, "agent", :hello}}
|
||||
{:ok, _} = Registry.start_link(keys: :unique, name: MyApp.Registry)
|
||||
name = {:via, Registry, {MyApp.Registry, "agent", :hello}}
|
||||
{:ok, agent_pid} = Agent.start_link(fn -> 0 end, name: name)
|
||||
Registry.lookup(Registry.ViaTest, "agent")
|
||||
Registry.lookup(MyApp.Registry, "agent")
|
||||
#=> [{agent_pid, :hello}]
|
||||
|
||||
To this point, we have been starting `Registry` using `start_link/1`.
|
||||
Typically the registry is started as part of a supervision tree though:
|
||||
|
||||
{Registry, keys: :unique, name: Registry.ViaTest}
|
||||
{Registry, keys: :unique, name: MyApp.Registry}
|
||||
|
||||
Only registries with unique keys can be used in `:via`. If the name is
|
||||
already taken, the case-specific `start_link` function (`Agent.start_link/2`
|
||||
|
||||
@@ -299,7 +299,7 @@ There are few known exceptions to this anti-pattern:
|
||||
|
||||
* [Protocol implementations](`Kernel.defimpl/2`) are, by design, defined under the protocol namespace
|
||||
|
||||
* [Custom Mix tasks](`Mix.Task`) are always defined under the `Mix.Tasks` namespace, such as `Mix.Tasks.PlugAuth`
|
||||
* In some scenarios, the namespace owner may allow exceptions to this rule. For example, in Elixir itself, you defined [custom Mix tasks](`Mix.Task`) by placing them under the `Mix.Tasks` namespace, such as `Mix.Tasks.PlugAuth`
|
||||
|
||||
* If you are the maintainer for both `plug` and `plug_auth`, then you may allow `plug_auth` to define modules with the `Plug` namespace, such as `Plug.Auth`. However, you are responsible for avoiding or managing any conflicts that may arise in the future
|
||||
|
||||
|
||||
@@ -126,7 +126,7 @@ Remember booleans are internally represented as atoms. Therefore there is no per
|
||||
|
||||
#### Problem
|
||||
|
||||
This anti-pattern refers to code that uses exceptions for control flow. Exception handling itself does not represent an anti-pattern, but developers must prefer to use `case` and pattern matching to change the flow of their code, instead of `try/rescue`. In turn, library authors should provide developers with APIs to handle errors without relying on exception handling. When developers have no freedom to decide if an error is exceptional or not, this is considered an anti-pattern.
|
||||
This anti-pattern refers to code that uses `Exception`s for control flow. Exception handling itself does not represent an anti-pattern, but developers must prefer to use `case` and pattern matching to change the flow of their code, instead of `try/rescue`. In turn, library authors should provide developers with APIs to handle errors without relying on exception handling. When developers have no freedom to decide if an error is exceptional or not, this is considered an anti-pattern.
|
||||
|
||||
#### Example
|
||||
|
||||
@@ -186,63 +186,11 @@ end
|
||||
|
||||
A common practice followed by the community is to make the non-raising version return `{:ok, result}` or `{:error, Exception.t}`. For example, an HTTP client may return `{:ok, %HTTP.Response{}}` on success cases and `{:error, %HTTP.Error{}}` for failures, where `HTTP.Error` is [implemented as an exception](`Kernel.defexception/1`). This makes it convenient for anyone to raise an exception by simply calling `Kernel.raise/1`.
|
||||
|
||||
## Feature envy
|
||||
|
||||
#### Problem
|
||||
|
||||
This anti-pattern occurs when a function accesses more data or calls more functions from another module than from its own. The presence of this anti-pattern can make a module less cohesive and increase code coupling.
|
||||
|
||||
#### Example
|
||||
|
||||
In the following code, all the data used in the `calculate_total_item/1` function of the module `Order` comes from the `OrderItem` module. This increases coupling and decreases code cohesion unnecessarily.
|
||||
|
||||
```elixir
|
||||
defmodule Order do
|
||||
# Some functions...
|
||||
|
||||
def calculate_total_item(id) do
|
||||
item = OrderItem.find_item(id)
|
||||
total = (item.price + item.taxes) * item.amount
|
||||
|
||||
if discount = OrderItem.find_discount(item) do
|
||||
total - total * discount
|
||||
else
|
||||
total
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
#### Refactoring
|
||||
|
||||
To remove this anti-pattern we can move `calculate_total_item/1` to `OrderItem`, decreasing coupling:
|
||||
|
||||
```elixir
|
||||
defmodule OrderItem do
|
||||
def find_item(id)
|
||||
def find_discount(item)
|
||||
|
||||
def calculate_total_item(id) do # <= function moved from Order!
|
||||
item = find_item(id)
|
||||
total = (item.price + item.taxes) * item.amount
|
||||
discount = find_discount(item)
|
||||
|
||||
unless is_nil(discount) do
|
||||
total - total * discount
|
||||
else
|
||||
total
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
This refactoring is only possible when you own both modules. If the module you are invoking belongs to another application, then it is not possible to add new functions to it, and your only option is to define an additional module that augments the third-party module.
|
||||
|
||||
## Primitive obsession
|
||||
|
||||
#### Problem
|
||||
|
||||
This anti-pattern happens when Elixir basic types (for example, *integer*, *float*, and *string*) are abusively used in function parameters and code variables, rather than creating specific composite data types (for example, *tuples*, *maps*, and *structs*) that can better represent a domain.
|
||||
This anti-pattern happens when Elixir basic types (for example, *integer*, *float*, and *string*) are excessively used to carry structured information, rather than creating specific composite data types (for example, *tuples*, *maps*, and *structs*) that can better represent a domain.
|
||||
|
||||
#### Example
|
||||
|
||||
@@ -250,17 +198,23 @@ An example of this anti-pattern is the use of a single *string* to represent an
|
||||
|
||||
```elixir
|
||||
defmodule MyApp do
|
||||
def process_address(address) when is_binary(address) do
|
||||
# Do something with address...
|
||||
def extract_postal_code(address) when is_binary(address) do
|
||||
# Extract postal code with address...
|
||||
end
|
||||
|
||||
def fill_in_country(address) when is_binary(address) do
|
||||
# Fill in missing country...
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
While you may receive the `address` as a string from a database, web request, or a third-party, if you find yourself frequently manipulating or extracting information from the string, it is a good indicator you should convert the address into structured data:
|
||||
|
||||
Another example of this anti-pattern is using floating numbers to model money and currency, when [richer data structures should be preferred](https://hexdocs.pm/ex_money/).
|
||||
|
||||
#### Refactoring
|
||||
|
||||
Possible solutions to this anti-pattern is to use maps or structs to model our address. The example below creates an `Address` struct, better representing this domain through a composite type. Additionally, we can modify the `process_address/1` function to accept a parameter of type `Address` instead of a *string*. With this modification, we can extract each field of this composite type individually when needed.
|
||||
Possible solutions to this anti-pattern is to use maps or structs to model our address. The example below creates an `Address` struct, better representing this domain through a composite type. Additionally, we introduce a `parse/1` function, that converts the string into an `Address`, which will simplify the logic of remainng functions. With this modification, we can extract each field of this composite type individually when needed.
|
||||
|
||||
```elixir
|
||||
defmodule Address do
|
||||
@@ -270,8 +224,16 @@ end
|
||||
|
||||
```elixir
|
||||
defmodule MyApp do
|
||||
def process_address(%Address{} = address) do
|
||||
# Do something with address...
|
||||
def parse(address) when is_binary(address) do
|
||||
# Returns %Address{}
|
||||
end
|
||||
|
||||
def extract_postal_code(%Address{} = address) do
|
||||
# Extract postal code with address...
|
||||
end
|
||||
|
||||
def fill_in_country(%Address{} = address) do
|
||||
# Fill in missing country...
|
||||
end
|
||||
end
|
||||
```
|
||||
@@ -342,7 +304,7 @@ Using multi-clause functions in Elixir, to group functions of the same name, is
|
||||
|
||||
A frequent example of this usage of multi-clause functions is when developers mix unrelated business logic into the same function definition. Such functions often have generic names or too broad specifications, making it difficult for maintainers and users of said functions to maintain and understand them.
|
||||
|
||||
Some developers may use documentation mechanisms such as `@doc` annotations to compensate for poor code readability, however the documentation itself may end-up full of conditionals to describe how the function behaves for each different argument combination.
|
||||
Some developers may use documentation mechanisms such as `@doc` annotations to compensate for poor code readability, however the documentation itself may end-up full of conditionals to describe how the function behaves for each different argument combination. This is a good indicator that the clauses are ultimately unrelated.
|
||||
|
||||
```elixir
|
||||
@doc """
|
||||
|
||||
Reference in New Issue
Block a user