Skip to content

Commit b217864

Browse files
committed
Last pass over anti-patterns
1 parent 9265603 commit b217864

File tree

4 files changed

+61
-126
lines changed

4 files changed

+61
-126
lines changed

Diff for: lib/elixir/pages/anti-patterns/code-anti-patterns.md

+40-51
Original file line numberDiff line numberDiff line change
@@ -309,9 +309,13 @@ There are few known exceptions to this anti-pattern:
309309

310310
#### Problem
311311

312-
In Elixir, it is possible to access values from `Map`s, which are key-value data structures, either statically or dynamically. When a key is expected to exist in the map, it must be accessed using the `map.key` notation, which asserts the key exists. If the key does not exist, an exception is raised (and in some situations also compiler warnings), allowing developers to catch bugs early on.
312+
In Elixir, it is possible to access values from `Map`s, which are key-value data structures, either statically or dynamically.
313313

314-
`map[:key]` must be used with optional keys. This way, if the informed key does not exist, `nil` is returned. When used with required keys, this return can be confusing and allow `nil` values to pass through the system, while `map.key` would raise upfront. In this way, this anti-pattern may cause bugs in the code.
314+
When a key is expected to exist in a map, it must be accessed using the `map.key` notation, making it clear to developers (and the compiler) that the key must exist. If the key does not exist, an exception is raised (and in some cases also compiler warnings). This is also known as the static notation, as the key is known at the time of writing the code.
315+
316+
When a key is optional, the `map[:key]` notation must be used instead. This way, if the informed key does not exist, `nil` is returned. This is the dynamic notation, as it also supports dynamic key access, such as `map[some_var]`.
317+
318+
When you use `map[:key]` to access a key that always exists in the map, you are making the code less clear for developers and for the compiler, as they now need to work with the assumption the key may not be there. This mismatch may also make it harder to track certain bugs. If the key is unexpected missing, you will have a `nil` value propagate through the system, instead of raising on map access.
315319

316320
#### Example
317321

@@ -321,8 +325,6 @@ The function `plot/1` tries to draw a graphic to represent the position of a poi
321325
defmodule Graphics do
322326
def plot(point) do
323327
# Some other code...
324-
325-
# Dynamic access to use point values
326328
{point[:x], point[:y], point[:z]}
327329
end
328330
end
@@ -331,92 +333,77 @@ end
331333
```elixir
332334
iex> point_2d = %{x: 2, y: 3}
333335
%{x: 2, y: 3}
334-
iex> point_3d = %{x: 5, y: 6, z: nil}
335-
%{x: 5, y: 6, z: nil}
336+
iex> point_3d = %{x: 5, y: 6, z: 7}
337+
%{x: 5, y: 6, z: 7}
336338
iex> Graphics.plot(point_2d)
337-
{2, 3, nil} # <= ambiguous return
339+
{2, 3, nil}
338340
iex> Graphics.plot(point_3d)
339-
{5, 6, nil}
341+
{5, 6, 7}
342+
```
343+
344+
Given we want to plot both 2D and 3D points, the behaviour above is expected. But what happens if we forget to pass a point with either `:x` or `:y`?
345+
346+
```elixir
347+
iex> bad_point = %{y: 3, z: 4}
348+
%{y: 3, z: 4}
349+
iex> Graphics.plot(bad_point)
350+
{nil, 3, 4}
340351
```
341352

342-
As can be seen in the example above, even when the key `:z` does not exist in the map (`point_2d`), dynamic access returns the value `nil`. This return can be dangerous because of its ambiguity. It is not possible to conclude from it whether the map has the key `:z` or not. If the function relies on the return value to make decisions about how to plot a point, this can be problematic and even cause errors when testing the code.
353+
The behaviour above is unexpected because our function should not work with points without a `:x` key. This leads to subtle bugs, as we may now pass `nil` to another function, instead of raising early on.
343354

344355
#### Refactoring
345356

346-
To remove this anti-pattern, whenever accessing an existing key of `Atom` type in the map, replace the dynamic `map[:key]` syntax by the static `map.key` notation. This way, when a non-existent key is accessed, Elixir raises an error immediately, allowing developers to find bugs faster. The next code illustrates the refactoring of `plot/1`, removing this anti-pattern:
357+
To remove this anti-pattern, we must use the dynamic `map[:key]` syntax and the static `map.key` notation according to our requirements. We expect `:x` and `:y` to always exist, but not `:z`. The next code illustrates the refactoring of `plot/1`, removing this anti-pattern:
347358

348359
```elixir
349360
defmodule Graphics do
350361
def plot(point) do
351362
# Some other code...
352-
353-
# Strict access to use point values
354-
{point.x, point.y, point.z}
363+
{point.x, point.y, point[:z]}
355364
end
356365
end
357366
```
358367

359368
```elixir
360-
iex> point_2d = %{x: 2, y: 3}
361-
%{x: 2, y: 3}
362-
iex> point_3d = %{x: 5, y: 6, z: nil}
363-
%{x: 5, y: 6, z: nil}
364369
iex> Graphics.plot(point_2d)
365-
** (KeyError) key :z not found in: %{x: 2, y: 3} # <= explicitly warns that
366-
graphic.ex:6: Graphics.plot/1 # <= the :z key does not exist!
367-
iex> Graphics.plot(point_3d)
368-
{5, 6, nil}
370+
{2, 3, nil}
371+
iex> Graphics.plot(bad_point)
372+
** (KeyError) key :x not found in: %{y: 3, z: 4} # <= explicitly warns that
373+
graphic.ex:4: Graphics.plot/1 # <= the :z key does not exist!
369374
```
370375

371376
Overall, the usage of `map.key` and `map[:key]` encode important information about your data structure, allowing developers to be clear about their intent. See both `Map` and `Access` module documentation for more information and examples.
372377

373-
An even simpler alternative to refactor this anti-pattern is to use pattern matching:
378+
An alternative to refactor this anti-pattern is to use pattern matching, defining explicit clauses for 2d vs 3d points:
374379

375380
```elixir
376381
defmodule Graphics do
377-
def plot(%{x: x, y: y, z: z}) do
382+
# 2d
383+
def plot(%{x: x, y: y}) do
378384
# Some other code...
385+
{x, y}
386+
end
379387

380-
# Strict access to use point values
388+
# 3d
389+
def plot(%{x: x, y: y, z: z}) do
390+
# Some other code...
381391
{x, y, z}
382392
end
383393
end
384394
```
385395

386-
```elixir
387-
iex> point_2d = %{x: 2, y: 3}
388-
%{x: 2, y: 3}
389-
iex> point_3d = %{x: 5, y: 6, z: nil}
390-
%{x: 5, y: 6, z: nil}
391-
iex> Graphics.plot(point_2d)
392-
** (FunctionClauseError) no function clause matching in Graphics.plot/1
393-
graphic.ex:2: Graphics.plot/1 # <= the :z key does not exist!
394-
iex> Graphics.plot(point_3d)
395-
{5, 6, nil}
396-
```
397-
398-
Pattern-matching is specially useful when matching over multiple keys at once and also when you want to match and assert on the values of a map.
396+
Pattern-matching is specially useful when matching over multiple keys as well as on the values themselves at once.
399397

400-
Another alternative is to use structs. By default, structs only support static access to its fields:
398+
Another option is to use structs. By default, structs only support static access to its fields. In such scenarios, you may consider defining structs for both 2D and 3D points:
401399

402400
```elixir
403-
defmodule Point.2D do
401+
defmodule Point2D do
404402
@enforce_keys [:x, :y]
405403
defstruct [x: nil, y: nil]
406404
end
407405
```
408406

409-
```elixir
410-
iex> point = %Point.2D{x: 2, y: 3}
411-
%Point.2D{x: 2, y: 3}
412-
iex> point.x # <= strict access to use point values
413-
2
414-
iex> point.z # <= trying to access a non-existent key
415-
** (KeyError) key :z not found in: %Point{x: 2, y: 3}
416-
iex> point[:x] # <= by default, struct does not support dynamic access
417-
** (UndefinedFunctionError) ... (Point does not implement the Access behaviour)
418-
```
419-
420407
Generally speaking, structs are useful when sharing data structures across modules, at the cost of adding a compile time dependency between these modules. If module `A` uses a struct defined in module `B`, `A` must be recompiled if the fields in the struct `B` change.
421408

422409
#### Additional remarks
@@ -498,7 +485,7 @@ case some_function(arg) do
498485
end
499486
```
500487

501-
In particular, avoid matching solely on `_`, as shown below, as it is less clear in intent and it may hide bugs if `some_function/1` adds new return values in the future:
488+
In particular, avoid matching solely on `_`, as shown below:
502489

503490
```elixir
504491
case some_function(arg) do
@@ -507,6 +494,8 @@ case some_function(arg) do
507494
end
508495
```
509496

497+
Matching on `_` is less clear in intent and it may hide bugs if `some_function/1` adds new return values in the future.
498+
510499
#### Additional remarks
511500

512501
This anti-pattern was formerly known as [Speculative assumptions](https://github.com/lucasvegi/Elixir-Code-Smells#speculative-assumptions).

Diff for: lib/elixir/pages/anti-patterns/design-anti-patterns.md

+2-63
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
# Design-related anti-patterns
22

3-
This document outlines potential anti-patterns related to your modules, functions, and the role they
4-
play within a codebase.
3+
This document outlines potential anti-patterns related to your modules, functions, and the role they play within a codebase.
54

65
## Alternative return types
76

@@ -242,66 +241,6 @@ defmodule MyApp do
242241
end
243242
```
244243

245-
## Propagating invalid data
246-
247-
#### Problem
248-
249-
This anti-pattern refers to a function that does not validate its parameters and propagates them to other functions, which can produce internal unexpected behavior. When an error is raised inside a function due to an invalid parameter value, it can be confusing for developers and make it harder to locate and fix the error.
250-
251-
#### Example
252-
253-
An example of this anti-pattern is when a function receives an invalid parameter and then passes it to other functions, either in the same library or in a third-party library. This can cause an error to be raised deep inside the call stack, which may be confusing for the developer who is working with invalid data. As shown next, the function `foo/1` is a user-facing API which doesn't validate its parameters at the boundary. In this way, it is possible that invalid data will be passed through, causing an error that is obscure and hard to debug.
254-
255-
```elixir
256-
defmodule MyLibrary do
257-
def foo(invalid_data) do
258-
# Some other code...
259-
260-
MyLibrary.Internal.sum(1, invalid_data)
261-
end
262-
end
263-
```
264-
265-
```elixir
266-
iex> MyLibrary.foo(2)
267-
3
268-
iex> MyLibrary.foo("José") # With invalid data
269-
** (ArithmeticError) bad argument in arithmetic expression: 1 + "José"
270-
:erlang.+(1, "José")
271-
my_library.ex:4: MyLibrary.Internal.sum/2
272-
```
273-
274-
#### Refactoring
275-
276-
To remove this anti-pattern, the client code must validate input parameters at the boundary with the user, via guard clauses, pattern matching, or conditionals. This prevents errors from occurring elsewhere in the call stack, making them easier to understand and debug. This refactoring also allows libraries to be implemented without worrying about creating internal protection mechanisms. The next code snippet illustrates the refactoring of `foo/1`, removing this anti-pattern:
277-
278-
```elixir
279-
defmodule MyLibrary do
280-
def foo(data) when is_integer(data) do
281-
# Some other code
282-
283-
MyLibrary.Internal.sum(1, data)
284-
end
285-
end
286-
```
287-
288-
```elixir
289-
iex> MyLibrary.foo(2) # With valid data
290-
3
291-
iex> MyLibrary.foo("José") # With invalid data
292-
** (FunctionClauseError) no function clause matching in MyLibrary.foo/1.
293-
The following arguments were given to MyLibrary.foo/1:
294-
295-
# 1
296-
"José"
297-
298-
my_library.ex:2: MyLibrary.foo/1
299-
```
300-
301-
#### Additional remarks
302-
303-
This anti-pattern was formerly known as [Working with invalid data](https://github.com/lucasvegi/Elixir-Code-Smells#working-with-invalid-data).
304-
305244
## Unrelated multi-clause function
306245

307246
#### Problem
@@ -377,7 +316,7 @@ end
377316

378317
You can see this pattern in practice within Elixir itself. The `+/2` operator can add `Integer`s and `Float`s together, but not `String`s, which instead use the `<>/2` operator. In this sense, it is reasonable to handle integers and floats in the same operation, but strings are unrelated enough to deserve their own function.
379318

380-
You will also find examples in Elixir of functions that work with any struct, such as `struct/2`:
319+
You will also find examples in Elixir of functions that work with any struct, which would seemingly be an occurrence of this anti-pattern, such as `struct/2`:
381320

382321
```elixir
383322
iex> struct(URI.parse("/foo/bar"), path: "/bar/baz")

Diff for: lib/elixir/pages/anti-patterns/macro-anti-patterns.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ end
6363

6464
#### Problem
6565

66-
**Macros** are powerful meta-programming mechanisms that can be used in Elixir to extend the language. While using macros is not an anti-pattern in itself, this meta-programming mechanism should only be used when absolutely necessary. Whenever a macro is used, but it would have been possible to solve the same problem using functions or other existing Elixir structures, the code becomes unnecessarily more complex and less readable. Because macros are more difficult to implement and reason about, their indiscriminate use can compromise the evolution of a system, reducing its maintainability.
66+
*Macros* are powerful meta-programming mechanisms that can be used in Elixir to extend the language. While using macros is not an anti-pattern in itself, this meta-programming mechanism should only be used when absolutely necessary. Whenever a macro is used, but it would have been possible to solve the same problem using functions or other existing Elixir structures, the code becomes unnecessarily more complex and less readable. Because macros are more difficult to implement and reason about, their indiscriminate use can compromise the evolution of a system, reducing its maintainability.
6767

6868
#### Example
6969

Diff for: lib/elixir/pages/anti-patterns/process-anti-patterns.md

+18-11
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ This document outlines potential anti-patterns related to processes and process-
66

77
#### Problem
88

9-
This anti-pattern refers to code that is unnecessarily organized by processes. A process itself does not represent an anti-pattern, but it should only be used to model runtime properties (such as concurrency, access to shared resources, and event scheduling). When you use a process for code organization, it can create bottlenecks in the system.
9+
This anti-pattern refers to code that is unnecessarily organized by processes. A process itself does not represent an anti-pattern, but it should only be used to model runtime properties (such as concurrency, access to shared resources, error isolation, etc). When you use a process for code organization, it can create bottlenecks in the system.
1010

1111
#### Example
1212

@@ -261,7 +261,9 @@ defmodule Counter do
261261
use GenServer
262262

263263
@doc "Starts a counter process."
264-
def start(initial_value, name \\ __MODULE__) when is_integer(initial_value) do
264+
def start_link(opts \\ []) do
265+
initial_valye = Keyword.get(opts, :initial_value, 0)
266+
name = Keywoird.get(opts, :name, __MODULE__)
265267
GenServer.start(__MODULE__, initial_value, name: name)
266268
end
267269

@@ -271,7 +273,7 @@ defmodule Counter do
271273
end
272274

273275
@doc "Bumps the value of the given counter."
274-
def bump(value, pid_name \\ __MODULE__) do
276+
def bump(pid_name \\ __MODULE__, value) do
275277
GenServer.call(pid_name, {:bump, value})
276278
end
277279

@@ -292,17 +294,17 @@ end
292294
```
293295

294296
```elixir
295-
iex> Counter.start(0)
297+
iex> Counter.start_link()
296298
{:ok, #PID<0.115.0>}
297299
iex> Counter.get()
298300
0
299-
iex> Counter.start(15, :other_counter)
301+
iex> Counter.start_link(initial_value: 15, name: :other_counter)
300302
{:ok, #PID<0.120.0>}
301303
iex> Counter.get(:other_counter)
302304
15
303-
iex> Counter.bump(-3, :other_counter)
305+
iex> Counter.bump(:other_counter, -3)
304306
12
305-
iex> Counter.bump(7)
307+
iex> Counter.bump(Counter, 7)
306308
7
307309
```
308310

@@ -317,8 +319,13 @@ defmodule SupervisedProcess.Application do
317319
@impl true
318320
def start(_type, _args) do
319321
children = [
320-
%{id: Counter, start: {Counter, :start, [0]}},
321-
%{id: :other_counter, start: {Counter, :start, [0, :other_counter]}}
322+
# With the default values for counter and name
323+
Counter,
324+
# With custom values for counter, name, and a custom ID
325+
Supervisor.child_spec(
326+
{Counter, name: :other_counter, initial_value: 15},
327+
id: :other_counter
328+
)
322329
]
323330

324331
Supervisor.start_link(children, strategy: :one_for_one, name: App.Supervisor)
@@ -332,8 +339,8 @@ iex> Supervisor.count_children(App.Supervisor)
332339
iex> Counter.get(Counter)
333340
0
334341
iex> Counter.get(:other_counter)
335-
0
336-
iex> Counter.bump(7, Counter)
342+
15
343+
iex> Counter.bump(Counter, 7)
337344
7
338345
iex> Supervisor.terminate_child(App.Supervisor, Counter)
339346
iex> Supervisor.count_children(App.Supervisor) # Only one active child

0 commit comments

Comments
 (0)