├── CITATION.cff ├── LICENSE ├── README.md ├── code_smells.livemd ├── docs ├── index.html └── survey_questions_version_a.pdf ├── etc ├── 2022-icpc-era.pdf ├── 2023-emse-code-smells-elixir.pdf ├── Code-Smells-in-Elixir-ICPC22-Lucas-Vegi.pdf └── finbits.png ├── mix.exs └── traditional └── README.md /CITATION.cff: -------------------------------------------------------------------------------- 1 | cff-version: 1.2.1 2 | message: 'If you use this catalog in your work, please cite it as below.' 3 | authors: 4 | - given-names: Lucas Francisco da Matta 5 | family-names: Vegi 6 | email: lucasvegi@gmail.com 7 | affiliation: UFMG 8 | orcid: 'https://orcid.org/0000-0002-7999-7098' 9 | - given-names: Marco Tulio 10 | family-names: Valente 11 | email: mtvalente@gmail.com 12 | affiliation: UFMG 13 | orcid: 'https://orcid.org/0000-0002-8180-7548' 14 | title: 'Catalog of Elixir-specific code smells' 15 | version: '1.0' 16 | date-released: '2022-02-15' 17 | url: 'https://github.com/lucasvegi/Elixir-Code-Smells' 18 | keywords: 19 | - elixir 20 | - code smells 21 | - functional programming 22 | license: MIT 23 | preferred-citation: 24 | type: article 25 | message: 'If you use this catalog in your work, please cite it as below.' 26 | authors: 27 | - given-names: Lucas Francisco da Matta 28 | family-names: Vegi 29 | email: lucasvegi@dcc.ufmg.br 30 | affiliation: Federal University of Minas Gerais (UFMG) 31 | orcid: 'https://orcid.org/0000-0002-7999-7098' 32 | - given-names: Marco Tulio 33 | family-names: Valente 34 | email: mtov@dcc.ufmg.br 35 | affiliation: Federal University of Minas Gerais (UFMG) 36 | orcid: 'https://orcid.org/0000-0002-8180-7548' 37 | doi: "10.1007/s10664-023-10343-6" 38 | journal: "Empirical Software Engineering" 39 | pages: 32 40 | start: 1 # First page number 41 | end: 32 # Last page number 42 | title: "Understanding code smells in Elixir functional language" 43 | issue: 102 44 | volume: 28 45 | year: 2023 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Lucas Francisco da Matta Vegi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [Catalog of Elixir-specific code smells][Elixir Smells] 2 | 3 | [![Run in Livebook](https://livebook.dev/badge/v1/black.svg)](https://livebook.dev/run?url=https%3A%2F%2Fgithub.com%2Flucasvegi%2FElixir-Code-Smells%2Fblob%2Fmain%2Fcode_smells.livemd) 4 | 5 | [![GitHub last commit](https://img.shields.io/github/last-commit/lucasvegi/Elixir-Code-Smells)](https://github.com/lucasvegi/Elixir-Code-Smells/commits/main) 6 | [![Twitter URL](https://img.shields.io/twitter/url?style=social&url=https%3A%2F%2Fgithub.com%2Flucasvegi%2FElixir-Code-Smells)](https://twitter.com/intent/tweet?url=https%3A%2F%2Fgithub.com%2Flucasvegi%2FElixir-Code-Smells&via=lucasvegi&text=Catalog%20of%20Elixir-specific%20code%20smells%3A&hashtags=MyElixirStatus%2CElixirLang) 7 | 8 | ## Table of Contents 9 | 10 | * __[Introduction](#introduction)__ 11 | * __[Design-related smells](#design-related-smells)__ 12 | * [GenServer Envy](#genserver-envy) 13 | * [Agent Obsession](#agent-obsession) 14 | * [Unsupervised process](#unsupervised-process) 15 | * [Large messages](#large-messages) 16 | * [Unrelated multi-clause function](#unrelated-multi-clause-function) 17 | * [Complex extractions in clauses](#complex-extractions-in-clauses) [^*] 18 | * [Using exceptions for control-flow](#using-exceptions-for-control-flow) 19 | * [Untested polymorphic behaviors](#untested-polymorphic-behaviors) 20 | * [Code organization by process](#code-organization-by-process) 21 | * [Large code generation by macros](#large-code-generation-by-macros) [^*] 22 | * [Data manipulation by migration](#data-manipulation-by-migration) 23 | * [Using App Configuration for libraries](#using-app-configuration-for-libraries) 24 | * [Compile-time global configuration](#compile-time-global-configuration) 25 | * ["Use" instead of "import"](#use-instead-of-import) 26 | * __[Low-level concerns smells](#low-level-concerns-smells)__ 27 | * [Working with invalid data](#working-with-invalid-data) 28 | * [Complex branching](#complex-branching) 29 | * [Complex else clauses in with](#complex-else-clauses-in-with) [^*] 30 | * [Alternative return types](#alternative-return-types) [^*] 31 | * [Accessing non-existent map/struct fields](#accessing-non-existent-mapstruct-fields) 32 | * [Speculative Assumptions](#speculative-assumptions) 33 | * [Modules with identical names](#modules-with-identical-names) 34 | * [Unnecessary macros](#unnecessary-macros) 35 | * [Dynamic atom creation](#dynamic-atom-creation) [^**] 36 | * __[Traditional code smells][TraditionalSmells]__ 37 | * __[About](#about)__ 38 | * __[Acknowledgments](#acknowledgments)__ 39 | 40 | [^*]: These code smells were suggested by the Elixir community. 41 | [^**]: This code smell emerged from a study with mining software repositories (MSR). 42 | 43 | ## Introduction 44 | 45 | [Elixir][Elixir] is a functional programming language whose popularity is rising in the industry [link][ElixirInProduction]. However, there are few works in the scientific literature focused on studying the internal quality of systems implemented in this language. 46 | 47 | In order to better understand the types of sub-optimal code structures that can harm the internal quality of Elixir systems, we scoured websites, blogs, forums, and videos (grey literature review), looking for specific code smells for Elixir that are discussed by its developers. 48 | 49 | As a result of this investigation, we have initially proposed a catalog of 18 new smells that are specific to Elixir systems. After that, 1 new smell emerged from a study with mining software repositories (MSR) performed by us, and other smells are being suggested by the community, so this catalog is constantly being updated __(currently 23 smells)__. These code smells are categorized into two different groups ([design-related](#design-related-smells) and [low-level concerns](#low-level-concerns-smells)), according to the type of impact and code extent they affect. This catalog of Elixir-specific code smells is presented below. Each code smell is documented using the following structure: 50 | 51 | * __Name:__ Unique identifier of the code smell. This name is important to facilitate communication between developers; 52 | * __Category:__ The portion of code affected by smell and its severity; 53 | * __Problem:__ How the code smell can harm code quality and what impacts this can have for developers; 54 | * __Example:__ Code and textual descriptions to illustrate the occurrence of the code smell; 55 | * __Refactoring:__ Examples of refactored code are presented to illustrate higher-quality alternatives compared to smelly code; 56 | 57 | * __Treatments:__ How to remove a code smell in a *__disciplined way__* with the assistance of *__refactoring strategies__*. When a refactoring should be used alone, it is listed in its own bullet point (*i.e.*, •). Conversely, when a refactoring is part of a sequence of operations to assist the removal, it is listed using a pipeline to define its order (*e.g.*, Refactoring1 |> Refactoring2 |> Refactoring3 |> etc.). This disciplined way to refactor a smell will help you change your code one small step at a time, thus minimizing the chances of introducing bugs or altering the original behavior of the system. All the refactoring strategies mapped to the code smells are part of our [Catalog of Elixir Refactorings](https://github.com/lucasvegi/Elixir-Refactorings). 58 | 59 | In addition to the Elixir-specific code smells, our catalog also documents 12 [traditional code smells][TraditionalSmells] discussed in the context of Elixir systems. 60 | 61 | The objective of this catalog of code smells is to instigate the improvement of the quality of code developed in Elixir. For this reason, we are interested in knowing Elixir's community opinion about these code smells: *Do you agree that these code smells can be harmful? Have you seen any of them in production code? Do you have any suggestions about some Elixir-specific code smell not cataloged by us?...* 62 | 63 | Please feel free to make pull requests and suggestions ([Issues][Issues] tab). We want to hear from you! 64 | 65 | [▲ back to Index](#table-of-contents) 66 | 67 | ## Design-related smells 68 | 69 | Design-related smells are more complex, affect a coarse-grained code element, and are therefore harder to detect. In this section, 14 different smells classified as design-related are explained and exemplified: 70 | 71 | ### GenServer Envy 72 | 73 | * __Category:__ Design-related smell. 74 | 75 | * __Problem:__ In Elixir, processes can be primitively created by ``Kernel.spawn/1``, ``Kernel.spawn/3``, ``Kernel.spawn_link/1`` and ``Kernel.spawn_link/3`` functions. Although it is possible to create them this way, it is more common to use abstractions (e.g., [``Agent``][Agent], [``Task``][Task], and [``GenServer``][GenServer]) provided by Elixir to create processes. The use of each specific abstraction is not a code smell in itself; however, there can be trouble when either a ``Task`` or ``Agent`` is used beyond its suggested purposes, being treated like a ``GenServer``. 76 | 77 | * __Example:__ As shown next, ``Agent`` and ``Task`` are abstractions to create processes with specialized purposes. In contrast, ``GenServer`` is a more generic abstraction used to create processes for many different purposes: 78 | 79 | * ``Agent``: As Elixir works on the principle of immutability, by default no value is shared between multiple places of code, enabling read and write as in a global variable. An ``Agent`` is a simple process abstraction focused on solving this limitation, enabling processes to share state. 80 | * ``Task``: This process abstraction is used when we only need to execute some specific action asynchronously, often in an isolated way, without communication with other processes. 81 | * ``GenServer``: This is the most generic process abstraction. The main benefit of this abstraction is explicitly segregating the server and the client roles, thus providing a better API for the organization of processes communication. Besides that, a ``GenServer`` can also encapsulate state (like an ``Agent``), provide sync and async calls (like a ``Task``), and more. 82 | 83 | Examples of this code smell appear when ``Agents`` or ``Tasks`` are used for general purposes and not only for specialized ones such as their documentation suggests. To illustrate some smell occurrences, we will cite two specific situations. 1) When a ``Task`` is used not only to async execute an action, but also to frequently exchange messages with other processes; 2) When an ``Agent``, beside sharing some global value between processes, is also frequently used to execute isolated tasks that are not of interest to other processes. 84 | 85 | * __Refactoring:__ When an ``Agent`` or ``Task`` goes beyond its suggested use cases and becomes painful, it is better to refactor it into a ``GenServer``. 86 | 87 | * __Treatments:__ 88 | 89 | * [Generalise a process abstraction](https://github.com/lucasvegi/Elixir-Refactorings?#generalise-a-process-abstraction) |> [Introduce processes](https://github.com/lucasvegi/Elixir-Refactorings?#introduce-processes) |> [Register a process](https://github.com/lucasvegi/Elixir-Refactorings?#register-a-process) |> [Remove dead code](https://github.com/lucasvegi/Elixir-Refactorings?#remove-dead-code) 90 | 91 | [▲ back to Index](#table-of-contents) 92 | ___ 93 | 94 | ### Agent Obsession 95 | 96 | * __Category:__ Design-related smell. 97 | 98 | * __Problem:__ In Elixir, an ``Agent`` is a process abstraction focused on sharing information between processes by means of message passing. It is a simple wrapper around shared information, thus facilitating its read and update from any place in the code. The use of an ``Agent`` to share information is not a code smell in itself; however, when the responsibility for interacting directly with an ``Agent`` is spread across the entire system, this can be problematic. This bad practice can increase the difficulty of code maintenance and make the code more prone to bugs. 99 | 100 | * __Example:__ The following code seeks to illustrate this smell. The responsibility for interacting directly with the ``Agent`` is spread across four different modules (i.e, ``A``, ``B``, ``C``, and ``D``). 101 | 102 | ```elixir 103 | defmodule A do 104 | #... 105 | def update(pid) do 106 | #... 107 | Agent.update(pid, fn _list -> 123 end) 108 | #... 109 | end 110 | end 111 | ``` 112 | 113 | ```elixir 114 | defmodule B do 115 | #... 116 | def update(pid) do 117 | #... 118 | Agent.update(pid, fn content -> %{a: content} end) 119 | #... 120 | end 121 | end 122 | ``` 123 | 124 | ```elixir 125 | defmodule C do 126 | #... 127 | def update(pid) do 128 | #... 129 | Agent.update(pid, fn content -> [:atom_value | [content]] end) 130 | #... 131 | end 132 | end 133 | ``` 134 | 135 | ```elixir 136 | defmodule D do 137 | #... 138 | def get(pid) do 139 | #... 140 | Agent.get(pid, fn content -> content end) 141 | #... 142 | end 143 | end 144 | ``` 145 | 146 | This spreading of responsibility can generate duplicated code and make code maintenance more difficult. Also, due to the lack of control over the format of the shared data, complex composed data can be shared. This freedom to use any format of data is dangerous and can induce developers to introduce bugs. 147 | 148 | ```elixir 149 | # start an agent with initial state of an empty list 150 | iex(1)> {:ok, agent} = Agent.start_link fn -> [] end 151 | {:ok, #PID<0.135.0>} 152 | 153 | # many data format (i.e., List, Map, Integer, Atom) are 154 | # combined through direct access spread across the entire system 155 | iex(2)> A.update(agent) 156 | iex(3)> B.update(agent) 157 | iex(4)> C.update(agent) 158 | 159 | # state of shared information 160 | iex(5)> D.get(agent) 161 | [:atom_value, %{a: 123}] 162 | ``` 163 | 164 | * __Refactoring:__ Instead of spreading direct access to an ``Agent`` over many places in the code, it is better to refactor this code by centralizing the responsibility for interacting with an ``Agent`` in a single module. This refactoring improves the maintainability by removing duplicated code; it also allows you to limit the accepted format for shared data, reducing bug-proneness. As shown below, the module ``KV.Bucket`` is centralizing the responsibility for interacting with the ``Agent``. Any other place in the code that needs to access shared data must now delegate this action to ``KV.Bucket``. Also, ``KV.Bucket`` now only allows data to be shared in ``Map`` format. 165 | 166 | ```elixir 167 | defmodule KV.Bucket do 168 | use Agent 169 | 170 | @doc """ 171 | Starts a new bucket. 172 | """ 173 | def start_link(_opts) do 174 | Agent.start_link(fn -> %{} end) 175 | end 176 | 177 | @doc """ 178 | Gets a value from the `bucket` by `key`. 179 | """ 180 | def get(bucket, key) do 181 | Agent.get(bucket, &Map.get(&1, key)) 182 | end 183 | 184 | @doc """ 185 | Puts the `value` for the given `key` in the `bucket`. 186 | """ 187 | def put(bucket, key, value) do 188 | Agent.update(bucket, &Map.put(&1, key, value)) 189 | end 190 | end 191 | ``` 192 | 193 | The following are examples of how to delegate access to shared data (provided by an ``Agent``) to ``KV.Bucket``. 194 | 195 | ```elixir 196 | # start an agent through a `KV.Bucket` 197 | iex(1)> {:ok, bucket} = KV.Bucket.start_link(%{}) 198 | {:ok, #PID<0.114.0>} 199 | 200 | # add shared values to the keys `milk` and `beer` 201 | iex(2)> KV.Bucket.put(bucket, "milk", 3) 202 | iex(3)> KV.Bucket.put(bucket, "beer", 7) 203 | 204 | # accessing shared data of specific keys 205 | iex(4)> KV.Bucket.get(bucket, "beer") 206 | 7 207 | iex(5)> KV.Bucket.get(bucket, "milk") 208 | 3 209 | ``` 210 | 211 | These examples are based on code written in Elixir's official documentation. Source: [link][AgentObsessionExample] 212 | 213 | * __Treatments:__ 214 | 215 | * [Generalise a function definition](https://github.com/lucasvegi/Elixir-Refactorings?#generalise-a-function-definition) |> [Moving a definition](https://github.com/lucasvegi/Elixir-Refactorings?#moving-a-definition) |> [Add or remove a parameter](https://github.com/lucasvegi/Elixir-Refactorings?#add-or-remove-a-parameter) 216 | * [Behaviour extraction](https://github.com/lucasvegi/Elixir-Refactorings?#behaviour-extraction) 217 | 218 | [▲ back to Index](#table-of-contents) 219 | ___ 220 | 221 | ### Unsupervised process 222 | 223 | * __Category:__ Design-related smell. 224 | 225 | * __Problem:__ In Elixir, creating a process outside a supervision tree is not a code smell in itself. However, when code creates a large number of long-running processes outside a supervision tree, this can make visibility and monitoring of these processes difficult, preventing developers from fully controlling their applications. 226 | 227 | * __Example:__ The following code example seeks to illustrate a library responsible for maintaining a numerical ``Counter`` through a ``GenServer`` process outside a supervision tree. Multiple counters can be created simultaneously by a client (one process for each counter), making these unsupervised processes difficult to manage. This can cause problems with the initialization, restart, and shutdown of a system. 228 | 229 | ```elixir 230 | defmodule Counter do 231 | use GenServer 232 | 233 | @moduledoc """ 234 | Global counter implemented through a GenServer process 235 | outside a supervision tree. 236 | """ 237 | 238 | @doc """ 239 | Function to create a counter. 240 | initial_value: any integer value. 241 | pid_name: optional parameter to define the process name. 242 | Default is Counter. 243 | """ 244 | def start(initial_value, pid_name \\ __MODULE__) 245 | when is_integer(initial_value) do 246 | GenServer.start(__MODULE__, initial_value, name: pid_name) 247 | end 248 | 249 | @doc """ 250 | Function to get the counter's current value. 251 | pid_name: optional parameter to inform the process name. 252 | Default is Counter. 253 | """ 254 | def get(pid_name \\ __MODULE__) do 255 | GenServer.call(pid_name, :get) 256 | end 257 | 258 | @doc """ 259 | Function to changes the counter's current value. 260 | Returns the updated value. 261 | value: amount to be added to the counter. 262 | pid_name: optional parameter to inform the process name. 263 | Default is Counter. 264 | """ 265 | def bump(value, pid_name \\ __MODULE__) do 266 | GenServer.call(pid_name, {:bump, value}) 267 | get(pid_name) 268 | end 269 | 270 | ## Callbacks 271 | 272 | @impl true 273 | def init(counter) do 274 | {:ok, counter} 275 | end 276 | 277 | @impl true 278 | def handle_call(:get, _from, counter) do 279 | {:reply, counter, counter} 280 | end 281 | 282 | def handle_call({:bump, value}, _from, counter) do 283 | {:reply, counter, counter + value} 284 | end 285 | end 286 | 287 | #...Use examples... 288 | 289 | iex(1)> Counter.start(0) 290 | {:ok, #PID<0.115.0>} 291 | 292 | iex(2)> Counter.get() 293 | 0 294 | 295 | iex(3)> Counter.start(15, C2) 296 | {:ok, #PID<0.120.0>} 297 | 298 | iex(4)> Counter.get(C2) 299 | 15 300 | 301 | iex(5)> Counter.bump(-3, C2) 302 | 12 303 | 304 | iex(6)> Counter.bump(7) 305 | 7 306 | ``` 307 | 308 | * __Refactoring:__ To ensure that clients of a library have full control over their systems, regardless of the number of processes used and the lifetime of each one, all processes must be started inside a supervision tree. As shown below, this code uses a ``Supervisor`` [link][Supervisor] as a supervision tree. When this Elixir application is started, two different counters (``Counter`` and ``C2``) are also started as child processes of the ``Supervisor`` named ``App.Supervisor``. Both are initialized with zero. By means of this supervision tree, it is possible to manage the lifecycle of all child processes (e.g., stopping or restarting each one), improving the visibility of the entire app. 309 | 310 | ```elixir 311 | defmodule SupervisedProcess.Application do 312 | use Application 313 | 314 | @impl true 315 | def start(_type, _args) do 316 | children = [ 317 | # The counters are Supervisor children started via Counter.start(0). 318 | %{ 319 | id: Counter, 320 | start: {Counter, :start, [0]} 321 | }, 322 | %{ 323 | id: C2, 324 | start: {Counter, :start, [0, C2]} 325 | } 326 | ] 327 | 328 | opts = [strategy: :one_for_one, name: App.Supervisor] 329 | Supervisor.start_link(children, opts) 330 | end 331 | end 332 | 333 | #...Use examples... 334 | 335 | iex(1)> Supervisor.count_children(App.Supervisor) 336 | %{active: 2, specs: 2, supervisors: 0, workers: 2} 337 | 338 | iex(2)> Counter.get(Counter) 339 | 0 340 | 341 | iex(3)> Counter.get(C2) 342 | 0 343 | 344 | iex(4)> Counter.bump(7, Counter) 345 | 7 346 | 347 | iex(5)> Supervisor.terminate_child(App.Supervisor, Counter) 348 | iex(6)> Supervisor.count_children(App.Supervisor) 349 | %{active: 1, specs: 2, supervisors: 0, workers: 2} #only one active 350 | 351 | iex(7)> Counter.get(Counter) #Error because it was previously terminated 352 | ** (EXIT) no process: the process is not alive... 353 | 354 | iex(8)> Supervisor.restart_child(App.Supervisor, Counter) 355 | iex(9)> Counter.get(Counter) #after the restart, this process can be accessed again 356 | 0 357 | ``` 358 | 359 | These examples are based on codes written in Elixir's official documentation. Source: [link][UnsupervisedProcessExample] 360 | 361 | * __Treatments:__ 362 | 363 | * [Moving error-handling mechanisms to supervision trees](https://github.com/lucasvegi/Elixir-Refactorings?#moving-error-handling-mechanisms-to-supervision-trees) 364 | * [Moving a definition](https://github.com/lucasvegi/Elixir-Refactorings?#moving-a-definition) 365 | 366 | [▲ back to Index](#table-of-contents) 367 | ___ 368 | 369 | ### Large messages 370 | 371 | * __Category:__ Design-related smell. 372 | 373 | * __Note:__ Formerly known as "Large messages between processes". 374 | 375 | * __Problem:__ In Elixir, processes run in an isolated manner, often concurrently with other. Communication between different processes is performed via message passing. The exchange of messages between processes is not a code smell in itself; however, when processes exchange messages, their contents are copied between them. For this reason, if a huge structure is sent as a message from one process to another, the sender can become blocked, compromising performance. If these large message exchanges occur frequently, the prolonged and frequent blocking of processes can cause a system to behave anomalously. 376 | 377 | * __Example:__ The following code is composed of two modules which will each run in a different process. As the names suggest, the ``Sender`` module has a function responsible for sending messages from one process to another (i.e., ``send_msg/3``). The ``Receiver`` module has a function to create a process to receive messages (i.e., ``create/0``) and another one to handle the received messages (i.e., ``run/0``). If a huge structure, such as a list with 1_000_000 different values, is sent frequently from ``Sender`` to ``Receiver``, the impacts of this smell could be felt. 378 | 379 | ```elixir 380 | defmodule Receiver do 381 | @doc """ 382 | Function for receiving messages from processes. 383 | """ 384 | def run do 385 | receive do 386 | {:msg, msg_received} -> msg_received 387 | {_, _} -> "won't match" 388 | end 389 | end 390 | 391 | @doc """ 392 | Create a process to receive a message. 393 | Messages are received in the run() function of Receiver. 394 | """ 395 | def create do 396 | spawn(Receiver, :run, []) 397 | end 398 | end 399 | ``` 400 | 401 | ```elixir 402 | defmodule Sender do 403 | @doc """ 404 | Function for sending messages between processes. 405 | pid_receiver: message recipient. 406 | msg: messages of any type and size can be sent. 407 | id_msg: used by receiver to decide what to do 408 | when a message arrives. 409 | Default is the atom :msg 410 | """ 411 | def send_msg(pid_receiver, msg, id_msg \\ :msg) do 412 | send(pid_receiver, {id_msg, msg}) 413 | end 414 | end 415 | ``` 416 | 417 | Examples of large messages between processes: 418 | 419 | ```elixir 420 | iex(1)> pid = Receiver.create 421 | #PID<0.144.0> 422 | 423 | #Simulating a message with large content - List with length 1_000_000 424 | iex(2)> msg = %{from: inspect(self()), to: inspect(pid), content: Enum.to_list(1..1_000_000)} 425 | 426 | iex(3)> Sender.send_msg(pid, msg) 427 | {:msg, 428 | %{ 429 | content: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 430 | 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 431 | 39, 40, 41, 42, 43, 44, 45, 46, 47, ...], 432 | from: "#PID<0.105.0>", 433 | to: "#PID<0.144.0>" 434 | }} 435 | ``` 436 | 437 | This example is based on a original code by Samuel Mullen. Source: [link][LargeMessageExample] 438 | 439 | * __Treatments:__ 440 | 441 | * [Defining a subset of a Map](https://github.com/lucasvegi/Elixir-Refactorings?#defining-a-subset-of-a-map) 442 | * [Extract expressions](https://github.com/lucasvegi/Elixir-Refactorings?#extract-expressions) 443 | * [Add a tag to messages](https://github.com/lucasvegi/Elixir-Refactorings?#add-a-tag-to-messages) 444 | 445 | [▲ back to Index](#table-of-contents) 446 | ___ 447 | 448 | ### Unrelated multi-clause function 449 | 450 | * __Category:__ Design-related smell. 451 | 452 | * __Note:__ Formerly known as "Complex multi-clause function". 453 | 454 | * __Problem:__ Using multi-clause functions in Elixir, to group functions of the same name, is not a code smell in itself. However, due to the great flexibility provided by this programming feature, some developers may abuse the number of guard clauses and pattern matches to group _unrelated_ functionality. 455 | 456 | * __Example:__ A recurrent example of abusive use of the multi-clause functions is when we’re trying to mix too much-unrelated business logic into the function definitions. This makes it difficult to read and understand the logic involved in the functions, which may impair code maintainability. Some developers use documentation mechanisms such as ``@doc`` annotations to compensate for poor code readability, but unfortunately, with a multi-clause function, we can only use these annotations once per function name, particularly on the first or header function. As shown next, all other variations of the function need to be documented only with comments, a mechanism that cannot automate tests, leaving the code prone to bugs. 457 | 458 | ```elixir 459 | @doc """ 460 | Update sharp product with 0 or empty count 461 | 462 | ## Examples 463 | 464 | iex> Namespace.Module.update(...) 465 | expected result... 466 | """ 467 | def update(%Product{count: nil, material: material}) 468 | when material in ["metal", "glass"] do 469 | # ... 470 | end 471 | 472 | # update blunt product 473 | def update(%Product{count: count, material: material}) 474 | when count > 0 and material in ["metal", "glass"] do 475 | # ... 476 | end 477 | 478 | # update animal... 479 | def update(%Animal{count: 1, skin: skin}) 480 | when skin in ["fur", "hairy"] do 481 | # ... 482 | end 483 | ``` 484 | 485 | * __Refactoring:__ As shown below, a possible solution to this smell is to break the business rules that are mixed up in a single unrelated multi-clause function in several different simple functions. Each function can have a specific ``@doc``, describing its behavior and parameters received. While this refactoring sounds simple, it can have a lot of impact on the function's current clients, so be careful! 486 | 487 | ```elixir 488 | @doc """ 489 | Update sharp product 490 | 491 | ## Parameter 492 | struct: %Product{...} 493 | 494 | ## Examples 495 | 496 | iex> Namespace.Module.update_sharp_product(%Product{...}) 497 | expected result... 498 | """ 499 | def update_sharp_product(struct) do 500 | # ... 501 | end 502 | 503 | @doc """ 504 | Update blunt product 505 | 506 | ## Parameter 507 | struct: %Product{...} 508 | 509 | ## Examples 510 | 511 | iex> Namespace.Module.update_blunt_product(%Product{...}) 512 | expected result... 513 | """ 514 | def update_blunt_product(struct) do 515 | # ... 516 | end 517 | 518 | @doc """ 519 | Update animal 520 | 521 | ## Parameter 522 | struct: %Animal{...} 523 | 524 | ## Examples 525 | 526 | iex> Namespace.Module.update_animal(%Animal{...}) 527 | expected result... 528 | """ 529 | def update_animal(struct) do 530 | # ... 531 | end 532 | ``` 533 | 534 | This example is based on a original code by Syamil MJ ([@syamilmj][syamilmj]). Source: [link][MultiClauseExample] 535 | 536 | * __Treatments:__ 537 | 538 | * [Rename an identifier](https://github.com/lucasvegi/Elixir-Refactorings?#rename-an-identifier) |> [Function clauses to/from case clauses](https://github.com/lucasvegi/Elixir-Refactorings?#function-clauses-tofrom-case-clauses) 539 | * [Moving a definition](https://github.com/lucasvegi/Elixir-Refactorings?#moving-a-definition) 540 | * [Struct guard to matching](https://github.com/lucasvegi/Elixir-Refactorings?#struct-guard-to-matching) 541 | * [Equality guard to pattern matching](https://github.com/lucasvegi/Elixir-Refactorings?#equality-guard-to-pattern-matching) 542 | * [Simplifying guard sequences](https://github.com/lucasvegi/Elixir-Refactorings?#simplifying-guard-sequences) 543 | * [Converts guards to conditionals](https://github.com/lucasvegi/Elixir-Refactorings?#converts-guards-to-conditionals) 544 | * [Simplifying pattern matching with nested structs](https://github.com/lucasvegi/Elixir-Refactorings?#simplifying-pattern-matching-with-nested-structs) 545 | * [Remove unnecessary calls to length/1](https://github.com/lucasvegi/Elixir-Refactorings?#remove-unnecessary-calls-to-length1) 546 | 547 | [▲ back to Index](#table-of-contents) 548 | ___ 549 | 550 | ### Complex extractions in clauses 551 | 552 | * __Category:__ Design-related smell. 553 | 554 | * __Note:__ This smell was suggested by the community via issues ([#9][Complex-extraction-in-clauses-issue]). 555 | 556 | * __Problem:__ When we use multi-clause functions, it is possible to extract values in the clauses for further usage and for pattern matching/guard checking. This extraction itself does not represent a code smell, but when you have too many clauses or too many arguments, it becomes hard to know which extracted parts are used for pattern/guards and what is used only inside the function body. This smell is related to [Unrelated multi-clause function](#unrelated-multi-clause-function), but with implications of its own. It impairs the code readability in a different way. 557 | 558 | * __Example:__ The following code, although simple, tries to illustrate the occurrence of this code smell. The multi-clause function ``drive/1`` is extracting fields of an ``%User{}`` struct for usage in the clause expression (e.g. ``age``) and for usage in the function body (e.g., ``name``). Ideally, a function should not mix pattern matching extractions for usage in its clauses expressions and also in the function body. 559 | 560 | 561 | ```elixir 562 | def drive(%User{name: name, age: age}) when age >= 18 do 563 | "#{name} can drive" 564 | end 565 | 566 | def drive(%User{name: name, age: age}) when age < 18 do 567 | "#{name} cannot drive" 568 | end 569 | ``` 570 | 571 | While the example is small and looks like a clear code, try to imagine a situation where ``drive/1`` was more complex, having many more clauses, arguments, and extractions. This is the really smelly code! 572 | 573 | * __Refactoring:__ As shown below, a possible solution to this smell is to extract only pattern/guard related variables in the signature once you have many arguments or multiple clauses: 574 | 575 | ```elixir 576 | def drive(%User{age: age} = user) when age >= 18 do 577 | %User{name: name} = user 578 | "#{name} can drive" 579 | end 580 | 581 | def drive(%User{age: age} = user) when age < 18 do 582 | %User{name: name} = user 583 | "#{name} cannot drive" 584 | end 585 | ``` 586 | 587 | This example and the refactoring are proposed by José Valim ([@josevalim][jose-valim]) 588 | 589 | * __Treatments:__ 590 | 591 | * [Simplifying pattern matching with nested structs](https://github.com/lucasvegi/Elixir-Refactorings?#simplifying-pattern-matching-with-nested-structs) 592 | * [Converts guards to conditionals](https://github.com/lucasvegi/Elixir-Refactorings?#converts-guards-to-conditionals) 593 | * [Equality guard to pattern matching](https://github.com/lucasvegi/Elixir-Refactorings?#equality-guard-to-pattern-matching) 594 | * [Struct guard to matching](https://github.com/lucasvegi/Elixir-Refactorings?#struct-guard-to-matching) 595 | * [Remove unnecessary calls to length/1](https://github.com/lucasvegi/Elixir-Refactorings?#remove-unnecessary-calls-to-length1) 596 | * [Function clauses to/from case clauses](https://github.com/lucasvegi/Elixir-Refactorings?#function-clauses-tofrom-case-clauses) 597 | * [Introduce a temporary duplicate definition](https://github.com/lucasvegi/Elixir-Refactorings?#introduce-a-temporary-duplicate-definition) |> [Temporary variable elimination](https://github.com/lucasvegi/Elixir-Refactorings?#temporary-variable-elimination) 598 | 599 | [▲ back to Index](#table-of-contents) 600 | ___ 601 | 602 | ### Using exceptions for control-flow 603 | 604 | * __Category:__ Design-related smell. 605 | 606 | * __Note:__ Formerly known as "Exceptions for control-flow". 607 | 608 | * __Problem:__ This smell refers to code that forces developers to handle exceptions for control-flow. Exception handling itself does not represent a code smell, but this should not be the only alternative available to developers to handle an error in client code. When developers have no freedom to decide if an error is exceptional or not, this is considered a code smell. 609 | 610 | * __Example:__ An example of this code smell, as shown below, is when a library (e.g. ``MyModule``) forces its clients to use ``try .. rescue`` statements to capture and evaluate errors. This library does not allow developers to decide if an error is exceptional or not in their applications. 611 | 612 | ```elixir 613 | defmodule MyModule do 614 | def janky_function(value) do 615 | if is_integer(value) do 616 | #... 617 | "Result..." 618 | else 619 | raise RuntimeError, message: "invalid argument. Is not integer!" 620 | end 621 | end 622 | end 623 | ``` 624 | 625 | ```elixir 626 | defmodule Client do 627 | 628 | # Client forced to use exceptions for control-flow. 629 | def foo(arg) do 630 | try do 631 | value = MyModule.janky_function(arg) 632 | "All good! #{value}." 633 | rescue 634 | e in RuntimeError -> 635 | reason = e.message 636 | "Uh oh! #{reason}." 637 | end 638 | end 639 | 640 | end 641 | 642 | #...Use examples... 643 | 644 | iex(1)> Client.foo(1) 645 | "All good! Result...." 646 | 647 | iex(2)> Client.foo("lucas") 648 | "Uh oh! invalid argument. Is not integer!." 649 | ``` 650 | 651 | * __Refactoring:__ Library authors should guarantee that clients are not required to use exceptions for control-flow in their applications. As shown below, this can be done by refactoring the library ``MyModule``, providing two versions of the function that forces clients to use exceptions for control-flow (e.g., ``janky_function``). 1) a version with the raised exceptions should have the same name as the smelly one, but with a trailing ``!`` (i.e., ``janky_function!``); 2) Another version, without raised exceptions, should have a name identical to the original version (i.e., ``janky_function``), and should return the result wrapped in a tuple. 652 | 653 | ```elixir 654 | defmodule MyModule do 655 | @moduledoc """ 656 | Refactored library 657 | """ 658 | 659 | @doc """ 660 | Refactored version without exceptions for control-flow. 661 | """ 662 | def janky_function(value) do 663 | if is_integer(value) do 664 | #... 665 | {:ok, "Result..."} 666 | else 667 | {:error, "invalid argument. Is not integer!"} 668 | end 669 | end 670 | 671 | def janky_function!(value) do 672 | case janky_function(value) do 673 | {:ok, result} -> 674 | result 675 | {:error, message} -> 676 | raise RuntimeError, message: message 677 | end 678 | end 679 | end 680 | ``` 681 | 682 | This refactoring gives clients more freedom to decide how to proceed in the event of errors, defining what is exceptional or not in different situations. As shown next, when an error is not exceptional, clients can use specific control-flow structures, such as the ``case`` statement along with pattern matching. 683 | 684 | ```elixir 685 | defmodule Client do 686 | 687 | # Clients now can also choose to use control-flow structures 688 | # for control-flow when an error is not exceptional. 689 | def foo(arg) do 690 | case MyModule.janky_function(arg) do 691 | {:ok, value} -> "All good! #{value}." 692 | {:error, reason} -> "Uh oh! #{reason}." 693 | end 694 | end 695 | 696 | end 697 | 698 | #...Use examples... 699 | 700 | iex(1)> Client.foo(1) 701 | "All good! Result...." 702 | 703 | iex(2)> Client.foo("lucas") 704 | "Uh oh! invalid argument. Is not integer!." 705 | ``` 706 | 707 | This example is based on code written by Tim Austin [neenjaw][neenjaw] and Angelika Tyborska [angelikatyborska][angelikatyborska]. Source: [link][ExceptionsForControlFlowExamples] 708 | 709 | * __Treatments:__ 710 | 711 | * [Rename an identifier](https://github.com/lucasvegi/Elixir-Refactorings?#rename-an-identifier) |> [Introduce a temporary duplicate definition](https://github.com/lucasvegi/Elixir-Refactorings?#introduce-a-temporary-duplicate-definition) |> [Folding against a function definition](https://github.com/lucasvegi/Elixir-Refactorings?#folding-against-a-function-definition) 712 | * [Introduce processes](https://github.com/lucasvegi/Elixir-Refactorings?#introduce-processes) |> [Moving error-handling mechanisms to supervision trees](https://github.com/lucasvegi/Elixir-Refactorings?#moving-error-handling-mechanisms-to-supervision-trees) 713 | 714 | [▲ back to Index](#table-of-contents) 715 | ___ 716 | 717 | ### Untested polymorphic behaviors 718 | 719 | * __Category:__ Design-related smell. 720 | 721 | * __Problem:__ This code smell refers to functions that have protocol-dependent parameters and are therefore polymorphic. A polymorphic function itself does not represent a code smell, but some developers implement these generic functions without accompanying guard clauses, allowing to pass parameters that do not implement the required protocol or that have no meaning. 722 | 723 | * __Example:__ An instance of this code smell happens when a function uses ``to_string()`` to convert data received by parameter. The function ``to_string()`` uses the protocol ``String.Chars`` for conversions. Many Elixir data types (e.g., ``BitString``, ``Integer``, ``Float``, ``URI``) implement this protocol. However, as shown below, other Elixir data types (e.g., ``Map``) do not implement it and can cause an error in ``dasherize/1`` function. Depending on the situation, this behavior can be desired or not. Besides that, it may not make sense to dasherize a ``URI`` or a number as shown next. 724 | 725 | ```elixir 726 | defmodule CodeSmells do 727 | def dasherize(data) do 728 | to_string(data) 729 | |> String.replace("_", "-") 730 | end 731 | end 732 | 733 | #...Use examples... 734 | 735 | iex(1)> CodeSmells.dasherize("Lucas_Vegi") 736 | "Lucas-Vegi" 737 | 738 | iex(2)> CodeSmells.dasherize(10) #<= Makes sense? 739 | "10" 740 | 741 | iex(3)> CodeSmells.dasherize(URI.parse("http://www.code_smells.com")) #<= Makes sense? 742 | "http://www.code-smells.com" 743 | 744 | iex(4)> CodeSmells.dasherize(%{last_name: "vegi", first_name: "lucas"}) 745 | ** (Protocol.UndefinedError) protocol String.Chars not implemented 746 | for %{first_name: "lucas", last_name: "vegi"} of type Map 747 | ``` 748 | 749 | * __Refactoring:__ There are two main alternatives to improve code affected by this smell. __1)__ You can either remove the protocol use (i.e., ``to_string/1``), by adding multi-clauses on ``dasherize/1`` or just remove it; or __2)__ You can document that ``dasherize/1`` uses the protocol ``String.Chars`` for conversions, showing its consequences. As shown next, we refactored using the first alternative, removing the protocol and restricting ``dasherize/1`` parameter only to desired data types (i.e., ``BitString`` and ``Atom``). Besides that, we use ``@doc`` to validate ``dasherize/1`` for desired inputs and to document the behavior to some types that we think don't make sense for the function (e.g., ``Integer`` and ``URI``). 750 | 751 | ```elixir 752 | defmodule CodeSmells do 753 | @doc """ 754 | Function that converts underscores to dashes. 755 | 756 | ## Parameter 757 | data: only BitString and Atom are supported. 758 | 759 | ## Examples 760 | 761 | iex> CodeSmells.dasherize(:lucas_vegi) 762 | "lucas-vegi" 763 | 764 | iex> CodeSmells.dasherize("Lucas_Vegi") 765 | "Lucas-Vegi" 766 | 767 | iex> CodeSmells.dasherize(%{last_name: "vegi", first_name: "lucas"}) 768 | ** (FunctionClauseError) no function clause matching in CodeSmells.dasherize/1 769 | 770 | iex> CodeSmells.dasherize(URI.parse("http://www.code_smells.com")) 771 | ** (FunctionClauseError) no function clause matching in CodeSmells.dasherize/1 772 | 773 | iex> CodeSmells.dasherize(10) 774 | ** (FunctionClauseError) no function clause matching in CodeSmells.dasherize/1 775 | """ 776 | def dasherize(data) when is_atom(data) do 777 | dasherize(Atom.to_string(data)) 778 | end 779 | 780 | def dasherize(data) when is_binary(data) do 781 | String.replace(data, "_", "-") 782 | end 783 | end 784 | 785 | #...Use examples... 786 | 787 | iex(1)> CodeSmells.dasherize(:lucas_vegi) 788 | "lucas-vegi" 789 | 790 | iex(2)> CodeSmells.dasherize("Lucas_Vegi") 791 | "Lucas-Vegi" 792 | 793 | iex(3)> CodeSmells.dasherize(10) 794 | ** (FunctionClauseError) no function clause matching in CodeSmells.dasherize/1 795 | ``` 796 | 797 | This example is based on code written by José Valim ([@josevalim][jose-valim]). Source: [link][JoseValimExamples] 798 | 799 | * __Treatments:__ 800 | 801 | * [Introduce overloading](https://github.com/lucasvegi/Elixir-Refactorings?#introduce-overloading) |> [Folding against a function definition](https://github.com/lucasvegi/Elixir-Refactorings?#folding-against-a-function-definition) 802 | * [Typing parameters and return values](https://github.com/lucasvegi/Elixir-Refactorings?#typing-parameters-and-return-values) 803 | 804 | [▲ back to Index](#table-of-contents) 805 | 806 | ___ 807 | 808 | ### Code organization by process 809 | 810 | * __Category:__ Design-related smell. 811 | 812 | * __Problem:__ This smell refers to code that is unnecessarily organized by processes. A process itself does not represent a code smell, but it should only be used to model runtime properties (e.g., concurrency, access to shared resources, event scheduling). When a process is used for code organization, it can create bottlenecks in the system. 813 | 814 | * __Example:__ An example of this code smell, as shown below, is a library that implements arithmetic operations (e.g., add, subtract) by means of a ``GenSever`` process[link][GenServer]. If the number of calls to this single process grows, this code organization can compromise the system performance, therefore becoming a bottleneck. 815 | 816 | ```elixir 817 | defmodule Calculator do 818 | use GenServer 819 | 820 | @moduledoc """ 821 | Calculator that performs two basic arithmetic operations. 822 | This code is unnecessarily organized by a GenServer process. 823 | """ 824 | 825 | @doc """ 826 | Function to perform the sum of two values. 827 | """ 828 | def add(a, b, pid) do 829 | GenServer.call(pid, {:add, a, b}) 830 | end 831 | 832 | @doc """ 833 | Function to perform subtraction of two values. 834 | """ 835 | def subtract(a, b, pid) do 836 | GenServer.call(pid, {:subtract, a, b}) 837 | end 838 | 839 | def init(init_arg) do 840 | {:ok, init_arg} 841 | end 842 | 843 | def handle_call({:add, a, b}, _from, state) do 844 | {:reply, a + b, state} 845 | end 846 | 847 | def handle_call({:subtract, a, b}, _from, state) do 848 | {:reply, a - b, state} 849 | end 850 | end 851 | 852 | # Start a generic server process 853 | iex(1)> {:ok, pid} = GenServer.start_link(Calculator, :init) 854 | {:ok, #PID<0.132.0>} 855 | 856 | #...Use examples... 857 | iex(2)> Calculator.add(1, 5, pid) 858 | 6 859 | 860 | iex(3)> Calculator.subtract(2, 3, pid) 861 | -1 862 | ``` 863 | 864 | * __Refactoring:__ In Elixir, as shown next, code organization must be done only by modules and functions. Whenever possible, a library should not impose specific behavior (such as parallelization) on its clients. It is better to delegate this behavioral decision to the developers of clients, thus increasing the potential for code reuse of a library. 865 | 866 | ```elixir 867 | defmodule Calculator do 868 | def add(a, b) do 869 | a + b 870 | end 871 | 872 | def subtract(a, b) do 873 | a - b 874 | end 875 | end 876 | 877 | #...Use examples... 878 | 879 | iex(1)> Calculator.add(1, 5) 880 | 6 881 | 882 | iex(2)> Calculator.subtract(2, 3) 883 | -1 884 | ``` 885 | 886 | This example is based on code provided in Elixir's official documentation. Source: [link][CodeOrganizationByProcessExample] 887 | 888 | * __Treatments:__ 889 | 890 | * [Remove processes](https://github.com/lucasvegi/Elixir-Refactorings?#remove-processes) |> [Remove dead code](https://github.com/lucasvegi/Elixir-Refactorings?#remove-dead-code) 891 | 892 | [▲ back to Index](#table-of-contents) 893 | ___ 894 | 895 | ### Large code generation by macros 896 | 897 | * __Category:__ Design-related smell. 898 | 899 | * __Note:__ This smell was suggested by the community via issues ([#13][Large-code-generation-issue]). 900 | 901 | * __Problem:__ This code smell is related to ``macros`` that generate too much code. When a ``macro`` provides a large code generation, it impacts how the compiler or the runtime works. The reason for this is that Elixir may have to expand, compile, and execute a code multiple times, which will make compilation slower. 902 | 903 | * __Example:__ The code shown below is an example of this smell. Imagine you are defining a router for a web application, where you could have macros like ``get/2``. On every invocation of the macro, which can be hundreds, the code inside ``get/2`` will be expanded and compiled, which can generate a large volume of code in total. 904 | 905 | ```elixir 906 | defmodule Routes do 907 | ... 908 | 909 | defmacro get(route, handler) do 910 | quote do 911 | route = unquote(route) 912 | handler = unquote(handler) 913 | 914 | if not is_binary(route) do 915 | raise ArgumentError, "route must be a binary" 916 | end 917 | 918 | if not is_atom(handler) do 919 | raise ArgumentError, "route must be a module" 920 | end 921 | 922 | @store_route_for_compilation {route, handler} 923 | end 924 | end 925 | end 926 | ``` 927 | 928 | * __Refactoring:__ To remove this code smell, the developer must simplify the ``macro``, delegating to other functions part of its work. As shown below, by encapsulating in the function ``__define__/3`` the functionality pre-existing inside the ``quote``, we reduce the code that is expanded and compiled on every invocation of the ``macro``, and instead we dispatch to a function to do the bulk of the work. 929 | 930 | ```elixir 931 | defmodule Routes do 932 | ... 933 | 934 | defmacro get(route, handler) do 935 | quote do 936 | Routes.__define__(__MODULE__, unquote(route), unquote(handler)) 937 | end 938 | end 939 | 940 | def __define__(module, route, handler) do 941 | 942 | if not is_binary(route) do 943 | raise ArgumentError, "route must be a binary" 944 | end 945 | 946 | if not is_atom(handler) do 947 | raise ArgumentError, "route must be a module" 948 | end 949 | 950 | Module.put_attribute(module, :store_route_for_compilation, {route, handler}) 951 | end 952 | end 953 | ``` 954 | 955 | This example and the refactoring are proposed by José Valim ([@josevalim][jose-valim]) 956 | 957 | * __Treatments:__ 958 | 959 | * [Extract function](https://github.com/lucasvegi/Elixir-Refactorings?#extract-function) |> [Moving a definition](https://github.com/lucasvegi/Elixir-Refactorings?#moving-a-definition) 960 | 961 | [▲ back to Index](#table-of-contents) 962 | ___ 963 | 964 | ### Data manipulation by migration 965 | 966 | * __Category:__ Design-related smell. 967 | 968 | * __Problem:__ This code smell refers to modules that perform both data and structural changes in a database schema via ``Ecto.Migration``[link][Migration]. Migrations must be used exclusively to modify a database schema over time (e.g., by including or excluding columns and tables). When this responsibility is mixed with data manipulation code, the module becomes less cohesive, more difficult to test, and therefore more prone to bugs. 969 | 970 | * __Example:__ An example of this code smell is when an ``Ecto.Migration`` is used simultaneously to alter a table, adding a new column to it, and also to update all pre-existing data in that table, assigning a value to this new column. As shown below, in addition to adding the ``is_custom_shop`` column in the ``guitars`` table, this ``Ecto.Migration`` changes the value of this column for some specific guitar models. 971 | 972 | ```elixir 973 | defmodule GuitarStore.Repo.Migrations.AddIsCustomShopToGuitars do 974 | use Ecto.Migration 975 | 976 | import Ecto.Query 977 | alias GuitarStore.Inventory.Guitar 978 | alias GuitarStore.Repo 979 | 980 | @doc """ 981 | A function that modifies the structure of table "guitars", 982 | adding column "is_custom_shop" to it. By default, all data 983 | pre-stored in this table will have the value false stored 984 | in this new column. 985 | 986 | Also, this function updates the "is_custom_shop" column value 987 | of some guitar models to true. 988 | """ 989 | def change do 990 | alter table("guitars") do 991 | add :is_custom_shop, :boolean, default: false 992 | end 993 | create index("guitars", ["is_custom_shop"]) 994 | 995 | custom_shop_entries() 996 | |> Enum.map(&update_guitars/1) 997 | end 998 | 999 | @doc """ 1000 | A function that updates values of column "is_custom_shop" to true. 1001 | """ 1002 | defp update_guitars({make, model, year}) do 1003 | from(g in Guitar, 1004 | where: g.make == ^make and g.model == ^model and g.year == ^year, 1005 | select: g 1006 | ) 1007 | |> Repo.update_all(set: [is_custom_shop: true]) 1008 | end 1009 | 1010 | @doc """ 1011 | Function that defines which guitar models that need to have the values 1012 | of the "is_custom_shop" column updated to true. 1013 | """ 1014 | defp custom_shop_entries() do 1015 | [ 1016 | {"Gibson", "SG", 1999}, 1017 | {"Fender", "Telecaster", 2020} 1018 | ] 1019 | end 1020 | end 1021 | ``` 1022 | 1023 | You can run this smelly migration above by going to the root of your project and typing the next command via console: 1024 | 1025 | ```elixir 1026 | mix ecto.migrate 1027 | ``` 1028 | 1029 | * __Refactoring:__ To remove this code smell, it is necessary to separate the data manipulation in a ``mix task`` [link][MixTask] different from the module that performs the structural changes in the database via ``Ecto.Migration``. This separation of responsibilities is a best practice for increasing code testability. As shown below, the module ``AddIsCustomShopToGuitars`` now use ``Ecto.Migration`` only to perform structural changes in the database schema: 1030 | 1031 | ```elixir 1032 | defmodule GuitarStore.Repo.Migrations.AddIsCustomShopToGuitars do 1033 | use Ecto.Migration 1034 | 1035 | @doc """ 1036 | A function that modifies the structure of table "guitars", 1037 | adding column "is_custom_shop" to it. By default, all data 1038 | pre-stored in this table will have the value false stored 1039 | in this new column. 1040 | """ 1041 | def change do 1042 | alter table("guitars") do 1043 | add :is_custom_shop, :boolean, default: false 1044 | end 1045 | 1046 | create index("guitars", ["is_custom_shop"]) 1047 | end 1048 | end 1049 | ``` 1050 | 1051 | Furthermore, the new mix task ``PopulateIsCustomShop``, shown next, has only the responsibility to perform data manipulation, thus improving testability: 1052 | 1053 | ```elixir 1054 | defmodule Mix.Tasks.PopulateIsCustomShop do 1055 | @shortdoc "Populates is_custom_shop column" 1056 | 1057 | use Mix.Task 1058 | 1059 | import Ecto.Query 1060 | alias GuitarStore.Inventory.Guitar 1061 | alias GuitarStore.Repo 1062 | 1063 | @requirements ["app.start"] 1064 | 1065 | def run(_) do 1066 | custom_shop_entries() 1067 | |> Enum.map(&update_guitars/1) 1068 | end 1069 | 1070 | defp update_guitars({make, model, year}) do 1071 | from(g in Guitar, 1072 | where: g.make == ^make and g.model == ^model and g.year == ^year, 1073 | select: g 1074 | ) 1075 | |> Repo.update_all(set: [is_custom_shop: true]) 1076 | end 1077 | 1078 | defp custom_shop_entries() do 1079 | [ 1080 | {"Gibson", "SG", 1999}, 1081 | {"Fender", "Telecaster", 2020} 1082 | ] 1083 | end 1084 | end 1085 | ``` 1086 | 1087 | You can run this ``mix task`` above by typing the next command via console: 1088 | 1089 | ```elixir 1090 | mix populate_is_custom_shop 1091 | ``` 1092 | 1093 | This example is based on code originally written by Carlos Souza. Source: [link][DataManipulationByMigrationExamples] 1094 | 1095 | * __Treatments:__ 1096 | 1097 | * [Splitting a large module](https://github.com/lucasvegi/Elixir-Refactorings?#splitting-a-large-module) |> [Rename an identifier](https://github.com/lucasvegi/Elixir-Refactorings?#rename-an-identifier) |> [Extract function](https://github.com/lucasvegi/Elixir-Refactorings?#extract-function) |> [Moving a definition](https://github.com/lucasvegi/Elixir-Refactorings?#moving-a-definition) |> [Remove dead code](https://github.com/lucasvegi/Elixir-Refactorings?#remove-dead-code) 1098 | * [Simplifying Ecto schema fields validation](https://github.com/lucasvegi/Elixir-Refactorings?#simplifying-ecto-schema-fields-validation) 1099 | * [Pipeline for database transactions](https://github.com/lucasvegi/Elixir-Refactorings?#pipeline-for-database-transactions) 1100 | 1101 | [▲ back to Index](#table-of-contents) 1102 | ___ 1103 | 1104 | ### Using App Configuration for libraries 1105 | 1106 | * __Category:__ Design-related smells. 1107 | 1108 | * __Note:__ Formerly known as "App configuration for code libs". 1109 | 1110 | * __Problem:__ The ``Application Environment`` [link][ApplicationEnvironment] is a mechanism that can be used to parameterize 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 a code smell 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 its 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. 1111 | 1112 | * __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: 1113 | 1114 | ```elixir 1115 | import Config 1116 | 1117 | config :app_config, 1118 | parts: 3 1119 | 1120 | import_config "#{config_env()}.exs" 1121 | ``` 1122 | 1123 | One of the functions implemented by the ``DashSplitter`` library is ``split/1``. This function has the purpose of separating a string received via 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: 1124 | 1125 | ```elixir 1126 | defmodule DashSplitter do 1127 | def split(string) when is_binary(string) do 1128 | parts = Application.fetch_env!(:app_config, :parts) # <= retrieve parameterized value 1129 | String.split(string, "-", parts: parts) # <= parts: 3 1130 | end 1131 | end 1132 | ``` 1133 | 1134 | Due to this type of parameterized value used by the ``DashSplitter`` library, all applications dependent on it can only use the ``split/1`` function with identical behavior in relation to 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: 1135 | 1136 | ```elixir 1137 | iex(1)> DashSplitter.split("Lucas-Francisco-Vegi") 1138 | ["Lucas", "Francisco", "Vegi"] 1139 | 1140 | iex(2)> DashSplitter.split("Lucas-Francisco-da-Matta-Vegi") 1141 | ["Lucas", "Francisco", "da-Matta-Vegi"] 1142 | ``` 1143 | 1144 | * __Refactoring:__ To remove this code smell 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: 1145 | 1146 | ```elixir 1147 | defmodule DashSplitter do 1148 | def split(string, opts \\ []) when is_binary(string) and is_list(opts) do 1149 | parts = Keyword.get(opts, :parts, 2) # <= default config of parts == 2 1150 | String.split(string, "-", parts: parts) 1151 | end 1152 | end 1153 | 1154 | #...Use examples... 1155 | 1156 | iex(1)> DashSplitter.split("Lucas-Francisco-da-Matta-Vegi", [parts: 5]) 1157 | ["Lucas", "Francisco", "da", "Matta", "Vegi"] 1158 | 1159 | iex(2)> DashSplitter.split("Lucas-Francisco-da-Matta-Vegi") #<= default config is used! 1160 | ["Lucas", "Francisco-da-Matta-Vegi"] 1161 | ``` 1162 | 1163 | These examples are based on code provided in Elixir's official documentation. Source: [link][AppConfigurationForCodeLibsExample] 1164 | 1165 | * __Treatments:__ 1166 | 1167 | * [Add or remove a parameter](https://github.com/lucasvegi/Elixir-Refactorings?#add-or-remove-a-parameter) |> [Typing parameters and return values](https://github.com/lucasvegi/Elixir-Refactorings?#typing-parameters-and-return-values) 1168 | 1169 | [▲ back to Index](#table-of-contents) 1170 | ___ 1171 | 1172 | ### Compile-time global configuration 1173 | 1174 | * __Category:__ Design-related smells. 1175 | 1176 | * __Note:__ Formerly known as "Compile-time app configuration". 1177 | 1178 | * __Problem:__ As explained in the description of [Using App Configuration for libraries](#using-app-configuration-for-libraries), the ``Application Environment`` can be used to parameterize values in an Elixir system. Although it is not a good practice to use this mechanism in the implementation of libraries, sometimes this can be unavoidable. If these parameterized values are assigned to ``module attributes``, it can be especially problematic. As ``module attribute`` values are defined at compile-time, when trying to assign ``Application Environment`` values to these attributes, warnings or errors can be triggered by Elixir. This happens because, when defining module attributes at compile time, the ``Application Environment`` is not yet available in memory. 1179 | 1180 | * __Example:__ The ``DashSplitter`` module represents a library. This module has an attribute ``@parts`` that has its constant value defined at compile-time by calling ``Application.fetch_env!/2``. The ``split/1`` function, implemented by this library, has the purpose of separating a string received via 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 by the module attribute ``@parts``, as shown next: 1181 | 1182 | ```elixir 1183 | defmodule DashSplitter do 1184 | @parts Application.fetch_env!(:app_config, :parts) # <= define module attribute 1185 | # at compile-time 1186 | def split(string) when is_binary(string) do 1187 | String.split(string, "-", parts: @parts) #<= reading from a module attribute 1188 | end 1189 | 1190 | end 1191 | ``` 1192 | 1193 | Due to this compile-time configuration based on the ``Application Environment`` mechanism, Elixir can raise warnings or errors, as shown next, during compilation: 1194 | 1195 | ```elixir 1196 | warning: Application.fetch_env!/2 is discouraged in the module body, 1197 | use Application.compile_env/3 instead... 1198 | 1199 | ** (ArgumentError) could not fetch application environment :parts 1200 | for application :app_config because the application was not loaded nor 1201 | configured 1202 | ``` 1203 | 1204 | * __Refactoring:__ To remove this code smell, when it is really unavoidable to use the ``Application Environment`` mechanism to configure library functions, this should be done at runtime and not during compilation. That is, instead of calling ``Application.fetch_env!(:app_config, :parts)`` at compile-time to set ``@parts``, this function must be called at runtime within ``split/1``. This will mitigate the risk that ``Application Environment`` is not yet available in memory when it is necessary to use it. Another possible refactoring, as shown below, is to replace the use of the ``Application.fetch_env!/2`` function to define ``@parts``, with the ``Application.compile_env/3``. The third parameter of ``Application.compile_env/3`` defines a default value that is returned whenever that ``Application Environment`` is not available in memory during the definition of ``@parts``. This prevents Elixir from raising an error at compile-time: 1205 | 1206 | ```elixir 1207 | defmodule DashSplitter do 1208 | @parts Application.compile_env(:app_config, :parts, 3) # <= default value 3 prevents an error! 1209 | 1210 | def split(string) when is_binary(string) do 1211 | String.split(string, "-", parts: @parts) #<= reading from a module attribute 1212 | end 1213 | 1214 | end 1215 | ``` 1216 | 1217 | These examples are based on code provided in Elixir's official documentation. Source: [link][AppConfigurationForCodeLibsExample] 1218 | 1219 | * __Treatments:__ 1220 | 1221 | * [Extract constant](https://github.com/lucasvegi/Elixir-Refactorings?#extract-constant) 1222 | * [Introduce a temporary duplicate definition](https://github.com/lucasvegi/Elixir-Refactorings?#introduce-a-temporary-duplicate-definition) 1223 | * [Folding against a function definition](https://github.com/lucasvegi/Elixir-Refactorings?#folding-against-a-function-definition) |> [Remove dead code](https://github.com/lucasvegi/Elixir-Refactorings?#remove-dead-code) 1224 | 1225 | * __Remark:__ This code smell can be detected by [Credo][Credo], a static code analysis tool. During its checks, Credo raises this [warning][CredoWarningApplicationConfigInModuleAttribute] when this smell is found. 1226 | 1227 | [▲ back to Index](#table-of-contents) 1228 | ___ 1229 | 1230 | ### "Use" instead of "import" 1231 | 1232 | * __Category:__ Design-related smells. 1233 | 1234 | * __Note:__ Formerly known as "Dependency with "use" when an "import" is enough". 1235 | 1236 | * __Problem:__ Elixir has mechanisms such as ``import``, ``alias``, and ``use`` to establish dependencies between modules. Establishing dependencies allows a module to call functions from other modules, facilitating code reuse. A code implemented with these mechanisms does not characterize a smell by itself; however, while the ``import`` and ``alias`` directives have lexical scope and only facilitate that a module to use functions of another, the ``use`` directive has a broader scope, something that can be problematic. The ``use`` directive allows a module to inject any type of code into another, including propagating dependencies. In this way, using the ``use`` directive makes code readability worse, because to understand exactly what will happen when it references a module, it is necessary to have knowledge of the internal details of the referenced module. 1237 | 1238 | * __Example:__ The code shown below is an example of this smell. Three different modules were defined -- ``ModuleA``, ``Library``, and ``ClientApp``. ``ClientApp`` is reusing code from the ``Library`` via the ``use`` directive, but is unaware of its internal details. Therefore, when ``Library`` is referenced by ``ClientApp``, it injects into ``ClientApp`` all the content present in its ``__using__/1`` macro. Due to the decreased readability of the code and the lack of knowledge of the internal details of the ``Library``, ``ClientApp`` defines a local function ``foo/0``. This will generate a conflict as ``ModuleA`` also has a function ``foo/0``; when ``ClientApp`` referenced ``Library`` via the ``use`` directive, it has a dependency for ``ModuleA`` propagated to itself: 1239 | 1240 | ```elixir 1241 | defmodule ModuleA do 1242 | def foo do 1243 | "From Module A" 1244 | end 1245 | end 1246 | ``` 1247 | 1248 | ```elixir 1249 | defmodule Library do 1250 | defmacro __using__(_opts) do 1251 | quote do 1252 | import ModuleA # <= propagating dependencies! 1253 | 1254 | def from_lib do 1255 | "From Library" 1256 | end 1257 | end 1258 | end 1259 | 1260 | def from_lib do 1261 | "From Library" 1262 | end 1263 | end 1264 | ``` 1265 | 1266 | ```elixir 1267 | defmodule ClientApp do 1268 | use Library 1269 | 1270 | def foo do 1271 | "Local function from client app" 1272 | end 1273 | 1274 | def from_client_app do 1275 | from_lib() <> " - " <> foo() 1276 | end 1277 | 1278 | end 1279 | ``` 1280 | 1281 | When we try to compile ``ClientApp``, Elixir will detect the conflict and throw the following error: 1282 | 1283 | ```elixir 1284 | iex(1)> c("client_app.ex") 1285 | 1286 | ** (CompileError) client_app.ex:4: imported ModuleA.foo/0 conflicts with local function 1287 | ``` 1288 | 1289 | * __Refactoring:__ To remove this code smell, it may be possible to replace ``use`` with ``alias`` or ``import`` when creating a dependency between an application and a library. This will make code behavior clearer, due to improved readability. In the following code, ``ClientApp`` was refactored in this way, and with that, the conflict as previously shown no longer exists: 1290 | 1291 | ```elixir 1292 | defmodule ClientApp do 1293 | import Library 1294 | 1295 | def foo do 1296 | "Local function from client app" 1297 | end 1298 | 1299 | def from_client_app do 1300 | from_lib() <> " - " <> foo() 1301 | end 1302 | 1303 | end 1304 | 1305 | #...Uses example... 1306 | 1307 | iex(1)> ClientApp.from_client_app() 1308 | "From Library - Local function from client app" 1309 | ``` 1310 | 1311 | These examples are based on code provided in Elixir's official documentation. Source: [link][DependencyWithUseExample] 1312 | 1313 | * __Treatments:__ 1314 | 1315 | * [Introduce import](https://github.com/lucasvegi/Elixir-Refactorings?#introduce-import) |> [Remove dead code](https://github.com/lucasvegi/Elixir-Refactorings?#remove-dead-code) 1316 | * [Alias expansion](https://github.com/lucasvegi/Elixir-Refactorings?#alias-expansion) 1317 | * [Remove import attributes](https://github.com/lucasvegi/Elixir-Refactorings?#remove-import-attributes) 1318 | 1319 | [▲ back to Index](#table-of-contents) 1320 | 1321 | ## Low-level concerns smells 1322 | 1323 | Low-level concerns smells are more simple than design-related smells and affect a small part of the code. Next, all 9 different smells classified as low-level concerns are explained and exemplified: 1324 | 1325 | ### Working with invalid data 1326 | 1327 | * __Category:__ Low-level concerns smells. 1328 | 1329 | * __Problem:__ This code smell refers to a function that does not validate its parameters' types and therefore can produce internal non-predicted behavior. When an error is raised inside a function due to an invalid parameter value, this can confuse the developers and make it harder to locate and fix the error. 1330 | 1331 | * __Example:__ An example of this code smell is when a function receives an invalid parameter and then passes it to a function from a third-party library. This will cause an error (raised deep inside the library function), which may be confusing for the developer who is working with invalid data. As shown next, the function ``foo/1`` is a client of a third-party library and doesn't validate its parameters at the boundary. In this way, it is possible that invalid data will be passed from ``foo/1`` to the library, causing a mysterious error. 1332 | 1333 | ```elixir 1334 | defmodule MyApp do 1335 | alias ThirdPartyLibrary, as: Library 1336 | 1337 | def foo(invalid_data) do 1338 | #...some code... 1339 | Library.sum(1, invalid_data) 1340 | #...some code... 1341 | end 1342 | end 1343 | 1344 | #...Use examples... 1345 | 1346 | # with valid data is ok 1347 | iex(1)> MyApp.foo(2) 1348 | 3 1349 | 1350 | #with invalid data cause a confusing error deep inside 1351 | iex(2)> MyApp.foo("Lucas") 1352 | ** (ArithmeticError) bad argument in arithmetic expression: 1 + "Lucas" 1353 | :erlang.+(1, "Lucas") 1354 | library.ex:3: ThirdPartyLibrary.sum/2 1355 | ``` 1356 | 1357 | * __Refactoring:__ To remove this code smell, client code must validate input parameters at the boundary with the user, via guard clauses or pattern matching. This will prevent errors from occurring deeply, making them easier to understand. This refactoring will also allow libraries to be implemented without worrying about creating internal protection mechanisms. The next code illustrates the refactoring of ``foo/1``, removing this smell: 1358 | 1359 | ```elixir 1360 | defmodule MyApp do 1361 | alias ThirdPartyLibrary, as: Library 1362 | 1363 | def foo(data) when is_integer(data) do 1364 | #...some code... 1365 | Library.sum(1, data) 1366 | #...some code... 1367 | end 1368 | end 1369 | 1370 | #...Use examples... 1371 | 1372 | #with valid data is ok 1373 | iex(1)> MyApp.foo(2) 1374 | 3 1375 | 1376 | # with invalid data errors are easy to locate and fix 1377 | iex(2)> MyApp.foo("Lucas") 1378 | ** (FunctionClauseError) no function clause matching in MyApp.foo/1 1379 | 1380 | The following arguments were given to MyApp.foo/1: 1381 | 1382 | # 1 1383 | "Lucas" 1384 | 1385 | my_app.ex:6: MyApp.foo/1 1386 | ``` 1387 | 1388 | This example is based on code provided in Elixir's official documentation. Source: [link][WorkingWithInvalidDataExample] 1389 | 1390 | * __Treatments:__ 1391 | 1392 | * [Typing parameters and return values](https://github.com/lucasvegi/Elixir-Refactorings?#typing-parameters-and-return-values) |> [Add type declarations and contracts](https://github.com/lucasvegi/Elixir-Refactorings?#add-type-declarations-and-contracts) 1393 | * [Introduce pattern matching over a parameter](https://github.com/lucasvegi/Elixir-Refactorings?#introduce-pattern-matching-over-a-parameter) 1394 | * [Struct guard to matching](https://github.com/lucasvegi/Elixir-Refactorings?#struct-guard-to-matching) 1395 | * [Simplifying guard sequences](https://github.com/lucasvegi/Elixir-Refactorings?#simplifying-guard-sequences) 1396 | * [Converts guards to conditionals](https://github.com/lucasvegi/Elixir-Refactorings?#converts-guards-to-conditionals) 1397 | 1398 | [▲ back to Index](#table-of-contents) 1399 | ___ 1400 | 1401 | ### Complex branching 1402 | 1403 | * __Category:__ Low-level concerns smell. 1404 | 1405 | * __Note:__ Formerly known as "Complex API error handling". 1406 | 1407 | * __Problem:__ When a function assumes the responsibility of handling multiple errors alone, it can increase its cyclomatic complexity (metric of control-flow) and become incomprehensible. This situation can configure a specific instance of "Long function", a traditional code smell, but has implications of its own. Under these circumstances, this function could get very confusing, difficult to maintain and test, and therefore bug-proneness. 1408 | 1409 | * __Example:__ An example of this code smell is when a function uses the ``case`` control-flow structure or other similar constructs (e.g., ``cond``, or ``receive``) to handle multiple variations of response types returned by the same API endpoint. This practice can make the function more complex, long, and difficult to understand, as shown next. 1410 | 1411 | ```elixir 1412 | def get_customer(customer_id) do 1413 | case get("/customers/#{customer_id}") do 1414 | {:ok, %Tesla.Env{status: 200, body: body}} -> {:ok, body} 1415 | {:ok, %Tesla.Env{body: body}} -> {:error, body} 1416 | {:error, _} = other -> other 1417 | end 1418 | end 1419 | ``` 1420 | 1421 | Although ``get_customer/1`` is not really long in this example, it could be. Thinking about this more complex scenario, where a large number of different responses can be provided to the same endpoint, is not a good idea to concentrate all on a single function. This is a risky scenario, where a little typo, or any problem introduced by the programmer in handling a response type, could eventually compromise the handling of all responses from the endpoint (if the function raises an exception, for example). 1422 | 1423 | * __Refactoring:__ As shown below, in this situation, instead of concentrating all handlings within the same function, creating a complex branching, it is better to delegate each branch (handling of a response type) to a different private function. In this way, the code will be cleaner, more concise, and readable. 1424 | 1425 | ```elixir 1426 | def get_customer(customer_id) when is_integer(customer_id) do 1427 | case get("/customers/#{customer_id}") do 1428 | {:ok, %Tesla.Env{status: 200, body: body}} -> success_api_response(body) 1429 | {:ok, %Tesla.Env{body: body}} -> x_error_api_response(body) 1430 | {:error, _} = other -> y_error_api_response(other) 1431 | end 1432 | end 1433 | 1434 | defp success_api_response(body) do 1435 | {:ok, body} 1436 | end 1437 | 1438 | defp x_error_api_response(body) do 1439 | {:error, body} 1440 | end 1441 | 1442 | defp y_error_api_response(other) do 1443 | other 1444 | end 1445 | ``` 1446 | 1447 | While this example of refactoring ``get_customer/1`` might seem quite more verbose than the original code, remember to imagine a scenario where ``get_customer/1`` is responsible for handling a number much larger than three different types of possible responses. This is the smelly scenario! 1448 | 1449 | This example is based on code written by Zack [MrDoops][MrDoops] and Dimitar Panayotov [dimitarvp][dimitarvp]. Source: [link][ComplexErrorHandleExample]. We got suggestions from José Valim ([@josevalim][jose-valim]) on the refactoring. 1450 | 1451 | * __Treatments:__ 1452 | 1453 | * [Extract function](https://github.com/lucasvegi/Elixir-Refactorings?#extract-function) 1454 | * [Introduce pattern matching over a parameter](https://github.com/lucasvegi/Elixir-Refactorings?#introduce-pattern-matching-over-a-parameter) 1455 | 1456 | [▲ back to Index](#table-of-contents) 1457 | ___ 1458 | 1459 | ### Complex else clauses in with 1460 | 1461 | * __Category:__ Low-level concerns smell. 1462 | 1463 | * __Note:__ This smell was suggested by the community via issues ([#7][Complex-else-clauses-in-with-issue]). 1464 | 1465 | * __Problem:__ This code smell refers to ``with`` statements that flatten all its error clauses into a single complex ``else`` block. This situation is harmful to the code readability and maintainability because difficult to know from which clause the error value came. 1466 | 1467 | * __Example:__ An example of this code smell, as shown below, is a function ``open_decoded_file/1`` that read a base 64 encoded string content from a file and returns a decoded binary string. This function uses a ``with`` statement that needs to handle two possible errors, all of which are concentrated in a single complex ``else`` block. 1468 | 1469 | ```elixir 1470 | def open_decoded_file(path) do 1471 | with {:ok, encoded} <- File.read(path), 1472 | {:ok, value} <- Base.decode64(encoded) do 1473 | value 1474 | else 1475 | {:error, _} -> :badfile 1476 | :error -> :badencoding 1477 | end 1478 | end 1479 | ``` 1480 | 1481 | * __Refactoring:__ As shown below, in this situation, instead of concentrating all error handlings within a single complex ``else`` block, it is better to normalize the return types in specific private functions. In this way, due to its organization, the code will be cleaner and more readable. 1482 | 1483 | ```elixir 1484 | def open_decoded_file(path) do 1485 | with {:ok, encoded} <- file_read(path), 1486 | {:ok, value} <- base_decode64(encoded) do 1487 | value 1488 | end 1489 | end 1490 | 1491 | defp file_read(path) do 1492 | case File.read(path) do 1493 | {:ok, contents} -> {:ok, contents} 1494 | {:error, _} -> :badfile 1495 | end 1496 | end 1497 | 1498 | defp base_decode64(contents) do 1499 | case Base.decode64(contents) do 1500 | {:ok, contents} -> {:ok, contents} 1501 | :error -> :badencoding 1502 | end 1503 | end 1504 | ``` 1505 | 1506 | This example and the refactoring are proposed by José Valim ([@josevalim][jose-valim]) 1507 | 1508 | * __Treatments:__ 1509 | 1510 | * [Extract function](https://github.com/lucasvegi/Elixir-Refactorings?#extract-function) |> [Remove dead code](https://github.com/lucasvegi/Elixir-Refactorings?#remove-dead-code) 1511 | * [Remove redundant last clause in "with"](https://github.com/lucasvegi/Elixir-Refactorings?#remove-redundant-last-clause-in-with) 1512 | * [Moving "with" clauses without pattern matching](https://github.com/lucasvegi/Elixir-Refactorings?#moving-with-clauses-without-pattern-matching) 1513 | 1514 | [▲ back to Index](#table-of-contents) 1515 | ___ 1516 | 1517 | ### Alternative return types 1518 | 1519 | * __Category:__ Low-level concerns smell. 1520 | 1521 | * __Note:__ This smell was suggested by the community via issues ([#6][Alternative-return-type-issue]). 1522 | 1523 | * __Problem:__ This code smell refers to functions that receive options (e.g., ``keyword list``) parameters that drastically change its return type. Because options are optional and sometimes set dynamically, if they change the return type it may be hard to understand what the function actually returns. 1524 | 1525 | * __Example:__ An example of this code smell, as shown below, is when a library (e.g. ``AlternativeInteger``) has a multi-clause function ``parse/2`` with many alternative return types. Depending on the options received as a parameter, the function will have a different return type. 1526 | 1527 | ```elixir 1528 | defmodule AlternativeInteger do 1529 | def parse(string, opts) when is_list(opts) do 1530 | case opts[:discard_rest] do 1531 | true -> #only an integer value convert from string parameter 1532 | _ -> #another return type (e.g., tuple) 1533 | end 1534 | end 1535 | 1536 | def parse(string, opts \\ :default) do 1537 | #another return type (e.g., tuple) 1538 | end 1539 | end 1540 | 1541 | #...Use examples... 1542 | 1543 | iex(1)> AlternativeInteger.parse("13") 1544 | {13, "..."} 1545 | 1546 | iex(2)> AlternativeInteger.parse("13", discard_rest: true) 1547 | 13 1548 | 1549 | iex(3)> AlternativeInteger.parse("13", discard_rest: false) 1550 | {13, "..."} 1551 | ``` 1552 | 1553 | * __Refactoring:__ To refactor this smell, as shown next, it's better to add in the library a specific function for each return type (e.g., ``parse_no_rest/1``), no longer delegating this to an options parameter. 1554 | 1555 | ```elixir 1556 | defmodule AlternativeInteger do 1557 | def parse_no_rest(string) do 1558 | #only an integer value convert from string parameter 1559 | end 1560 | 1561 | def parse(string) do 1562 | #another return type (e.g., tuple) 1563 | end 1564 | end 1565 | 1566 | #...Use examples... 1567 | 1568 | iex(1)> AlternativeInteger.parse("13") 1569 | {13, "..."} 1570 | 1571 | iex(2)> AlternativeInteger.parse_no_rest("13") 1572 | 13 1573 | ``` 1574 | 1575 | This example and the refactoring are proposed by José Valim ([@josevalim][jose-valim]) 1576 | 1577 | * __Treatments:__ 1578 | 1579 | * [Introduce a temporary duplicate definition](https://github.com/lucasvegi/Elixir-Refactorings?#introduce-a-temporary-duplicate-definition) |> [Rename an identifier](https://github.com/lucasvegi/Elixir-Refactorings?#rename-an-identifier) |> [Add or remove a parameter](https://github.com/lucasvegi/Elixir-Refactorings?#add-or-remove-a-parameter) |> [Typing parameters and return values](https://github.com/lucasvegi/Elixir-Refactorings?#typing-parameters-and-return-values) |> [Remove dead code](https://github.com/lucasvegi/Elixir-Refactorings?#remove-dead-code) 1580 | 1581 | [▲ back to Index](#table-of-contents) 1582 | ___ 1583 | 1584 | ### Accessing non-existent Map/Struct fields 1585 | 1586 | * __Category:__ Low-level concerns smells. 1587 | 1588 | * __Note:__ Formerly known as "Map/struct dynamic access". 1589 | 1590 | * __Problem:__ In Elixir, it is possible to access values from ``Maps``, which are key-value data structures, either strictly or dynamically. When trying to dynamically access the value of a key from a ``Map``, if the informed key does not exist, a null value (``nil``) will be returned. This return can be confusing and does not allow developers to conclude whether the key is non-existent in the ``Map`` or just has no bound value. In this way, this code smell may cause bugs in the code. 1591 | 1592 | * __Example:__ The code shown below is an example of this smell. The function ``plot/1`` tries to draw a graphic to represent the position of a point in a cartesian plane. This function receives a parameter of ``Map`` type with the point attributes, which can be a point of a 2D or 3D cartesian coordinate system. To decide if a point is 2D or 3D, this function uses dynamic access to retrieve values of the ``Map`` keys: 1593 | 1594 | ```elixir 1595 | defmodule Graphics do 1596 | def plot(point) do 1597 | #...some code... 1598 | 1599 | # Dynamic access to use point values 1600 | {point[:x], point[:y], point[:z]} 1601 | 1602 | #...some code... 1603 | end 1604 | end 1605 | 1606 | #...Use examples... 1607 | iex(1)> point_2d = %{x: 2, y: 3} 1608 | %{x: 2, y: 3} 1609 | 1610 | iex(2)> point_3d = %{x: 5, y: 6, z: nil} 1611 | %{x: 5, y: 6, z: nil} 1612 | 1613 | iex(3)> Graphics.plot(point_2d) 1614 | {2, 3, nil} # <= ambiguous return 1615 | 1616 | iex(4)> Graphics.plot(point_3d) 1617 | {5, 6, nil} 1618 | ``` 1619 | 1620 | 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. 1621 | 1622 | * __Refactoring:__ To remove this code smell, whenever a ``Map`` has keys of ``Atom`` type, replace the dynamic access to its values per strict access. When a non-existent key is strictly accessed, Elixir raises an error immediately, allowing developers to find bugs faster. The next code illustrates the refactoring of ``plot/1``, removing this smell: 1623 | 1624 | ```elixir 1625 | defmodule Graphics do 1626 | def plot(point) do 1627 | #...some code... 1628 | 1629 | # Strict access to use point values 1630 | {point.x, point.y, point.z} 1631 | 1632 | #...some code... 1633 | end 1634 | end 1635 | 1636 | #...Use examples... 1637 | iex(1)> point_2d = %{x: 2, y: 3} 1638 | %{x: 2, y: 3} 1639 | 1640 | iex(2)> point_3d = %{x: 5, y: 6, z: nil} 1641 | %{x: 5, y: 6, z: nil} 1642 | 1643 | iex(3)> Graphics.plot(point_2d) 1644 | ** (KeyError) key :z not found in: %{x: 2, y: 3} # <= explicitly warns that 1645 | graphic.ex:6: Graphics.plot/1 # <= the z key does not exist! 1646 | 1647 | iex(4)> Graphics.plot(point_3d) 1648 | {5, 6, nil} 1649 | ``` 1650 | 1651 | As shown below, another alternative to refactor this smell is to replace a ``Map`` with a ``struct`` (named map). By default, structs only support strict access to values. In this way, accesses will always return clear and objective results: 1652 | 1653 | ```elixir 1654 | defmodule Point do 1655 | @enforce_keys [:x, :y] 1656 | defstruct [x: nil, y: nil] 1657 | end 1658 | 1659 | #...Use examples... 1660 | iex(1)> point = %Point{x: 2, y: 3} 1661 | %Point{x: 2, y: 3} 1662 | 1663 | iex(2)> point.x # <= strict access to use point values 1664 | 2 1665 | 1666 | iex(3)> point.z # <= trying to access a non-existent key 1667 | ** (KeyError) key :z not found in: %Point{x: 2, y: 3} 1668 | 1669 | iex(4)> point[:x] # <= by default, struct does not support dynamic access 1670 | ** (UndefinedFunctionError) ... (Point does not implement the Access behaviour) 1671 | ``` 1672 | 1673 | These examples are based on code written by José Valim ([@josevalim][jose-valim]). Source: [link][JoseValimExamples] 1674 | 1675 | * __Treatments:__ 1676 | 1677 | * [Default value for an absent key in a Map](https://github.com/lucasvegi/Elixir-Refactorings?#default-value-for-an-absent-key-in-a-map) 1678 | * [Introduce pattern matching over a parameter](https://github.com/lucasvegi/Elixir-Refactorings?#introduce-pattern-matching-over-a-parameter) 1679 | * [Simplifying checks by using truthness condition](https://github.com/lucasvegi/Elixir-Refactorings?#simplifying-checks-by-using-truthness-condition) 1680 | * [Explicit a double boolean negation](https://github.com/lucasvegi/Elixir-Refactorings?#explicit-a-double-boolean-negation) 1681 | * [Struct field access elimination](https://github.com/lucasvegi/Elixir-Refactorings?#struct-field-access-elimination) |> [Equality guard to pattern matching](https://github.com/lucasvegi/Elixir-Refactorings?#equality-guard-to-pattern-matching) 1682 | 1683 | [▲ back to Index](#table-of-contents) 1684 | ___ 1685 | 1686 | ### Speculative Assumptions 1687 | 1688 | * __Category:__ Low-level concerns smells. 1689 | 1690 | * __Note:__ Formerly known as "Unplanned value extraction". 1691 | 1692 | * __Problem:__ Overall, Elixir application’s are composed of many supervised processes, so the effects of an error will be localized in a single process, not propagating to the entire application. A supervisor will detect the failing process, and restart it at that level. For this type of design to behave well, it's important that problematic code crashes when it fails to fulfill its purpose. However, some code may have undesired behavior making many assumptions we have not really planned for, such as being able to return incorrect values instead of forcing a crash. These speculative assumptions can give a false impression that the code is working correctly. 1693 | 1694 | * __Example:__ The code shown below is an example of this smell. The function ``get_value/2`` tries to extract a value from a specific key of a URL query string. As it is not implemented using pattern matching, ``get_value/2`` always returns a value, regardless of the format of the URL query string passed as a parameter in the call. Sometimes the returned value will be valid; however, if a URL query string with an unexpected format is used in the call, ``get_value/2`` will extract incorrect values from it: 1695 | 1696 | ```elixir 1697 | defmodule Extract do 1698 | 1699 | @doc """ 1700 | Extract value from a key in a URL query string. 1701 | """ 1702 | def get_value(string, desired_key) do 1703 | parts = String.split(string, "&") 1704 | Enum.find_value(parts, fn pair -> 1705 | key_value = String.split(pair, "=") 1706 | Enum.at(key_value, 0) == desired_key && Enum.at(key_value, 1) 1707 | end) 1708 | end 1709 | 1710 | end 1711 | 1712 | #...Use examples... 1713 | 1714 | # URL query string according to with the planned format - OK! 1715 | iex(1)> Extract.get_value("name=Lucas&university=UFMG&lab=ASERG", "lab") 1716 | "ASERG" 1717 | 1718 | iex(2)> Extract.get_value("name=Lucas&university=UFMG&lab=ASERG", "university") 1719 | "UFMG" 1720 | 1721 | # Unplanned URL query string format - Unplanned value extraction! 1722 | iex(3)> Extract.get_value("name=Lucas&university=institution=UFMG&lab=ASERG", "university") 1723 | "institution" # <= why not "institution=UFMG"? or only "UFMG"? 1724 | ``` 1725 | 1726 | * __Refactoring:__ To remove this code smell, ``get_value/2`` can be refactored through the use of pattern matching. So, if an unexpected URL query string format is used, the function will be crash instead of returning an invalid value. This behavior, shown below, will allow clients to decide how to handle these errors and will not give a false impression that the code is working correctly when unexpected values are extracted: 1727 | 1728 | ```elixir 1729 | defmodule Extract do 1730 | 1731 | @doc """ 1732 | Extract value from a key in a URL query string. 1733 | Refactored by using pattern matching. 1734 | """ 1735 | def get_value(string, desired_key) do 1736 | parts = String.split(string, "&") 1737 | Enum.find_value(parts, fn pair -> 1738 | [key, value] = String.split(pair, "=") # <= pattern matching 1739 | key == desired_key && value 1740 | end) 1741 | end 1742 | 1743 | end 1744 | 1745 | #...Use examples... 1746 | 1747 | # URL query string according to with the planned format - OK! 1748 | iex(1)> Extract.get_value("name=Lucas&university=UFMG&lab=ASERG", "name") 1749 | "Lucas" 1750 | 1751 | # Unplanned URL query string format - Crash explaining the problem to the client! 1752 | iex(2)> Extract.get_value("name=Lucas&university=institution=UFMG&lab=ASERG", "university") 1753 | ** (MatchError) no match of right hand side value: ["university", "institution", "UFMG"] 1754 | extract.ex:7: anonymous fn/2 in Extract.get_value/2 # <= left hand: [key, value] pair 1755 | 1756 | iex(3)> Extract.get_value("name=Lucas&university&lab=ASERG", "university") 1757 | ** (MatchError) no match of right hand side value: ["university"] 1758 | extract.ex:7: anonymous fn/2 in Extract.get_value/2 # <= left hand: [key, value] pair 1759 | ``` 1760 | 1761 | These examples are based on code written by José Valim ([@josevalim][jose-valim]). Source: [link][JoseValimExamples] 1762 | 1763 | * __Treatments:__ 1764 | 1765 | * [Introduce pattern matching over a parameter](https://github.com/lucasvegi/Elixir-Refactorings?#introduce-pattern-matching-over-a-parameter) 1766 | * [Pipeline using "with"](https://github.com/lucasvegi/Elixir-Refactorings?#pipeline-using-with) 1767 | 1768 | [▲ back to Index](#table-of-contents) 1769 | ___ 1770 | 1771 | ### Modules with identical names 1772 | 1773 | * __Category:__ Low-level concerns smells. 1774 | 1775 | * __Problem:__ This code smell is related to possible module name conflicts that can occur when a library is implemented. Due to a limitation of the Erlang VM (BEAM), also used by Elixir, only one instance of a module can be loaded at a time. If there are name conflicts between more than one module, they will be considered the same by BEAM and only one of them will be loaded. This can cause unwanted code behavior. 1776 | 1777 | * __Example:__ The code shown below is an example of this smell. Two different modules were defined with identical names (``Foo``). When BEAM tries to load both simultaneously, only the module defined in the file (``module_two.ex``) stay loaded, redefining the current version of ``Foo`` (``module_one.ex``) in memory. That makes it impossible to call ``from_module_one/0``, for example: 1778 | 1779 | ```elixir 1780 | defmodule Foo do 1781 | @moduledoc """ 1782 | Defined in `module_one.ex` file. 1783 | """ 1784 | def from_module_one do 1785 | "Function from module one!" 1786 | end 1787 | end 1788 | ``` 1789 | 1790 | ```elixir 1791 | defmodule Foo do 1792 | @moduledoc """ 1793 | Defined in `module_two.ex` file. 1794 | """ 1795 | def from_module_two do 1796 | "Function from module two!" 1797 | end 1798 | end 1799 | ``` 1800 | 1801 | When BEAM tries to load both simultaneously, the name conflict causes only one of them to stay loaded: 1802 | 1803 | ```elixir 1804 | iex(1)> c("module_one.ex") 1805 | [Foo] 1806 | 1807 | iex(2)> c("module_two.ex") 1808 | warning: redefining module Foo (current version defined in memory) 1809 | module_two.ex:1 1810 | [Foo] 1811 | 1812 | iex(3)> Foo.from_module_two() 1813 | "Function from module two!" 1814 | 1815 | iex(4)> Foo.from_module_one() # <= impossible to call due to name conflict 1816 | ** (UndefinedFunctionError) function Foo.from_module_one/0 is undefined... 1817 | ``` 1818 | 1819 | * __Refactoring:__ To remove this code smell, a library must standardize the naming of its modules, always using its own name as a prefix (namespace) for all its module's names (e.g., ``LibraryName.ModuleName``). When a module file is within subdirectories of a library, the names of the subdirectories must also be used in the module naming (e.g., ``LibraryName.SubdirectoryName.ModuleName``). In the refactored code shown below, this module naming pattern was used. For this, the ``Foo`` module, defined in the file ``module_two.ex``, was also moved to the ``utils`` subdirectory. This refactoring, in addition to eliminating the internal conflict of names within the library, will prevent the occurrence of name conflicts with client code: 1820 | 1821 | ```elixir 1822 | defmodule MyLibrary.Foo do 1823 | @moduledoc """ 1824 | Defined in `module_one.ex` file. 1825 | Name refactored! 1826 | """ 1827 | def from_module_one do 1828 | "Function from module one!" 1829 | end 1830 | end 1831 | ``` 1832 | 1833 | ```elixir 1834 | defmodule MyLibrary.Utils.Foo do 1835 | @moduledoc """ 1836 | Defined in `module_two.ex` file. 1837 | Name refactored! 1838 | """ 1839 | def from_module_two do 1840 | "Function from module two!" 1841 | end 1842 | end 1843 | ``` 1844 | 1845 | When BEAM tries to load them simultaneously, both will stay loaded successfully: 1846 | 1847 | ```elixir 1848 | iex(1)> c("module_one.ex") 1849 | [MyLibrary.Foo] 1850 | 1851 | iex(2)> c("module_two.ex") 1852 | [MyLibrary.Utils.Foo] 1853 | 1854 | iex(3)> MyLibrary.Foo.from_module_one() 1855 | "Function from module one!" 1856 | 1857 | iex(4)> MyLibrary.Utils.Foo.from_module_two() 1858 | "Function from module two!" 1859 | ``` 1860 | 1861 | This example is based on the description provided in Elixir's official documentation. Source: [link][ModulesWithIdenticalNamesExample] 1862 | 1863 | * __Treatments:__ 1864 | 1865 | * [Rename an identifier](https://github.com/lucasvegi/Elixir-Refactorings?#rename-an-identifier) 1866 | * [Introduce a temporary duplicate definition](https://github.com/lucasvegi/Elixir-Refactorings?#introduce-a-temporary-duplicate-definition) |> [Remove dead code](https://github.com/lucasvegi/Elixir-Refactorings?#remove-dead-code) 1867 | * [Move file](https://github.com/lucasvegi/Elixir-Refactorings?#move-file) 1868 | 1869 | [▲ back to Index](#table-of-contents) 1870 | ___ 1871 | 1872 | ### Unnecessary macros 1873 | 1874 | * __Category:__ Low-level concerns smells. 1875 | 1876 | * __Problem:__ ``Macros`` are powerful meta-programming mechanisms that can be used in Elixir to extend the language. While implementing ``macros`` is not a code smell in itself, this meta-programming mechanism should only be used when absolutely necessary. Whenever a macro is implemented, and it was possible to solve the same problem using functions or other pre-existing Elixir structures, the code becomes unnecessarily more complex and less readable. Because ``macros`` are more difficult to implement and understand, their indiscriminate use can compromise the evolution of a system, reducing its maintainability. 1877 | 1878 | * __Example:__ The code shown below is an example of this smell. The ``MyMath`` module implements the ``sum/2`` macro to perform the sum of two numbers received as parameters. While this code has no syntax errors and can be executed correctly to get the desired result, it is unnecessarily more complex. By implementing this functionality as a macro rather than a conventional function, the code became less clear and less objective: 1879 | 1880 | ```elixir 1881 | defmodule MyMath do 1882 | 1883 | defmacro sum(v1, v2) do 1884 | quote do 1885 | unquote(v1) + unquote(v2) 1886 | end 1887 | end 1888 | 1889 | end 1890 | 1891 | #...Use examples... 1892 | 1893 | iex(1)> require MyMath 1894 | MyMath 1895 | 1896 | iex(2)> MyMath.sum(3, 5) 1897 | 8 1898 | 1899 | iex(3)> MyMath.sum(3+1, 5+6) 1900 | 15 1901 | ``` 1902 | 1903 | * __Refactoring:__ To remove this code smell, the developer must replace the unnecessary macro with structures that are simpler to write and understand, such as named functions. The code shown below is the result of the refactoring of the previous example. Basically, the ``sum/2`` macro has been transformed into a conventional named function. Note that the ``require`` command is no longer needed: 1904 | 1905 | ```elixir 1906 | defmodule MyMath do 1907 | 1908 | def sum(v1, v2) do # <= macro became a named function! 1909 | v1 + v2 1910 | end 1911 | 1912 | end 1913 | 1914 | #...Use examples... 1915 | 1916 | # No need to require anymore! 1917 | 1918 | iex(1)> MyMath.sum(3, 5) 1919 | 8 1920 | 1921 | iex(2)> MyMath.sum(3+1, 5+6) 1922 | 15 1923 | ``` 1924 | 1925 | This example is based on the description provided in Elixir's official documentation. Source: [link][UnnecessaryMacroExample] 1926 | 1927 | * __Treatments:__ 1928 | 1929 | * [Inline macro](https://github.com/lucasvegi/Elixir-Refactorings?#inline-macro) 1930 | * [Extract function](https://github.com/lucasvegi/Elixir-Refactorings?#extract-function) |> [Moving a definition](https://github.com/lucasvegi/Elixir-Refactorings?#moving-a-definition) 1931 | 1932 | [▲ back to Index](#table-of-contents) 1933 | ___ 1934 | 1935 | ### Dynamic atom creation 1936 | 1937 | * __Category:__ Low-level concerns smells. 1938 | 1939 | * __Note:__ This smell emerged from a study with mining software repositories (MSR). 1940 | 1941 | * __Problem:__ An ``atom`` is a basic data type of Elixir whose value is its own name. They are often useful to identify resources or to express the state of an operation. The creation of an ``atom`` do not characterize a smell by itself; however, ``atoms`` are not collected by Elixir's Garbage Collector, so values of this type live in memory while an application is executing, during its entire lifetime. Also, BEAM limit the number of ``atoms`` that can exist in an application (``1_048_576``) and each ``atom`` has a maximum size limited to 255 Unicode code points. For these reasons, the dynamic atom creation is considered a code smell, since in this way the developer has no control over how many ``atoms`` will be created during the execution of the application. This unpredictable scenario can expose an app to unexpected behavior caused by excessive memory usage, or even by reaching the maximum number of ``atoms`` possible. 1942 | 1943 | * __Example:__ The code shown below is an example of this smell. Imagine that you are implementing a code that performs the conversion of ``string`` values into ``atoms`` to identify resources. These ``strings`` can come from user input or even have been received as response from requests to an API. As this is a dynamic and unpredictable scenario, it is possible for identical ``strings`` to be converted into new ``atoms`` that are repeated unnecessarily. This kind of conversion, in addition to wasting memory, can be problematic for an application if it happens too often. 1944 | 1945 | ```elixir 1946 | defmodule Identifier do 1947 | ... 1948 | 1949 | def generate(id) when is_bitstring(id) do 1950 | String.to_atom(id) #<= dynamic atom creation!! 1951 | end 1952 | end 1953 | 1954 | #...Use examples... 1955 | 1956 | iex(1)> string_from_user_input = "my_id" 1957 | "my_id" 1958 | 1959 | iex(2)> string_from_API_response = "my_id" 1960 | "my_id" 1961 | 1962 | iex(3)> Identifier.generate(string_from_user_input) 1963 | :my_id 1964 | 1965 | iex(4)> Identifier.generate(string_from_API_response) 1966 | :my_id #<= atom repeated was created! 1967 | ``` 1968 | 1969 | When we use the ``String.to_atom/1`` function to dynamically create an ``atom``, it is created regardless of whether there is already another one with the same value in memory, so when this happens automatically, we will not have control over meeting the limits established by BEAM. 1970 | 1971 | * __Refactoring:__ To remove this smell, as shown below, first you must ensure that all the identifier ``atoms`` are created statically, only once, at the beginning of an application's execution: 1972 | 1973 | ```elixir 1974 | # statically created atoms... 1975 | _ = :my_id 1976 | _ = :my_id2 1977 | _ = :my_id3 1978 | _ = :my_id4 1979 | ``` 1980 | 1981 | Next, you should replace the use of the ``String.to_atom/1`` function with the ``String.to_existing_atom/1`` function. This will allow string-to-atom conversions to just map the strings to atoms already in memory (statically created at the beginning of the execution), thus preventing repeated ``atoms`` from being created dynamically. This second part of the refactoring is presented below. 1982 | 1983 | ```elixir 1984 | defmodule Identifier do 1985 | ... 1986 | 1987 | def generate(id) when is_bitstring(id) do 1988 | String.to_existing_atom(id) #<= just maps a string to an existing atom! 1989 | end 1990 | end 1991 | 1992 | #...Use examples... 1993 | 1994 | iex(1)> Identifier.generate("my_id") 1995 | :my_id 1996 | 1997 | iex(2)> Identifier.generate("my_id2") 1998 | :my_id2 1999 | 2000 | iex(3)> Identifier.generate("non_existent_id") 2001 | ** (ArgumentError) errors were found at the given arguments: 2002 | * 1st argument: not an already existing atom 2003 | ``` 2004 | 2005 | Note that in the third use example, when a ``string`` different from an already existing ``atom`` is given, Elixir shows an error instead of performing the conversion. This demonstrates that this refactoring creates a more controlled and predictable scenario for the application in terms of memory usage. 2006 | 2007 | This example and the refactoring are based on the Elixir's official documentation. Sources: [1][to_atom], [2][to_existing_atom] 2008 | 2009 | * __Treatments:__ 2010 | 2011 | * [Extract function](https://github.com/lucasvegi/Elixir-Refactorings?#extract-function) |> [Introduce pattern matching over a parameter](https://github.com/lucasvegi/Elixir-Refactorings?#introduce-pattern-matching-over-a-parameter) 2012 | * [Folding against a function definition](https://github.com/lucasvegi/Elixir-Refactorings?#folding-against-a-function-definition) 2013 | * [Introduce a temporary duplicate definition](https://github.com/lucasvegi/Elixir-Refactorings?#introduce-a-temporary-duplicate-definition) |> [Remove dead code](https://github.com/lucasvegi/Elixir-Refactorings?#remove-dead-code) 2014 | 2015 | [▲ back to Index](#table-of-contents) 2016 | 2017 | ## About 2018 | 2019 | This catalog was proposed by Lucas Vegi and Marco Tulio Valente, from [ASERG/DCC/UFMG][ASERG]. 2020 | 2021 | For more info see the following paper: 2022 | 2023 | * [Code Smells in Elixir: Early Results from a Grey Literature Review][preprint-copy], International Conference on Program Comprehension (ICPC), 2022. [[slides]][ICPC22-PDF] [[video]][ICPC22-YouTube] [[podcast (pt-BR) - English subtitles available]][Podcast-Spotify] 2024 | * [Understanding code smells in Elixir functional language][emse-paper], Empirical Software Engineering Journal (EMSE), 2023. 2025 | 2026 | Please feel free to make pull requests and suggestions ([Issues][Issues] tab). 2027 | 2028 | [▲ back to Index](#table-of-contents) 2029 | 2030 | ## Acknowledgments 2031 | 2032 | We are supported by __[Finbits][Finbits]__TM, a Brazilian Elixir-based fintech: 2033 | 2034 |
2035 | 2036 |

2037 |
2038 | 2039 | Our research is also part of the initiative called __[Research with Elixir][ResearchWithElixir]__ (in portuguese). 2040 | 2041 | [▲ back to Index](#table-of-contents) 2042 | 2043 | 2044 | [Elixir Smells]: https://github.com/lucasvegi/Elixir-Code-Smells 2045 | [Elixir]: http://elixir-lang.org 2046 | [ASERG]: http://aserg.labsoft.dcc.ufmg.br/ 2047 | [MultiClauseExample]: https://syamilmj.com/2021-09-01-elixir-multi-clause-anti-pattern/ 2048 | [ComplexErrorHandleExample]: https://elixirforum.com/t/what-are-sort-of-smells-do-you-tend-to-find-in-elixir-code/14971 2049 | [JoseValimExamples]: http://blog.plataformatec.com.br/2014/09/writing-assertive-code-with-elixir/ 2050 | [dimitarvp]: https://elixirforum.com/u/dimitarvp 2051 | [MrDoops]: https://elixirforum.com/u/MrDoops 2052 | [neenjaw]: https://exercism.org/profiles/neenjaw 2053 | [angelikatyborska]: https://exercism.org/profiles/angelikatyborska 2054 | [ExceptionsForControlFlowExamples]: https://exercism.org/tracks/elixir/concepts/try-rescue 2055 | [DataManipulationByMigrationExamples]: https://www.idopterlabs.com.br/post/criando-uma-mix-task-em-elixir 2056 | [Migration]: https://hexdocs.pm/ecto_sql/Ecto.Migration.html 2057 | [MixTask]: https://hexdocs.pm/mix/Mix.html#module-mix-task 2058 | [CodeOrganizationByProcessExample]: https://hexdocs.pm/elixir/main/library-guidelines.html#avoid-using-processes-for-code-organization 2059 | [GenServer]: https://hexdocs.pm/elixir/master/GenServer.html 2060 | [UnsupervisedProcessExample]: https://hexdocs.pm/elixir/main/library-guidelines.html#avoid-spawning-unsupervised-processes 2061 | [Supervisor]: https://hexdocs.pm/elixir/master/Supervisor.html 2062 | [Discussions]: https://github.com/lucasvegi/Elixir-Code-Smells/discussions 2063 | [Issues]: https://github.com/lucasvegi/Elixir-Code-Smells/issues 2064 | [LargeMessageExample]: https://samuelmullen.com/articles/elixir-processes-send-and-receive 2065 | [Agent]: https://hexdocs.pm/elixir/1.13/Agent.html 2066 | [Task]: https://hexdocs.pm/elixir/1.13/Task.html 2067 | [GenServer]: https://hexdocs.pm/elixir/1.13/GenServer.html 2068 | [AgentObsessionExample]: https://elixir-lang.org/getting-started/mix-otp/agent.html#agents 2069 | [ElixirInProduction]: https://elixir-companies.com/ 2070 | [WorkingWithInvalidDataExample]: https://hexdocs.pm/elixir/main/library-guidelines.html#avoid-working-with-invalid-data 2071 | [ModulesWithIdenticalNamesExample]: https://hexdocs.pm/elixir/main/library-guidelines.html#avoid-defining-modules-that-are-not-in-your-namespace 2072 | [UnnecessaryMacroExample]: https://hexdocs.pm/elixir/main/library-guidelines.html#avoid-macros 2073 | [ApplicationEnvironment]: https://hexdocs.pm/elixir/1.13/Config.html 2074 | [AppConfigurationForCodeLibsExample]: https://hexdocs.pm/elixir/main/library-guidelines.html#avoid-application-configuration 2075 | [CredoWarningApplicationConfigInModuleAttribute]: https://hexdocs.pm/credo/Credo.Check.Warning.ApplicationConfigInModuleAttribute.html 2076 | [Credo]: https://hexdocs.pm/credo/overview.html 2077 | [DependencyWithUseExample]: https://hexdocs.pm/elixir/main/library-guidelines.html#avoid-use-when-an-import-is-enough 2078 | [ICPC-ERA]: https://conf.researchr.org/track/icpc-2022/icpc-2022-era 2079 | [preprint-copy]: https://doi.org/10.48550/arXiv.2203.08877 2080 | [emse-paper]: https://link.springer.com/epdf/10.1007/s10664-023-10343-6?sharing_token=-a5aIHuxjO5IVwjuWKcGDve4RwlQNchNByi7wbcMAY7M-LcfCdzMaX7W988J1lKodpMwih75AE3ZQ9gFhldJBeLq53jeNkeHR7W04UAwrxBvoXDh5P83TYkQfuz-PrYpU1J5KqxUgojIbDDFDV_jVtrEE8oVtobDqNSSrInauuI%3D 2081 | [jose-valim]: https://github.com/josevalim 2082 | [syamilmj]: https://github.com/syamilmj 2083 | [Complex-extraction-in-clauses-issue]: https://github.com/lucasvegi/Elixir-Code-Smells/issues/9 2084 | [Alternative-return-type-issue]: https://github.com/lucasvegi/Elixir-Code-Smells/issues/6 2085 | [Complex-else-clauses-in-with-issue]: https://github.com/lucasvegi/Elixir-Code-Smells/issues/7 2086 | [Large-code-generation-issue]: https://github.com/lucasvegi/Elixir-Code-Smells/issues/13 2087 | [ICPC22-PDF]: https://github.com/lucasvegi/Elixir-Code-Smells/blob/main/etc/Code-Smells-in-Elixir-ICPC22-Lucas-Vegi.pdf 2088 | [ICPC22-YouTube]: https://youtu.be/3X2gxg13tXo 2089 | [Podcast-Spotify]: http://elixiremfoco.com/episode?id=lucas-vegi-e-marco-tulio 2090 | [to_atom]: https://hexdocs.pm/elixir/String.html#to_atom/1 2091 | [to_existing_atom]: https://hexdocs.pm/elixir/String.html#to_existing_atom/1 2092 | [Finbits]: https://www.finbits.com.br/ 2093 | [ResearchWithElixir]: http://pesquisecomelixir.com.br/ 2094 | [TraditionalSmells]: https://github.com/lucasvegi/Elixir-Code-Smells/tree/main/traditional 2095 | -------------------------------------------------------------------------------- /code_smells.livemd: -------------------------------------------------------------------------------- 1 | # Catalog of Elixir-specific code smells 2 | 3 | ## Introduction 4 | 5 | [Elixir](https://elixir-lang.org/) is a functional programming language whose popularity is rising in the industry [link](https://elixir-companies.com/). However, there are few works in the scientific literature focused on studying the internal quality of systems implemented in this language. 6 | 7 | In order to better understand the types of sub-optimal code structures that can harm the internal quality of Elixir systems, we scoured websites, blogs, forums, and videos (grey literature review), looking for specific code smells for Elixir that are discussed by its developers. 8 | 9 | As a result of this investigation, we have initially proposed a catalog of 18 new smells that are specific to Elixir systems. Other smells are being suggested by the community, so this catalog is constantly being updated __(currently 23 smells)__. These code smells are categorized into two different groups ([design-related](#design-related-smells) and [low-level concerns](#low-level-concerns-smells)), according to the type of impact and code extent they affect. This catalog of Elixir-specific code smells is presented below. Each code smell is documented using the following structure: 10 | 11 | * __Name:__ Unique identifier of the code smell. This name is important to facilitate communication between developers; 12 | * __Category:__ The portion of code affected by smell and its severity; 13 | * __Problem:__ How the code smell can harm code quality and what impacts this can have for developers; 14 | * __Example:__ Code and textual descriptions to illustrate the occurrence of the code smell; 15 | * __Refactoring:__ Ways to change smelly code in order to improve its qualities. Examples of refactored code are presented to illustrate these changes. 16 | 17 | The objective of this catalog of code smells is to instigate the improvement of the quality of code developed in Elixir. For this reason, we are interested in knowing Elixir's community opinion about these code smells: *Do you agree that these code smells can be harmful? Have you seen any of them in production code? Do you have any suggestions about some Elixir-specific code smell not cataloged by us?...* 18 | 19 | Please feel free to make pull requests and suggestions ([Issues](https://github.com/lucasvegi/Elixir-Code-Smells/issues) tab). We want to hear from you! 20 | 21 | ## Design-related smells 22 | 23 | Design-related smells are more complex, affect a coarse-grained code element, and are therefore harder to detect. In this section, 14 different smells classified as design-related are explained and exemplified: 24 | 25 | 26 | 27 | ## GenServer Envy 28 | 29 | * __Category:__ Design-related smell. 30 | 31 | * __Problem:__ In Elixir, processes can be primitively created by `Kernel.spawn/1`, `Kernel.spawn/3`, `Kernel.spawn_link/1` and `Kernel.spawn_link/3` functions. Although it is possible to create them this way, it is more common to use abstractions (e.g., [`Agent`](https://hexdocs.pm/elixir/1.13/Agent.html), [`Task`](https://hexdocs.pm/elixir/1.13/Task.html), and [`GenServer`](https://hexdocs.pm/elixir/master/GenServer.html)) provided by Elixir to create processes. The use of each specific abstraction is not a code smell in itself; however, there can be trouble when either a `Task` or `Agent` is used beyond its suggested purposes, being treated like a `GenServer`. 32 | 33 | * __Example:__ As shown next, `Agent` and `Task` are abstractions to create processes with specialized purposes. In contrast, `GenServer` is a more generic abstraction used to create processes for many different purposes: 34 | 35 | * `Agent`: As Elixir works on the principle of immutability, by default no value is shared between multiple places of code, enabling read and write as in a global variable. An `Agent` is a simple process abstraction focused on solving this limitation, enabling processes to share state. 36 | * `Task`: This process abstraction is used when we only need to execute some specific action asynchronously, often in an isolated way, without communication with other processes. 37 | * `GenServer`: This is the most generic process abstraction. The main benefit of this abstraction is explicitly segregating the server and the client roles, thus providing a better API for the organization of processes communication. Besides that, a `GenServer` can also encapsulate state (like an `Agent`), provide sync and async calls (like a `Task`), and more. 38 | 39 | Examples of this code smell appear when `Agents` or `Tasks` are used for general purposes and not only for specialized ones such as their documentation suggests. To illustrate some smell occurrences, we will cite two specific situations. 1) When a `Task` is used not only to async execute an action, but also to frequently exchange messages with other processes; 2) When an `Agent`, beside sharing some global value between processes, is also frequently used to execute isolated tasks that are not of interest to other processes. 40 | 41 | * __Refactoring:__ When an `Agent` or `Task` goes beyond its suggested use cases and becomes painful, it is better to refactor it into a `GenServer`. 42 | 43 | 44 | 45 | ## Agent Obsession 46 | 47 | * __Category:__ Design-related smell. 48 | 49 | * __Problem:__ In Elixir, an `Agent` is a process abstraction focused on sharing information between processes by means of message passing. It is a simple wrapper around shared information, thus facilitating its read and update from any place in the code. The use of an `Agent` to share information is not a code smell in itself; however, when the responsibility for interacting directly with an `Agent` is spread across the entire system, this can be problematic. This bad practice can increase the difficulty of code maintenance and make the code more prone to bugs. 50 | 51 | * __Example:__ The following code seeks to illustrate this smell. The responsibility for interacting directly with the `Agent` is spread across four different modules (i.e, `A`, `B`, `C`, and `D`). 52 | 53 | ```elixir 54 | defmodule A do 55 | # ... 56 | def update(pid) do 57 | # ... 58 | Agent.update(pid, fn _list -> 123 end) 59 | # ... 60 | end 61 | end 62 | ``` 63 | 64 | ```elixir 65 | defmodule B do 66 | # ... 67 | def update(pid) do 68 | # ... 69 | Agent.update(pid, fn content -> %{a: content} end) 70 | # ... 71 | end 72 | end 73 | ``` 74 | 75 | ```elixir 76 | defmodule C do 77 | # ... 78 | def update(pid) do 79 | # ... 80 | Agent.update(pid, fn content -> [:atom_value | [content]] end) 81 | # ... 82 | end 83 | end 84 | ``` 85 | 86 | ```elixir 87 | defmodule D do 88 | # ... 89 | def get(pid) do 90 | # ... 91 | Agent.get(pid, fn content -> content end) 92 | # ... 93 | end 94 | end 95 | ``` 96 | 97 | This spreading of responsibility can generate duplicated code and make code maintenance more difficult. Also, due to the lack of control over the format of the shared data, complex composed data can be shared. This freedom to use any format of data is dangerous and can induce developers to introduce bugs. 98 | 99 | ```elixir 100 | # start an agent with initial state of an empty list 101 | {:ok, agent} = Agent.start_link(fn -> [] end) 102 | # {:ok, #PID<0.135.0>} 103 | 104 | # many data format (i.e., List, Map, Integer, Atom) are 105 | # combined through direct access spread across the entire system 106 | A.update(agent) 107 | B.update(agent) 108 | C.update(agent) 109 | 110 | # state of shared information 111 | D.get(agent) 112 | # [:atom_value, %{a: 123}] 113 | ``` 114 | 115 | * __Refactoring:__ Instead of spreading direct access to an `Agent` over many places in the code, it is better to refactor this code by centralizing the responsibility for interacting with an `Agent` in a single module. This refactoring improves the maintainability by removing duplicated code; it also allows you to limit the accepted format for shared data, reducing bug-proneness. As shown below, the module `KV.Bucket` is centralizing the responsibility for interacting with the `Agent`. Any other place in the code that needs to access shared data must now delegate this action to `KV.Bucket`. Also, `KV.Bucket` now only allows data to be shared in `Map` format. 116 | 117 | ```elixir 118 | defmodule KV.Bucket do 119 | use Agent 120 | 121 | @doc """ 122 | Starts a new bucket. 123 | """ 124 | def start_link(_opts) do 125 | Agent.start_link(fn -> %{} end) 126 | end 127 | 128 | @doc """ 129 | Gets a value from the `bucket` by `key`. 130 | """ 131 | def get(bucket, key) do 132 | Agent.get(bucket, &Map.get(&1, key)) 133 | end 134 | 135 | @doc """ 136 | Puts the `value` for the given `key` in the `bucket`. 137 | """ 138 | def put(bucket, key, value) do 139 | Agent.update(bucket, &Map.put(&1, key, value)) 140 | end 141 | end 142 | ``` 143 | 144 | The following are examples of how to delegate access to shared data (provided by an `Agent`) to `KV.Bucket`. 145 | 146 | ```elixir 147 | # start an agent through a `KV.Bucket` 148 | {:ok, bucket} = KV.Bucket.start_link(%{}) 149 | # {:ok, #PID<0.114.0>} 150 | 151 | # add shared values to the keys `milk` and `beer` 152 | KV.Bucket.put(bucket, "milk", 3) 153 | KV.Bucket.put(bucket, "beer", 7) 154 | 155 | # accessing shared data of specific keys 156 | KV.Bucket.get(bucket, "beer") 157 | # 7 158 | KV.Bucket.get(bucket, "milk") 159 | # 3 160 | ``` 161 | 162 | These examples are based on code written in Elixir's official documentation. Source: [link](https://elixir-lang.org/getting-started/mix-otp/agent.html#agents) 163 | 164 | 165 | 166 | ## Unsupervised process 167 | 168 | * __Category:__ Design-related smell. 169 | 170 | * __Problem:__ In Elixir, creating a process outside a supervision tree is not a code smell in itself. However, when code creates a large number of long-running processes outside a supervision tree, this can make visibility and monitoring of these processes difficult, preventing developers from fully controlling their applications. 171 | 172 | * __Example:__ The following code example seeks to illustrate a library responsible for maintaining a numerical `Counter` through a `GenServer` process outside a supervision tree. Multiple counters can be created simultaneously by a client (one process for each counter), making these unsupervised processes difficult to manage. This can cause problems with the initialization, restart, and shutdown of a system. 173 | 174 | ```elixir 175 | defmodule Counter do 176 | use GenServer 177 | 178 | @moduledoc """ 179 | Global counter implemented through a GenServer process 180 | outside a supervision tree. 181 | """ 182 | 183 | @doc """ 184 | Function to create a counter. 185 | initial_value: any integer value. 186 | pid_name: optional parameter to define the process name. 187 | Default is Counter. 188 | """ 189 | def start(initial_value, pid_name \\ __MODULE__) 190 | when is_integer(initial_value) do 191 | GenServer.start(__MODULE__, initial_value, name: pid_name) 192 | end 193 | 194 | @doc """ 195 | Function to get the counter's current value. 196 | pid_name: optional parameter to inform the process name. 197 | Default is Counter. 198 | """ 199 | def get(pid_name \\ __MODULE__) do 200 | GenServer.call(pid_name, :get) 201 | end 202 | 203 | @doc """ 204 | Function to changes the counter's current value. 205 | Returns the updated value. 206 | value: amount to be added to the counter. 207 | pid_name: optional parameter to inform the process name. 208 | Default is Counter. 209 | """ 210 | def bump(value, pid_name \\ __MODULE__) do 211 | GenServer.call(pid_name, {:bump, value}) 212 | get(pid_name) 213 | end 214 | 215 | ## Callbacks 216 | 217 | @impl true 218 | def init(counter) do 219 | {:ok, counter} 220 | end 221 | 222 | @impl true 223 | def handle_call(:get, _from, counter) do 224 | {:reply, counter, counter} 225 | end 226 | 227 | def handle_call({:bump, value}, _from, counter) do 228 | {:reply, counter, counter + value} 229 | end 230 | end 231 | 232 | # ...Use examples... 233 | 234 | Counter.start(0) 235 | # {:ok, #PID<0.115.0>} 236 | 237 | Counter.get() 238 | # 0 239 | 240 | Counter.start(15, C2) 241 | # {:ok, #PID<0.120.0>} 242 | 243 | Counter.get(C2) 244 | # 15 245 | 246 | Counter.bump(-3, C2) 247 | # 12 248 | 249 | Counter.bump(7) 250 | # 7 251 | ``` 252 | 253 | * __Refactoring:__ To ensure that clients of a library have full control over their systems, regardless of the number of processes used and the lifetime of each one, all processes must be started inside a supervision tree. As shown below, this code uses a `Supervisor` [link](https://hexdocs.pm/elixir/master/Supervisor.html) as a supervision tree. When this Elixir application is started, two different counters (`Counter` and `C2`) are also started as child processes of the `Supervisor` named `App.Supervisor`. Both are initialized with zero. By means of this supervision tree, it is possible to manage the lifecycle of all child processes (e.g., stopping or restarting each one), improving the visibility of the entire app. 254 | 255 | ```elixir 256 | defmodule SupervisedProcess.Application do 257 | use Application 258 | 259 | @impl true 260 | def start(_type, _args) do 261 | children = [ 262 | # The counters are Supervisor children started via Counter.start(0). 263 | %{ 264 | id: Counter, 265 | start: {Counter, :start, [0]} 266 | }, 267 | %{ 268 | id: C2, 269 | start: {Counter, :start, [0, C2]} 270 | } 271 | ] 272 | 273 | opts = [strategy: :one_for_one, name: App.Supervisor] 274 | Supervisor.start_link(children, opts) 275 | end 276 | end 277 | 278 | #...Use examples... 279 | 280 | iex(1)> Supervisor.count_children(App.Supervisor) 281 | %{active: 2, specs: 2, supervisors: 0, workers: 2} 282 | 283 | iex(2)> Counter.get(Counter) 284 | 0 285 | 286 | iex(3)> Counter.get(C2) 287 | 0 288 | 289 | iex(4)> Counter.bump(7, Counter) 290 | 7 291 | 292 | iex(5)> Supervisor.terminate_child(App.Supervisor, Counter) 293 | iex(6)> Supervisor.count_children(App.Supervisor) 294 | %{active: 1, specs: 2, supervisors: 0, workers: 2} #only one active 295 | 296 | iex(7)> Counter.get(Counter) #Error because it was previously terminated 297 | ** (EXIT) no process: the process is not alive... 298 | 299 | iex(8)> Supervisor.restart_child(App.Supervisor, Counter) 300 | iex(9)> Counter.get(Counter) #after the restart, this process can be accessed again 301 | 0 302 | ``` 303 | 304 | These examples are based on codes written in Elixir's official documentation. Source: [link](https://hexdocs.pm/elixir/main/library-guidelines.html#avoid-spawning-unsupervised-processes) 305 | 306 | 307 | 308 | ## Large messages 309 | 310 | * __Category:__ Design-related smell. 311 | 312 | * __Note:__ Formerly known as "Large messages between processes". 313 | 314 | * __Problem:__ In Elixir, processes run in an isolated manner, often concurrently with other Elixir. Communication between different processes is performed via message passing. The exchange of messages between processes is not a code smell in itself; however, when processes exchange messages, their contents are copied between them. For this reason, if a huge structure is sent as a message from one process to another, the sender can become blocked, compromising performance. If these large message exchanges occur frequently, the prolonged and frequent blocking of processes can cause a system to behave anomalously. 315 | 316 | * __Example:__ The following code is composed of two modules which will each run in a different process. As the names suggest, the `Sender` module has a function responsible for sending messages from one process to another (i.e., `send_msg/3`). The `Receiver` module has a function to create a process to receive messages (i.e., `create/0`) and another one to handle the received messages (i.e., `run/0`). If a huge structure, such as a list with 1_000_000 different values, is sent frequently from `Sender` to `Receiver`, the impacts of this smell could be felt. 317 | 318 | ```elixir 319 | defmodule Receiver do 320 | @doc """ 321 | Function for receiving messages from processes. 322 | """ 323 | def run do 324 | receive do 325 | {:msg, msg_received} -> msg_received 326 | {_, _} -> "won't match" 327 | end 328 | end 329 | 330 | @doc """ 331 | Create a process to receive a message. 332 | Messages are received in the run() function of Receiver. 333 | """ 334 | def create do 335 | spawn(Receiver, :run, []) 336 | end 337 | end 338 | ``` 339 | 340 | ```elixir 341 | defmodule Sender do 342 | @doc """ 343 | Function for sending messages between processes. 344 | pid_receiver: message recipient. 345 | msg: messages of any type and size can be sent. 346 | id_msg: used by receiver to decide what to do 347 | when a message arrives. 348 | Default is the atom :msg 349 | """ 350 | def send_msg(pid_receiver, msg, id_msg \\ :msg) do 351 | send(pid_receiver, {id_msg, msg}) 352 | end 353 | end 354 | ``` 355 | 356 | Examples of large messages between processes: 357 | 358 | ```elixir 359 | pid = Receiver.create() 360 | # PID<0.144.0> 361 | 362 | # Simulating a message with large content - List with length 1_000_000 363 | msg = %{from: inspect(self()), to: inspect(pid), content: Enum.to_list(1..1_000_000)} 364 | 365 | Sender.send_msg(pid, msg) 366 | # {:msg, 367 | # %{ 368 | # content: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 369 | # 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 370 | # 39, 40, 41, 42, 43, 44, 45, 46, 47, ...], 371 | # from: "#PID<0.105.0>", 372 | # to: "#PID<0.144.0>" 373 | # }} 374 | ``` 375 | 376 | This example is based on a original code by Samuel Mullen. Source: [link](https://samuelmullen.com/articles/elixir-processes-send-and-receive) 377 | 378 | 379 | 380 | ## Unrelated multi-clause function 381 | 382 | * __Category:__ Design-related smell. 383 | 384 | * __Note:__ Formerly known as "Complex multi-clause function". 385 | 386 | * __Problem:__ Using multi-clause functions in Elixir, to group functions of the same name, is not a code smell in itself. However, due to the great flexibility provided by this programming feature, some developers may abuse the number of guard clauses and pattern matches to group _unrelated_ functionality. 387 | 388 | * __Example:__ A recurrent example of abusive use of the multi-clause functions is when we’re trying to mix too much-unrelated business logic into the function definitions. This makes it difficult to read and understand the logic involved in the functions, which may impair code maintainability. Some developers use documentation mechanisms such as `@doc` annotations to compensate for poor code readability, but unfortunately, with a multi-clause function, we can only use these annotations once per function name, particularly on the first or header function. As shown next, all other variations of the function need to be documented only with comments, a mechanism that cannot automate tests, leaving the code prone to bugs. 389 | 390 | ```elixir 391 | defmodule Product do 392 | defstruct count: nil, material: nil 393 | end 394 | ``` 395 | 396 | ```elixir 397 | defmodule Animal do 398 | defstruct count: nil, skin: nil 399 | end 400 | ``` 401 | 402 | ```elixir 403 | defmodule MultiClause do 404 | @doc """ 405 | Update sharp product with 0 or empty count 406 | 407 | ## Examples 408 | 409 | iex> Namespace.Module.update(...) 410 | expected result... 411 | """ 412 | def update(%Product{count: nil, material: material}) 413 | when material in ["metal", "glass"] do 414 | # ... 415 | end 416 | 417 | # update blunt product 418 | def update(%Product{count: count, material: material}) 419 | when count > 0 and material in ["metal", "glass"] do 420 | # ... 421 | end 422 | 423 | # update animal... 424 | def update(%Animal{count: 1, skin: skin}) 425 | when skin in ["fur", "hairy"] do 426 | # ... 427 | end 428 | end 429 | ``` 430 | 431 | * __Refactoring:__ As shown below, a possible solution to this smell is to break the business rules that are mixed up in a single unrelated multi-clause function in several different simple functions. Each function can have a specific `@doc`, describing its behavior and parameters received. While this refactoring sounds simple, it can have a lot of impact on the function's current clients, so be careful! 432 | 433 | ```elixir 434 | defmodule MultiClauseRefactoring do 435 | @doc """ 436 | Update sharp product 437 | 438 | ## Parameter 439 | struct: %Product{...} 440 | 441 | ## Examples 442 | 443 | iex> Namespace.Module.update_sharp_product(%Product{...}) 444 | expected result... 445 | """ 446 | def update_sharp_product(struct) do 447 | # ... 448 | end 449 | 450 | @doc """ 451 | Update blunt product 452 | 453 | ## Parameter 454 | struct: %Product{...} 455 | 456 | ## Examples 457 | 458 | iex> Namespace.Module.update_blunt_product(%Product{...}) 459 | expected result... 460 | """ 461 | def update_blunt_product(struct) do 462 | # ... 463 | end 464 | 465 | @doc """ 466 | Update animal 467 | 468 | ## Parameter 469 | struct: %Animal{...} 470 | 471 | ## Examples 472 | 473 | iex> Namespace.Module.update_animal(%Animal{...}) 474 | expected result... 475 | """ 476 | def update_animal(struct) do 477 | # ... 478 | end 479 | end 480 | ``` 481 | 482 | This example is based on a original code by Syamil MJ ([@syamilmj](https://github.com/syamilmj)). Source: [link](https://syamilmj.com/2021-09-01-elixir-multi-clause-anti-pattern/) 483 | 484 | 485 | 486 | ## Complex extractions in clauses 487 | 488 | * __Category:__ Design-related smell. 489 | 490 | * __Note:__ This smell was suggested by the community via issues ([#9](https://github.com/lucasvegi/Elixir-Code-Smells/issues/9)). 491 | 492 | * __Problem:__ When we use multi-clause functions, it is possible to extract values in the clauses for further usage and for pattern matching/guard checking. This extraction itself does not represent a code smell, but when you have too many clauses or too many arguments, it becomes hard to know which extracted parts are used for pattern/guards and what is used only inside the function body. This smell is related to [Unrelated multi-clause function](#unrelated-multi-clause-function), but with implications of its own. It impairs the code readability in a different way. 493 | 494 | * __Example:__ The following code, although simple, tries to illustrate the occurrence of this code smell. The multi-clause function `drive/1` is extracting fields of an `%User{}` struct for usage in the clause expression (e.g. `age`) and for usage in the function body (e.g., `name`). Ideally, a function should not mix pattern matching extractions for usage in its clauses expressions and also in the function boby. 495 | 496 | ```elixir 497 | defmodule User do 498 | defstruct name: nil, age: nil 499 | end 500 | ``` 501 | 502 | ```elixir 503 | defmodule ExtractionInClauses do 504 | def drive(%User{name: name, age: age}) when age >= 18 do 505 | "#{name} can drive" 506 | end 507 | 508 | def drive(%User{name: name, age: age}) when age < 18 do 509 | "#{name} cannot drive" 510 | end 511 | end 512 | ``` 513 | 514 | While the example is small and looks like a clear code, try to imagine a situation where `drive/1` was more complex, having many more clauses, arguments, and extractions. This is the really smelly code! 515 | 516 | * __Refactoring:__ As shown below, a possible solution to this smell is to extract only pattern/guard related variables in the signature once you have many arguments or multiple clauses: 517 | 518 | ```elixir 519 | defmodule ExtractionInClausesRefactoring do 520 | def drive(%User{age: age} = user) when age >= 18 do 521 | %User{name: name} = user 522 | "#{name} can drive" 523 | end 524 | 525 | def drive(%User{age: age} = user) when age < 18 do 526 | %User{name: name} = user 527 | "#{name} cannot drive" 528 | end 529 | end 530 | ``` 531 | 532 | This example and the refactoring are proposed by José Valim ([@josevalim](https://github.com/josevalim)) 533 | 534 | 535 | 536 | ## Using exceptions for control-flow 537 | 538 | * __Category:__ Design-related smell. 539 | 540 | * __Note:__ Formerly known as "Exceptions for control-flow". 541 | 542 | * __Problem:__ This smell refers to code that forces developers to handle exceptions for control-flow. Exception handling itself does not represent a code smell, but this should not be the only alternative available to developers to handle an error in client code. When developers have no freedom to decide if an error is exceptional or not, this is considered a code smell. 543 | 544 | * __Example:__ An example of this code smell, as shown below, is when a library (e.g. `MyModule`) forces its clients to use `try .. rescue` statements to capture and evaluate errors. This library does not allow developers to decide if an error is exceptional or not in their applications. 545 | 546 | ```elixir 547 | defmodule MyModule do 548 | def janky_function(value) do 549 | if is_integer(value) do 550 | # ... 551 | "Result..." 552 | else 553 | raise RuntimeError, message: "invalid argument. Is not integer!" 554 | end 555 | end 556 | end 557 | ``` 558 | 559 | ```elixir 560 | defmodule Client do 561 | # Client forced to use exceptions for control-flow. 562 | def foo(arg) do 563 | try do 564 | value = MyModule.janky_function(arg) 565 | "All good! #{value}." 566 | rescue 567 | e in RuntimeError -> 568 | reason = e.message 569 | "Uh oh! #{reason}." 570 | end 571 | end 572 | end 573 | 574 | # ...Use examples... 575 | 576 | Client.foo(1) 577 | # "All good! Result...." 578 | 579 | Client.foo("lucas") 580 | # "Uh oh! invalid argument. Is not integer!." 581 | ``` 582 | 583 | * __Refactoring:__ Library authors should guarantee that clients are not required to use exceptions for control-flow in their applications. As shown below, this can be done by refactoring the library `MyModule`, providing two versions of the function that forces clients to use exceptions for control-flow (e.g., `janky_function`). 1) a version with the raised exceptions should have the same name as the smelly one, but with a trailing `!` (i.e., `janky_function!`); 2) Another version, without raised exceptions, should have a name identical to the original version (i.e., `janky_function`), and should return the result wrapped in a tuple. 584 | 585 | ```elixir 586 | defmodule MyModule do 587 | @moduledoc """ 588 | Refactored library 589 | """ 590 | 591 | @doc """ 592 | Refactored version without exceptions for control-flow. 593 | """ 594 | def janky_function(value) do 595 | if is_integer(value) do 596 | # ... 597 | {:ok, "Result..."} 598 | else 599 | {:error, "invalid argument. Is not integer!"} 600 | end 601 | end 602 | 603 | def janky_function!(value) do 604 | case janky_function(value) do 605 | {:ok, result} -> 606 | result 607 | 608 | {:error, message} -> 609 | raise RuntimeError, message: message 610 | end 611 | end 612 | end 613 | ``` 614 | 615 | This refactoring gives clients more freedom to decide how to proceed in the event of errors, defining what is exceptional or not in different situations. As shown next, when an error is not exceptional, clients can use specific control-flow structures, such as the `case` statement along with pattern matching. 616 | 617 | ```elixir 618 | defmodule Client do 619 | # Clients now can also choose to use control-flow structures 620 | # for control-flow when an error is not exceptional. 621 | def foo(arg) do 622 | case MyModule.janky_function(arg) do 623 | {:ok, value} -> "All good! #{value}." 624 | {:error, reason} -> "Uh oh! #{reason}." 625 | end 626 | end 627 | end 628 | 629 | # ...Use examples... 630 | 631 | Client.foo(1) 632 | # "All good! Result...." 633 | 634 | Client.foo("lucas") 635 | # "Uh oh! invalid argument. Is not integer!." 636 | ``` 637 | 638 | This example is based on code written by Tim Austin [neenjaw](https://exercism.org/profiles/neenjaw) and Angelika Tyborska [angelikatyborska](https://exercism.org/profiles/angelikatyborska). Source: [link](https://exercism.org/tracks/elixir/concepts/try-rescue) 639 | 640 | 641 | 642 | ## Untested polymorphic behaviors 643 | 644 | * __Category:__ Design-related smell. 645 | 646 | * __Problem:__ This code smell refers to functions that have protocol-dependent parameters and are therefore polymorphic. A polymorphic function itself does not represent a code smell, but some developers implement these generic functions without accompanying guard clauses, allowing to pass parameters that do not implement the required protocol or that have no meaning. 647 | 648 | * __Example:__ An instance of this code smell happens when a function uses `to_string()` to convert data received by parameter. The function `to_string()` uses the protocol `String.Chars` for conversions. Many Elixir data types (e.g., `BitString`, `Integer`, `Float`, `URI`) implement this protocol. However, as shown below, other Elixir data types (e.g., `Map`) do not implement it and can cause an error in `dasherize/1` function. Depending on the situation, this behavior can be desired or not. Besides that, it may not make sense to dasherize a `URI` or a number as shown next. 649 | 650 | ```elixir 651 | defmodule CodeSmells do 652 | def dasherize(data) do 653 | to_string(data) 654 | |> String.replace("_", "-") 655 | end 656 | end 657 | 658 | # ...Use examples... 659 | 660 | CodeSmells.dasherize("Lucas_Vegi") 661 | # "Lucas-Vegi" 662 | 663 | # <= Makes sense? 664 | CodeSmells.dasherize(10) 665 | # "10" 666 | 667 | # <= Makes sense? 668 | CodeSmells.dasherize(URI.parse("http://www.code_smells.com")) 669 | # "http://www.code-smells.com" 670 | 671 | CodeSmells.dasherize(%{last_name: "vegi", first_name: "lucas"}) 672 | # ** (Protocol.UndefinedError) protocol String.Chars not implemented 673 | # for %{first_name: "lucas", last_name: "vegi"} of type Map 674 | ``` 675 | 676 | * __Refactoring:__ There are two main alternatives to improve code affected by this smell. __1)__ You can either remove the protocol use (i.e., `to_string/1`), by adding multi-clauses on `dasherize/1` or just remove it; or __2)__ You can document that `dasherize/1` uses the protocol `String.Chars` for conversions, showing its consequences. As shown next, we refactored using the first alternative, removing the protocol and restricting `dasherize/1` parameter only to desired data types (i.e., `BitString` and `Atom`). Besides that, we use `@doc` to validate `dasherize/1` for desired inputs and to document the behavior to some types that we think don't make sense for the function (e.g., `Integer` and `URI`). 677 | 678 | ```elixir 679 | defmodule CodeSmells do 680 | @doc """ 681 | Function that converts underscores to dashes. 682 | 683 | ## Parameter 684 | data: only BitString and Atom are supported. 685 | 686 | ## Examples 687 | 688 | iex> CodeSmells.dasherize(:lucas_vegi) 689 | "lucas-vegi" 690 | 691 | iex> CodeSmells.dasherize("Lucas_Vegi") 692 | "Lucas-Vegi" 693 | 694 | iex> CodeSmells.dasherize(%{last_name: "vegi", first_name: "lucas"}) 695 | ** (FunctionClauseError) no function clause matching in CodeSmells.dasherize/1 696 | 697 | iex> CodeSmells.dasherize(URI.parse("http://www.code_smells.com")) 698 | ** (FunctionClauseError) no function clause matching in CodeSmells.dasherize/1 699 | 700 | iex> CodeSmells.dasherize(10) 701 | ** (FunctionClauseError) no function clause matching in CodeSmells.dasherize/1 702 | """ 703 | def dasherize(data) when is_atom(data) do 704 | dasherize(Atom.to_string(data)) 705 | end 706 | 707 | def dasherize(data) when is_binary(data) do 708 | String.replace(data, "_", "-") 709 | end 710 | end 711 | 712 | # ...Use examples... 713 | 714 | CodeSmells.dasherize(:lucas_vegi) 715 | # "lucas-vegi" 716 | 717 | CodeSmells.dasherize("Lucas_Vegi") 718 | # "Lucas-Vegi" 719 | 720 | CodeSmells.dasherize(10) 721 | # ** (FunctionClauseError) no function clause matching in CodeSmells.dasherize/1 722 | ``` 723 | 724 | This example is based on code written by José Valim ([@josevalim](https://github.com/josevalim)). Source: [link](http://blog.plataformatec.com.br/2014/09/writing-assertive-code-with-elixir/) 725 | 726 | 727 | 728 | ## Code organization by process 729 | 730 | * __Category:__ Design-related smell. 731 | 732 | * __Problem:__ This smell refers to code that is unnecessarily organized by processes. A process itself does not represent a code smell, but it should only be used to model runtime properties (e.g., concurrency, access to shared resources, event scheduling). When a process is used for code organization, it can create bottlenecks in the system. 733 | 734 | * __Example:__ An example of this code smell, as shown below, is a library that implements arithmetic operations (e.g., add, subtract) by means of a `GenSever` process[link](https://hexdocs.pm/elixir/master/GenServer.html). If the number of calls to this single process grows, this code organization can compromise the system performance, therefore becoming a bottleneck. 735 | 736 | ```elixir 737 | defmodule Calculator do 738 | use GenServer 739 | 740 | @moduledoc """ 741 | Calculator that performs two basic arithmetic operations. 742 | This code is unnecessarily organized by a GenServer process. 743 | """ 744 | 745 | @doc """ 746 | Function to perform the sum of two values. 747 | """ 748 | def add(a, b, pid) do 749 | GenServer.call(pid, {:add, a, b}) 750 | end 751 | 752 | @doc """ 753 | Function to perform subtraction of two values. 754 | """ 755 | def subtract(a, b, pid) do 756 | GenServer.call(pid, {:subtract, a, b}) 757 | end 758 | 759 | def init(init_arg) do 760 | {:ok, init_arg} 761 | end 762 | 763 | def handle_call({:add, a, b}, _from, state) do 764 | {:reply, a + b, state} 765 | end 766 | 767 | def handle_call({:subtract, a, b}, _from, state) do 768 | {:reply, a - b, state} 769 | end 770 | end 771 | 772 | # Start a generic server process 773 | {:ok, pid} = GenServer.start_link(Calculator, :init) 774 | # {:ok, #PID<0.132.0>} 775 | 776 | # ...Use examples... 777 | Calculator.add(1, 5, pid) 778 | # 6 779 | 780 | Calculator.subtract(2, 3, pid) 781 | # -1 782 | ``` 783 | 784 | * __Refactoring:__ In Elixir, as shown next, code organization must be done only by modules and functions. Whenever possible, a library should not impose specific behavior (such as parallelization) on its clients. It is better to delegate this behavioral decision to the developers of clients, thus increasing the potential for code reuse of a library. 785 | 786 | ```elixir 787 | defmodule Calculator do 788 | def add(a, b) do 789 | a + b 790 | end 791 | 792 | def subtract(a, b) do 793 | a - b 794 | end 795 | end 796 | 797 | # ...Use examples... 798 | 799 | Calculator.add(1, 5) 800 | # 6 801 | 802 | Calculator.subtract(2, 3) 803 | # -1 804 | ``` 805 | 806 | This example is based on code provided in Elixir's official documentation. Source: [link](https://hexdocs.pm/elixir/main/library-guidelines.html#avoid-using-processes-for-code-organization) 807 | 808 | 809 | 810 | ## Large code generation by macros 811 | 812 | * __Category:__ Design-related smell. 813 | 814 | * __Note:__ This smell was suggested by the community via issues ([#13](https://github.com/lucasvegi/Elixir-Code-Smells/issues/13)). 815 | 816 | * __Problem:__ This code smell is related to `macros` that generate too much code. When a `macro` provides a large code generation, it impacts how the compiler or the runtime works. The reason for this is that Elixir may have to expand, compile, and execute a code multiple times, which will make compilation slower. 817 | 818 | * __Example:__ The code shown below is an example of this smell. Imagine you are defining a router for a web application, where you could have macros like `get/2`. On every invocation of the macro, which can be hundreds, the code inside `get/2` will be expanded and compiled, which can generate a large volume of code in total. 819 | 820 | ```elixir 821 | defmodule Routes do 822 | defmacro get(route, handler) do 823 | quote do 824 | route = unquote(route) 825 | handler = unquote(handler) 826 | 827 | if not is_binary(route) do 828 | raise ArgumentError, "route must be a binary" 829 | end 830 | 831 | if not is_atom(handler) do 832 | raise ArgumentError, "route must be a module" 833 | end 834 | 835 | @store_route_for_compilation {route, handler} 836 | end 837 | end 838 | end 839 | ``` 840 | 841 | * __Refactoring:__ To remove this code smell, the developer must simplify the `macro`, delegating to other functions part of its work. As shown below, by encapsulating in the function `__define__/3` the functionality pre-existing inside the `quote`, we reduce the code that is expanded and compiled on every invocation of the `macro`, and instead we dispatch to a function to do the bulk of the work. 842 | 843 | ```elixir 844 | defmodule Routes do 845 | defmacro get(route, handler) do 846 | quote do 847 | Routes.__define__(__MODULE__, unquote(route), unquote(handler)) 848 | end 849 | end 850 | 851 | def __define__(module, route, handler) do 852 | if not is_binary(route) do 853 | raise ArgumentError, "route must be a binary" 854 | end 855 | 856 | if not is_atom(handler) do 857 | raise ArgumentError, "route must be a module" 858 | end 859 | 860 | Module.put_attribute(module, :store_route_for_compilation, {route, handler}) 861 | end 862 | end 863 | ``` 864 | 865 | This example and the refactoring are proposed by José Valim ([@josevalim](https://github.com/josevalim)) 866 | 867 | 868 | 869 | ## Data manipulation by migration 870 | 871 | * __Category:__ Design-related smell. 872 | 873 | * __Problem:__ This code smell refers to modules that perform both data and structural changes in a database schema via `Ecto.Migration`[link](https://hexdocs.pm/ecto_sql/Ecto.Migration.html). Migrations must be used exclusively to modify a database schema over time (e.g., by including or excluding columns and tables). When this responsibility is mixed with data manipulation code, the module becomes less cohesive, more difficult to test, and therefore more prone to bugs. 874 | 875 | * __Example:__ An example of this code smell is when an `Ecto.Migration` is used simultaneously to alter a table, adding a new column to it, and also to update all pre-existing data in that table, assigning a value to this new column. As shown below, in addition to adding the `is_custom_shop` column in the `guitars` table, this `Ecto.Migration` changes the value of this column for some specific guitar models. 876 | 877 | ```elixir 878 | defmodule GuitarStore.Repo.Migrations.AddIsCustomShopToGuitars do 879 | use Ecto.Migration 880 | 881 | import Ecto.Query 882 | alias GuitarStore.Inventory.Guitar 883 | alias GuitarStore.Repo 884 | 885 | @doc """ 886 | A function that modifies the structure of table "guitars", 887 | adding column "is_custom_shop" to it. By default, all data 888 | pre-stored in this table will have the value false stored 889 | in this new column. 890 | 891 | Also, this function updates the "is_custom_shop" column value 892 | of some guitar models to true. 893 | """ 894 | def change do 895 | alter table("guitars") do 896 | add :is_custom_shop, :boolean, default: false 897 | end 898 | create index("guitars", ["is_custom_shop"]) 899 | 900 | custom_shop_entries() 901 | |> Enum.map(&update_guitars/1) 902 | end 903 | 904 | @doc """ 905 | A function that updates values of column "is_custom_shop" to true. 906 | """ 907 | defp update_guitars({make, model, year}) do 908 | from(g in Guitar, 909 | where: g.make == ^make and g.model == ^model and g.year == ^year, 910 | select: g 911 | ) 912 | |> Repo.update_all(set: [is_custom_shop: true]) 913 | end 914 | 915 | @doc """ 916 | Function that defines which guitar models that need to have the values 917 | of the "is_custom_shop" column updated to true. 918 | """ 919 | defp custom_shop_entries() do 920 | [ 921 | {"Gibson", "SG", 1999}, 922 | {"Fender", "Telecaster", 2020} 923 | ] 924 | end 925 | end 926 | ``` 927 | 928 | You can run this smelly migration above by going to the root of your project and typing the next command via console: 929 | 930 | ```elixir 931 | mix ecto.migrate 932 | ``` 933 | 934 | * __Refactoring:__ To remove this code smell, it is necessary to separate the data manipulation in a `mix task` [link](https://hexdocs.pm/mix/Mix.html#module-mix-task) different from the module that performs the structural changes in the database via `Ecto.Migration`. This separation of responsibilities is a best practice for increasing code testability. As shown below, the module `AddIsCustomShopToGuitars` now use `Ecto.Migration` only to perform structural changes in the database schema: 935 | 936 | ```elixir 937 | defmodule GuitarStore.Repo.Migrations.AddIsCustomShopToGuitars do 938 | use Ecto.Migration 939 | 940 | @doc """ 941 | A function that modifies the structure of table "guitars", 942 | adding column "is_custom_shop" to it. By default, all data 943 | pre-stored in this table will have the value false stored 944 | in this new column. 945 | """ 946 | def change do 947 | alter table("guitars") do 948 | add :is_custom_shop, :boolean, default: false 949 | end 950 | 951 | create index("guitars", ["is_custom_shop"]) 952 | end 953 | end 954 | ``` 955 | 956 | Furthermore, the new mix task `PopulateIsCustomShop`, shown next, has only the responsibility to perform data manipulation, thus improving testability: 957 | 958 | ```elixir 959 | defmodule Mix.Tasks.PopulateIsCustomShop do 960 | @shortdoc "Populates is_custom_shop column" 961 | 962 | use Mix.Task 963 | 964 | import Ecto.Query 965 | alias GuitarStore.Inventory.Guitar 966 | alias GuitarStore.Repo 967 | 968 | @requirements ["app.start"] 969 | 970 | def run(_) do 971 | custom_shop_entries() 972 | |> Enum.map(&update_guitars/1) 973 | end 974 | 975 | defp update_guitars({make, model, year}) do 976 | from(g in Guitar, 977 | where: g.make == ^make and g.model == ^model and g.year == ^year, 978 | select: g 979 | ) 980 | |> Repo.update_all(set: [is_custom_shop: true]) 981 | end 982 | 983 | defp custom_shop_entries() do 984 | [ 985 | {"Gibson", "SG", 1999}, 986 | {"Fender", "Telecaster", 2020} 987 | ] 988 | end 989 | end 990 | ``` 991 | 992 | You can run this `mix task` above by typing the next command via console: 993 | 994 | ```elixir 995 | mix populate_is_custom_shop 996 | ``` 997 | 998 | This example is based on code originally written by Carlos Souza. Source: [link](https://www.idopterlabs.com.br/post/criando-uma-mix-task-em-elixir) 999 | 1000 | 1001 | 1002 | ## Using App Configuration for libraries 1003 | 1004 | * __Category:__ Design-related smells. 1005 | 1006 | * __Note:__ Formerly known as "App configuration for code libs". 1007 | 1008 | * __Problem:__ The `Application Environment` [link](https://hexdocs.pm/elixir/1.13/Config.html) is a global configuration mechanism and therefore can be used to parameterize 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 a code smell 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 its functions with different behaviors in different places in the code. Libraries are created to foster code reuse, so this kind of limitation imposed by global configurations can be problematic in this scenario. 1009 | 1010 | * __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: 1011 | 1012 | ```elixir 1013 | import Config 1014 | 1015 | config :app_config, 1016 | parts: 3 1017 | 1018 | import_config "#{config_env()}.exs" 1019 | ``` 1020 | 1021 | One of the functions implemented by the `DashSplitter` library is `split/1`. This function has the purpose of separating a string received via 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: 1022 | 1023 | ```elixir 1024 | defmodule DashSplitter do 1025 | def split(string) when is_binary(string) do 1026 | parts = Application.fetch_env!(:app_config, :parts) # <= retrieve parameterized value 1027 | String.split(string, "-", parts: parts) # <= parts: 3 1028 | end 1029 | end 1030 | ``` 1031 | 1032 | Due to this type of global configuration used by the `DashSplitter` library, all applications dependent on it can only use the `split/1` function with identical behavior in relation to 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: 1033 | 1034 | ```elixir 1035 | iex(1)> DashSplitter.split("Lucas-Francisco-Vegi") 1036 | ["Lucas", "Francisco", "Vegi"] 1037 | 1038 | iex(2)> DashSplitter.split("Lucas-Francisco-da-Matta-Vegi") 1039 | ["Lucas", "Francisco", "da-Matta-Vegi"] 1040 | ``` 1041 | 1042 | * __Refactoring:__ To remove this code smell 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: 1043 | 1044 | ```elixir 1045 | defmodule DashSplitter do 1046 | def split(string, opts \\ []) when is_binary(string) and is_list(opts) do 1047 | # <= default config of parts == 2 1048 | parts = Keyword.get(opts, :parts, 2) 1049 | String.split(string, "-", parts: parts) 1050 | end 1051 | end 1052 | 1053 | # ...Use examples... 1054 | 1055 | DashSplitter.split("Lucas-Francisco-da-Matta-Vegi", parts: 5) 1056 | # ["Lucas", "Francisco", "da", "Matta", "Vegi"] 1057 | 1058 | # <= default config is used! 1059 | DashSplitter.split("Lucas-Francisco-da-Matta-Vegi") 1060 | # ["Lucas", "Francisco-da-Matta-Vegi"] 1061 | ``` 1062 | 1063 | These examples are based on code provided in Elixir's official documentation. Source: [link](https://hexdocs.pm/elixir/main/library-guidelines.html#avoid-application-configuration) 1064 | 1065 | 1066 | 1067 | ## Compile-time global configuration 1068 | 1069 | * __Category:__ Design-related smells. 1070 | 1071 | * __Note:__ Formerly known as "Compile-time app configuration". 1072 | 1073 | * __Problem:__ As explained in the description of [App configuration for code libs](#app-configuration-for-code-libs), the `Application Environment` can be used to parameterize values in an Elixir system. Although it is not a good practice to use this mechanism in the implementation of libraries, sometimes this can be unavoidable. If these parameterized values are assigned to `module attributes`, it can be especially problematic. As `module attribute` values are defined at compile-time, when trying to assign `Application Environment` values to these attributes, warnings or errors can be triggered by Elixir. This happens because, when defining module attributes at compile time, the `Application Environment` is not yet available in memory. 1074 | 1075 | * __Example:__ The `DashSplitter` module represents a library. This module has an attribute `@parts` that has its constant value defined at compile-time by calling `Application.fetch_env!/2`. The `split/1` function, implemented by this library, has the purpose of separating a string received via 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 by the module attribute `@parts`, as shown next: 1076 | 1077 | ```elixir 1078 | defmodule DashSplitter do 1079 | @parts Application.fetch_env!(:app_config, :parts) # <= define module attribute 1080 | # at compile-time 1081 | def split(string) when is_binary(string) do 1082 | String.split(string, "-", parts: @parts) #<= reading from a module attribute 1083 | end 1084 | 1085 | end 1086 | ``` 1087 | 1088 | Due to this compile-time configuration based on the `Application Environment` mechanism, Elixir can raise warnings or errors, as shown next, during compilation: 1089 | 1090 | ```elixir 1091 | warning: Application.fetch_env!/2 is discouraged in the module body, 1092 | use Application.compile_env/3 instead... 1093 | 1094 | ** (ArgumentError) could not fetch application environment :parts 1095 | for application :app_config because the application was not loaded nor 1096 | configured 1097 | ``` 1098 | 1099 | ```elixir 1100 | defmodule DashSplitter do 1101 | # <= define module attribute 1102 | @parts Application.fetch_env!(:app_config, :parts) 1103 | # at compile-time 1104 | def split(string) when is_binary(string) do 1105 | # <= reading from a module attribute 1106 | String.split(string, "-", parts: @parts) 1107 | end 1108 | end 1109 | ``` 1110 | 1111 | * __Refactoring:__ To remove this code smell, when it is really unavoidable to use the `Application Environment` mechanism to configure library functions, this should be done at runtime and not during compilation. That is, instead of calling `Application.fetch_env!(:app_config, :parts)` at compile-time to set `@parts`, this function must be called at runtime within `split/1`. This will mitigate the risk that `Application Environment` is not yet available in memory when it is necessary to use it. Another possible refactoring, as shown below, is to replace the use of the `Application.fetch_env!/2` function to define `@parts`, with the `Application.compile_env/3`. The third parameter of `Application.compile_env/3` defines a default value that is returned whenever that `Application Environment` is not available in memory during the definition of `@parts`. This prevents Elixir from raising an error at compile-time: 1112 | 1113 | ```elixir 1114 | defmodule DashSplitter do 1115 | # <= default value 3 prevents an error! 1116 | @parts Application.compile_env(:app_config, :parts, 3) 1117 | 1118 | def split(string) when is_binary(string) do 1119 | # <= reading from a module attribute 1120 | String.split(string, "-", parts: @parts) 1121 | end 1122 | end 1123 | ``` 1124 | 1125 | These examples are based on code provided in Elixir's official documentation. Source: [link](https://hexdocs.pm/elixir/main/library-guidelines.html#avoid-application-configuration) 1126 | 1127 | * __Remark:__ This code smell can be detected by [Credo](https://hexdocs.pm/credo/overview.html), a static code analysis tool. During its checks, Credo raises this [warning](https://hexdocs.pm/credo/Credo.Check.Warning.ApplicationConfigInModuleAttribute.html) when this smell is found. 1128 | 1129 | 1130 | 1131 | ## "Use" instead of "import" 1132 | 1133 | * __Category:__ Design-related smells. 1134 | 1135 | * __Note:__ Formerly known as "Dependency with "use" when an "import" is enough". 1136 | 1137 | * __Problem:__ Elixir has mechanisms such as `import`, `alias`, and `use` to establish dependencies between modules. Establishing dependencies allows a module to call functions from other modules, facilitating code reuse. A code implemented with these mechanisms does not characterize a smell by itself; however, while the `import` and `alias` directives have lexical scope and only facilitate that a module to use functions of another, the `use` directive has a broader scope, something that can be problematic. The `use` directive allows a module to inject any type of code into another, including propagating dependencies. In this way, using the `use` directive makes code readability worse, because to understand exactly what will happen when it references a module, it is necessary to have knowledge of the internal details of the referenced module. 1138 | 1139 | * __Example:__ The code shown below is an example of this smell. Three different modules were defined -- `ModuleA`, `Library`, and `ClientApp`. `ClientApp` is reusing code from the `Library` via the `use` directive, but is unaware of its internal details. Therefore, when `Library` is referenced by `ClientApp`, it injects into `ClientApp` all the content present in its `__using__/1` macro. Due to the decreased readability of the code and the lack of knowledge of the internal details of the `Library`, `ClientApp` defines a local function `foo/0`. This will generate a conflict as `ModuleA` also has a function `foo/0`; when `ClientApp` referenced `Library` via the `use` directive, it has a dependency for `ModuleA` propagated to itself: 1140 | 1141 | ```elixir 1142 | defmodule ModuleA do 1143 | def foo do 1144 | "From Module A" 1145 | end 1146 | end 1147 | ``` 1148 | 1149 | ```elixir 1150 | defmodule Library do 1151 | defmacro __using__(_opts) do 1152 | quote do 1153 | # <= propagating dependencies! 1154 | import ModuleA 1155 | 1156 | def from_lib do 1157 | "From Library" 1158 | end 1159 | end 1160 | end 1161 | 1162 | def from_lib do 1163 | "From Library" 1164 | end 1165 | end 1166 | ``` 1167 | 1168 | When we try to compile `ClientApp`, Elixir will detect the conflict and throw the following error: 1169 | 1170 | 1171 | 1172 | ```elixir 1173 | iex(1)> c("client_app.ex") 1174 | 1175 | ** (CompileError) client_app.ex:4: imported ModuleA.foo/0 conflicts with local function 1176 | ``` 1177 | 1178 | ```elixir 1179 | defmodule ClientApp do 1180 | use Library 1181 | 1182 | def foo do 1183 | "Local function from client app" 1184 | end 1185 | 1186 | def from_client_app do 1187 | from_lib() <> " - " <> foo() 1188 | end 1189 | end 1190 | ``` 1191 | 1192 | * __Refactoring:__ To remove this code smell, it may be possible to replace `use` with `alias` or `import` when creating a dependency between an application and a library. This will make code behavior clearer, due to improved readability. In the following code, `ClientApp` was refactored in this way, and with that, the conflict as previously shown no longer exists: 1193 | 1194 | ```elixir 1195 | defmodule ClientApp do 1196 | import Library 1197 | 1198 | def foo do 1199 | "Local function from client app" 1200 | end 1201 | 1202 | def from_client_app do 1203 | from_lib() <> " - " <> foo() 1204 | end 1205 | end 1206 | 1207 | # ...Uses example... 1208 | 1209 | ClientApp.from_client_app() 1210 | # "From Library - Local function from client app" 1211 | ``` 1212 | 1213 | These examples are based on code provided in Elixir's official documentation. Source: [link](https://hexdocs.pm/elixir/main/library-guidelines.html#avoid-use-when-an-import-is-enough) 1214 | 1215 | ## Low-level concerns smells 1216 | 1217 | Low-level concerns smells are more simple than design-related smells and affect a small part of the code. Next, all 9 different smells classified as low-level concerns are explained and exemplified: 1218 | 1219 | 1220 | 1221 | ## Working with invalid data 1222 | 1223 | * __Category:__ Low-level concerns smells. 1224 | 1225 | * __Problem:__ This code smell refers to a function that does not validate its parameters' types and therefore can produce internal non-predicted behavior. When an error is raised inside a function due to an invalid parameter value, this can confuse the developers and make it harder to locate and fix the error. 1226 | 1227 | * __Example:__ An example of this code smell is when a function receives an invalid parameter and then passes it to a function from a third-party library. This will cause an error (raised deep inside the library function), which may be confusing for the developer who is working with invalid data. As shown next, the function `foo/1` is a client of a third-party library and doesn't validate its parameters at the boundary. In this way, it is possible that invalid data will be passed from `foo/1` to the library, causing a mysterious error. 1228 | 1229 | ```elixir 1230 | defmodule MyApp do 1231 | alias ThirdPartyLibrary, as: Library 1232 | 1233 | def foo(invalid_data) do 1234 | #...some code... 1235 | Library.sum(1, invalid_data) 1236 | #...some code... 1237 | end 1238 | end 1239 | 1240 | #...Use examples... 1241 | 1242 | # with valid data is ok 1243 | iex(1)> MyApp.foo(2) 1244 | 3 1245 | 1246 | #with invalid data cause a confusing error deep inside 1247 | iex(2)> MyApp.foo("Lucas") 1248 | ** (ArithmeticError) bad argument in arithmetic expression: 1 + "Lucas" 1249 | :erlang.+(1, "Lucas") 1250 | library.ex:3: ThirdPartyLibrary.sum/2 1251 | ``` 1252 | 1253 | * __Refactoring:__ To remove this code smell, client code must validate input parameters at the boundary with the user, via guard clauses or pattern matching. This will prevent errors from occurring deeply, making them easier to understand. This refactoring will also allow libraries to be implemented without worrying about creating internal protection mechanisms. The next code illustrates the refactoring of `foo/1`, removing this smell: 1254 | 1255 | ```elixir 1256 | defmodule MyApp do 1257 | alias ThirdPartyLibrary, as: Library 1258 | 1259 | def foo(data) when is_integer(data) do 1260 | #...some code... 1261 | Library.sum(1, data) 1262 | #...some code... 1263 | end 1264 | end 1265 | 1266 | #...Use examples... 1267 | 1268 | #with valid data is ok 1269 | iex(1)> MyApp.foo(2) 1270 | 3 1271 | 1272 | # with invalid data errors are easy to locate and fix 1273 | iex(2)> MyApp.foo("Lucas") 1274 | ** (FunctionClauseError) no function clause matching in MyApp.foo/1 1275 | 1276 | The following arguments were given to MyApp.foo/1: 1277 | 1278 | # 1 1279 | "Lucas" 1280 | 1281 | my_app.ex:6: MyApp.foo/1 1282 | ``` 1283 | 1284 | This example is based on code provided in Elixir's official documentation. Source: [link](https://hexdocs.pm/elixir/main/library-guidelines.html#avoid-working-with-invalid-data) 1285 | 1286 | 1287 | 1288 | ## Complex branching 1289 | 1290 | * __Category:__ Low-level concerns smell. 1291 | 1292 | * __Note:__ Formerly known as "Complex API error handling". 1293 | 1294 | * __Problem:__ When a function assumes the responsibility of handling multiple errors alone, it can increase its cyclomatic complexity (metric of control-flow) and become incomprehensible. This situation can configure a specific instance of "Long function", a traditional code smell, but has implications of its own. Under these circumstances, this function could get very confusing, difficult to maintain and test, and therefore bug-proneness. 1295 | 1296 | * __Example:__ An example of this code smell is when a function uses the `case` control-flow structure or other similar constructs (e.g., `cond`, or `receive`) to handle multiple variations of response types returned by the same API endpoint. This practice can make the function more complex, long, and difficult to understand, as shown next. 1297 | 1298 | ```elixir 1299 | def get_customer(customer_id) do 1300 | case get("/customers/#{customer_id}") do 1301 | {:ok, %Tesla.Env{status: 200, body: body}} -> {:ok, body} 1302 | {:ok, %Tesla.Env{body: body}} -> {:error, body} 1303 | {:error, _} = other -> other 1304 | end 1305 | end 1306 | ``` 1307 | 1308 | Although `get_customer/1` is not really long in this example, it could be. Thinking about this more complex scenario, where a large number of different responses can be provided to the same endpoint, is not a good idea to concentrate all on a single function. This is a risky scenario, where a little typo, or any problem introduced by the programmer in handling a response type, could eventually compromise the handling of all responses from the endpoint (if the function raises an exception, for example). 1309 | 1310 | * __Refactoring:__ As shown below, in this situation, instead of concentrating all handlings within the same function, creating a complex branching, it is better to delegate each branch (handling of a response type) to a different private function. In this way, the code will be cleaner, more concise, and readable. 1311 | 1312 | ```elixir 1313 | def get_customer(customer_id) when is_integer(customer_id) do 1314 | case get("/customers/#{customer_id}") do 1315 | {:ok, %Tesla.Env{status: 200, body: body}} -> success_api_response(body) 1316 | {:ok, %Tesla.Env{body: body}} -> x_error_api_response(body) 1317 | {:error, _} = other -> y_error_api_response(other) 1318 | end 1319 | end 1320 | 1321 | defp success_api_response(body) do 1322 | {:ok, body} 1323 | end 1324 | 1325 | defp x_error_api_response(body) do 1326 | {:error, body} 1327 | end 1328 | 1329 | defp y_error_api_response(other) do 1330 | other 1331 | end 1332 | ``` 1333 | 1334 | While this example of refactoring `get_customer/1` might seem quite more verbose than the original code, remember to imagine a scenario where `get_customer/1` is responsible for handling a number much larger than three different types of possible responses. This is the smelly scenario! 1335 | 1336 | This example is based on code written by Zack [MrDoops](https://elixirforum.com/u/MrDoops) and Dimitar Panayotov [dimitarvp](https://elixirforum.com/u/dimitarvp). Source: [link](https://elixirforum.com/t/what-are-sort-of-smells-do-you-tend-to-find-in-elixir-code/14971). We got suggestions from José Valim ([@josevalim](https://github.com/josevalim)) on the refactoring. 1337 | 1338 | 1339 | 1340 | ## Complex else clauses in with 1341 | 1342 | * __Category:__ Low-level concerns smell. 1343 | 1344 | * __Note:__ This smell was suggested by the community via issues ([#7](https://github.com/lucasvegi/Elixir-Code-Smells/issues/7)). 1345 | 1346 | * __Problem:__ This code smell refers to `with` statements that flatten all its error clauses into a single complex `else` block. This situation is harmful to the code readability and maintainability because difficult to know from which clause the error value came. 1347 | 1348 | * __Example:__ An example of this code smell, as shown below, is a function `open_decoded_file/1` that read a base 64 encoded string content from a file and returns a decoded binary string. This function uses a `with` statement that needs to handle two possible errors, all of which are concentrated in a single complex `else` block. 1349 | 1350 | ```elixir 1351 | defmodule ElseClauses do 1352 | def open_decoded_file(path) do 1353 | with {:ok, encoded} <- File.read(path), 1354 | {:ok, value} <- Base.decode64(encoded) do 1355 | value 1356 | else 1357 | {:error, _} -> :badfile 1358 | :error -> :badencoding 1359 | end 1360 | end 1361 | end 1362 | ``` 1363 | 1364 | * __Refactoring:__ As shown below, in this situation, instead of concentrating all error handlings within a single complex `else` block, it is better to normalize the return types in specific private functions. In this way, due to its organization, the code will be cleaner and more readable. 1365 | 1366 | ```elixir 1367 | defmodule ElseClausesRefactoring do 1368 | def open_decoded_file(path) do 1369 | with {:ok, encoded} <- file_read(path), 1370 | {:ok, value} <- base_decode64(encoded) do 1371 | value 1372 | end 1373 | end 1374 | 1375 | defp file_read(path) do 1376 | case File.read(path) do 1377 | {:ok, contents} -> {:ok, contents} 1378 | {:error, _} -> :badfile 1379 | end 1380 | end 1381 | 1382 | defp base_decode64(contents) do 1383 | case Base.decode64(contents) do 1384 | {:ok, contents} -> {:ok, contents} 1385 | :error -> :badencoding 1386 | end 1387 | end 1388 | end 1389 | ``` 1390 | 1391 | This example and the refactoring are proposed by José Valim ([@josevalim](https://github.com/josevalim)) 1392 | 1393 | 1394 | 1395 | ## Alternative return types 1396 | 1397 | * __Category:__ Low-level concerns smell. 1398 | 1399 | * __Note:__ This smell was suggested by the community via issues ([#6](https://github.com/lucasvegi/Elixir-Code-Smells/issues/6)). 1400 | 1401 | * __Problem:__ This code smell refers to functions that receive options (e.g., `keyword list`) parameters that drastically change its return type. Because options are optional and sometimes set dynamically, if they change the return type it may be hard to understand what the function actually returns. 1402 | 1403 | * __Example:__ An example of this code smell, as shown below, is when a library (e.g. `AlternativeInteger`) has a multi-clause function `parse/2` with many alternative return types. Depending on the options received as a parameter, the function will have a different return type. 1404 | 1405 | ```elixir 1406 | defmodule AlternativeInteger do 1407 | def parse(string, opts) when is_list(opts) do 1408 | case opts[:discard_rest] do 1409 | # only an integer value convert from string parameter 1410 | true -> String.to_integer(string) 1411 | # another return type (e.g., tuple) 1412 | _ -> {String.to_integer(string)} 1413 | end 1414 | end 1415 | 1416 | def parse(string, opts \\ :default) do 1417 | # another return type (e.g., tuple) 1418 | end 1419 | end 1420 | 1421 | # ...Use examples... 1422 | 1423 | AlternativeInteger.parse("13") 1424 | # {13, "..."} 1425 | 1426 | AlternativeInteger.parse("13", discard_rest: true) 1427 | # 13 1428 | 1429 | AlternativeInteger.parse("13", discard_rest: false) 1430 | # {13, "..."} 1431 | ``` 1432 | 1433 | * __Refactoring:__ To refactor this smell, as shown next, it's better to add in the library a specific function for each return type (e.g., `parse_no_rest/1`), no longer delegating this to an options parameter. 1434 | 1435 | ```elixir 1436 | defmodule AlternativeInteger do 1437 | def parse_no_rest(string) do 1438 | # only an integer value convert from string parameter 1439 | String.to_integer(string) 1440 | end 1441 | 1442 | def parse(string) do 1443 | # another return type (e.g., tuple) 1444 | {String.to_integer(string)} 1445 | end 1446 | end 1447 | 1448 | # ...Use examples... 1449 | 1450 | AlternativeInteger.parse("13") 1451 | # {13, "..."} 1452 | 1453 | AlternativeInteger.parse_no_rest("13") 1454 | # 13 1455 | ``` 1456 | 1457 | This example and the refactoring are proposed by José Valim ([@josevalim](https://github.com/josevalim)) 1458 | 1459 | 1460 | 1461 | ## Accessing non-existent Map/Struct fields 1462 | 1463 | * __Category:__ Low-level concerns smells. 1464 | 1465 | * __Note:__ Formerly known as "Map/struct dynamic access". 1466 | 1467 | * __Problem:__ In Elixir, it is possible to access values from `Maps`, which are key-value data structures, either strictly or dynamically. When trying to dynamically access the value of a key from a `Map`, if the informed key does not exist, a null value (`nil`) will be returned. This return can be confusing and does not allow developers to conclude whether the key is non-existent in the `Map` or just has no bound value. In this way, this code smell may cause bugs in the code. 1468 | 1469 | * __Example:__ The code shown below is an example of this smell. The function `plot/1` tries to draw a graphic to represent the position of a point in a cartesian plane. This function receives a parameter of `Map` type with the point attributes, which can be a point of a 2D or 3D cartesian coordinate system. To decide if a point is 2D or 3D, this function uses dynamic access to retrieve values of the `Map` keys: 1470 | 1471 | ```elixir 1472 | defmodule Graphics do 1473 | def plot(point) do 1474 | # ...some code... 1475 | 1476 | # Dynamic access to use point values 1477 | {point[:x], point[:y], point[:z]} 1478 | 1479 | # ...some code... 1480 | end 1481 | end 1482 | 1483 | # ...Use examples... 1484 | point_2d = %{x: 2, y: 3} 1485 | # %{x: 2, y: 3} 1486 | 1487 | point_3d = %{x: 5, y: 6, z: nil} 1488 | # %{x: 5, y: 6, z: nil} 1489 | 1490 | Graphics.plot(point_2d) 1491 | # {2, 3, nil} # <= ambiguous return 1492 | 1493 | Graphics.plot(point_3d) 1494 | # {5, 6, nil} 1495 | ``` 1496 | 1497 | 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. 1498 | 1499 | * __Refactoring:__ To remove this code smell, whenever a `Map` has keys of `Atom` type, replace the dynamic access to its values per strict access. When a non-existent key is strictly accessed, Elixir raises an error immediately, allowing developers to find bugs faster. The next code illustrates the refactoring of `plot/1`, removing this smell: 1500 | 1501 | ```elixir 1502 | defmodule Graphics do 1503 | def plot(point) do 1504 | # ...some code... 1505 | 1506 | # Strict access to use point values 1507 | {point.x, point.y, point.z} 1508 | 1509 | # ...some code... 1510 | end 1511 | end 1512 | 1513 | # ...Use examples... 1514 | point_2d = %{x: 2, y: 3} 1515 | # %{x: 2, y: 3} 1516 | 1517 | point_3d = %{x: 5, y: 6, z: nil} 1518 | # %{x: 5, y: 6, z: nil} 1519 | 1520 | Graphics.plot(point_2d) 1521 | # ** (KeyError) key :z not found in: %{x: 2, y: 3} # <= explicitly warns that 1522 | # graphic.ex:6: Graphics.plot/1 # <= the z key does not exist! 1523 | 1524 | Graphics.plot(point_3d) 1525 | # {5, 6, nil} 1526 | ``` 1527 | 1528 | As shown below, another alternative to refactor this smell is to replace a `Map` with a `struct` (named map). By default, structs only support strict access to values. In this way, accesses will always return clear and objective results: 1529 | 1530 | ```elixir 1531 | defmodule Point do 1532 | @enforce_keys [:x, :y] 1533 | defstruct x: nil, y: nil 1534 | end 1535 | ``` 1536 | 1537 | ```elixir 1538 | # ...Use examples... 1539 | point = %Point{x: 2, y: 3} 1540 | IO.inspect(point) 1541 | # %Point{x: 2, y: 3} 1542 | 1543 | # <= strict access to use point values 1544 | point.x 1545 | # 2 1546 | 1547 | # <= trying to access a non-existent key 1548 | point.z 1549 | # ** (KeyError) key :z not found in: %Point{x: 2, y: 3} 1550 | 1551 | # <= by default, struct does not support dynamic access 1552 | point[:x] 1553 | # ** (UndefinedFunctionError) ... (Point does not implement the Access behaviour) 1554 | ``` 1555 | 1556 | These examples are based on code written by José Valim ([@josevalim](https://github.com/josevalim)). Source: [link](http://blog.plataformatec.com.br/2014/09/writing-assertive-code-with-elixir/) 1557 | 1558 | 1559 | 1560 | ## Speculative Assumptions 1561 | 1562 | * __Category:__ Low-level concerns smells. 1563 | 1564 | * __Note:__ Formerly known as "Unplanned value extraction". 1565 | 1566 | * __Problem:__ Overall, Elixir application’s are composed of many supervised processes, so the effects of an error will be localized in a single process, not propagating to the entire application. A supervisor will detect the failing process, and restart it at that level. For this type of design to behave well, it's important that problematic code crashes when it fails to fulfill its purpose. However, some code may have undesired behavior making many assumptions we have not really planned for, such as being able to return incorrect values instead of forcing a crash. These speculative assumptions can give a false impression that the code is working correctly. 1567 | 1568 | * __Example:__ The code shown below is an example of this smell. The function `get_value/2` tries to extract a value from a specific key of a URL query string. As it is not implemented using pattern matching, `get_value/2` always returns a value, regardless of the format of the URL query string passed as a parameter in the call. Sometimes the returned value will be valid; however, if a URL query string with an unexpected format is used in the call, `get_value/2` will extract incorrect values from it: 1569 | 1570 | ```elixir 1571 | defmodule Extract do 1572 | @doc """ 1573 | Extract value from a key in a URL query string. 1574 | """ 1575 | def get_value(string, desired_key) do 1576 | parts = String.split(string, "&") 1577 | 1578 | Enum.find_value(parts, fn pair -> 1579 | key_value = String.split(pair, "=") 1580 | Enum.at(key_value, 0) == desired_key && Enum.at(key_value, 1) 1581 | end) 1582 | end 1583 | end 1584 | 1585 | # ...Use examples... 1586 | 1587 | # URL query string according to with the planned format - OK! 1588 | Extract.get_value("name=Lucas&university=UFMG&lab=ASERG", "lab") 1589 | # "ASERG" 1590 | 1591 | Extract.get_value("name=Lucas&university=UFMG&lab=ASERG", "university") 1592 | # "UFMG" 1593 | 1594 | # Unplanned URL query string format - Unplanned value extraction! 1595 | Extract.get_value("name=Lucas&university=institution=UFMG&lab=ASERG", "university") 1596 | # "institution" # <= why not "institution=UFMG"? or only "UFMG"? 1597 | ``` 1598 | 1599 | * __Refactoring:__ To remove this code smell, `get_value/2` can be refactored through the use of pattern matching. So, if an unexpected URL query string format is used, the function will be crash instead of returning an invalid value. This behavior, shown below, will allow clients to decide how to handle these errors and will not give a false impression that the code is working correctly when unexpected values are extracted: 1600 | 1601 | ```elixir 1602 | defmodule Extract do 1603 | @doc """ 1604 | Extract value from a key in a URL query string. 1605 | Refactored by using pattern matching. 1606 | """ 1607 | def get_value(string, desired_key) do 1608 | parts = String.split(string, "&") 1609 | 1610 | Enum.find_value(parts, fn pair -> 1611 | # <= pattern matching 1612 | [key, value] = String.split(pair, "=") 1613 | key == desired_key && value 1614 | end) 1615 | end 1616 | end 1617 | 1618 | # ...Use examples... 1619 | 1620 | # URL query string according to with the planned format - OK! 1621 | Extract.get_value("name=Lucas&university=UFMG&lab=ASERG", "name") 1622 | # "Lucas" 1623 | 1624 | # Unplanned URL query string format - Crash explaining the problem to the client! 1625 | Extract.get_value("name=Lucas&university=institution=UFMG&lab=ASERG", "university") 1626 | # ** (MatchError) no match of right hand side value: ["university", "institution", "UFMG"] 1627 | # extract.ex:7: anonymous fn/2 in Extract.get_value/2 # <= left hand: [key, value] pair 1628 | 1629 | Extract.get_value("name=Lucas&university&lab=ASERG", "university") 1630 | # ** (MatchError) no match of right hand side value: ["university"] 1631 | # extract.ex:7: anonymous fn/2 in Extract.get_value/2 # <= left hand: [key, value] pair 1632 | ``` 1633 | 1634 | These examples are based on code written by José Valim ([@josevalim](https://github.com/josevalim)). Source: [link](http://blog.plataformatec.com.br/2014/09/writing-assertive-code-with-elixir/) 1635 | 1636 | 1637 | 1638 | ## Modules with identical names 1639 | 1640 | * __Category:__ Low-level concerns smells. 1641 | 1642 | * __Problem:__ This code smell is related to possible module name conflicts that can occur when a library is implemented. Due to a limitation of the Erlang VM (BEAM), also used by Elixir, only one instance of a module can be loaded at a time. If there are name conflicts between more than one module, they will be considered the same by BEAM and only one of them will be loaded. This can cause unwanted code behavior. 1643 | 1644 | * __Example:__ The code shown below is an example of this smell. Two different modules were defined with identical names (`Foo`). When BEAM tries to load both simultaneously, only the module defined in the file (`module_two.ex`) stay loaded, redefining the current version of `Foo` (`module_one.ex`) in memory. That makes it impossible to call `from_module_one/0`, for example: 1645 | 1646 | ```elixir 1647 | defmodule Foo do 1648 | @moduledoc """ 1649 | Defined in `module_one.ex` file. 1650 | """ 1651 | def from_module_one do 1652 | "Function from module one!" 1653 | end 1654 | end 1655 | ``` 1656 | 1657 | ```elixir 1658 | defmodule Foo do 1659 | @moduledoc """ 1660 | Defined in `module_two.ex` file. 1661 | """ 1662 | def from_module_two do 1663 | "Function from module two!" 1664 | end 1665 | end 1666 | ``` 1667 | 1668 | When BEAM tries to load both simultaneously, the name conflict causes only one of them to stay loaded: 1669 | 1670 | 1671 | 1672 | ```elixir 1673 | iex(1)> c("module_one.ex") 1674 | [Foo] 1675 | 1676 | iex(2)> c("module_two.ex") 1677 | warning: redefining module Foo (current version defined in memory) 1678 | module_two.ex:1 1679 | [Foo] 1680 | 1681 | iex(3)> Foo.from_module_two() 1682 | "Function from module two!" 1683 | 1684 | iex(4)> Foo.from_module_one() # <= impossible to call due to name conflict 1685 | ** (UndefinedFunctionError) function Foo.from_module_one/0 is undefined... 1686 | ``` 1687 | 1688 | ```elixir 1689 | Foo.from_module_two() 1690 | # "Function from module two!" 1691 | ``` 1692 | 1693 | ```elixir 1694 | # <= impossible to call due to name conflict 1695 | Foo.from_module_one() 1696 | # ** (UndefinedFunctionError) function Foo.from_module_one/0 is undefined... 1697 | ``` 1698 | 1699 | * __Refactoring:__ To remove this code smell, a library must standardize the naming of its modules, always using its own name as a prefix (namespace) for all its module's names (e.g., `LibraryName.ModuleName`). When a module file is within subdirectories of a library, the names of the subdirectories must also be used in the module naming (e.g., `LibraryName.SubdirectoryName.ModuleName`). In the refactored code shown below, this module naming pattern was used. For this, the `Foo` module, defined in the file `module_two.ex`, was also moved to the `utils` subdirectory. This refactoring, in addition to eliminating the internal conflict of names within the library, will prevent the occurrence of name conflicts with client code: 1700 | 1701 | ```elixir 1702 | defmodule MyLibrary.Foo do 1703 | @moduledoc """ 1704 | Defined in `module_one.ex` file. 1705 | Name refactored! 1706 | """ 1707 | def from_module_one do 1708 | "Function from module one!" 1709 | end 1710 | end 1711 | ``` 1712 | 1713 | ```elixir 1714 | defmodule MyLibrary.Utils.Foo do 1715 | @moduledoc """ 1716 | Defined in `module_two.ex` file. 1717 | Name refactored! 1718 | """ 1719 | def from_module_two do 1720 | "Function from module two!" 1721 | end 1722 | end 1723 | ``` 1724 | 1725 | When BEAM tries to load them simultaneously, both will stay loaded successfully: 1726 | 1727 | 1728 | 1729 | ```elixir 1730 | iex(1)> c("module_one.ex") 1731 | [MyLibrary.Foo] 1732 | 1733 | iex(2)> c("module_two.ex") 1734 | [MyLibrary.Utils.Foo] 1735 | 1736 | iex(3)> MyLibrary.Foo.from_module_one() 1737 | "Function from module one!" 1738 | 1739 | iex(4)> MyLibrary.Utils.Foo.from_module_two() 1740 | "Function from module two!" 1741 | ``` 1742 | 1743 | ```elixir 1744 | MyLibrary.Foo.from_module_one() 1745 | # "Function from module one!" 1746 | ``` 1747 | 1748 | ```elixir 1749 | MyLibrary.Utils.Foo.from_module_two() 1750 | # "Function from module two!" 1751 | ``` 1752 | 1753 | This example is based on the description provided in Elixir's official documentation. Source: [link](https://hexdocs.pm/elixir/main/library-guidelines.html#avoid-defining-modules-that-are-not-in-your-namespace) 1754 | 1755 | 1756 | 1757 | ## Unnecessary macros 1758 | 1759 | * __Category:__ Low-level concerns smells. 1760 | 1761 | * __Problem:__ `Macros` are powerful meta-programming mechanisms that can be used in Elixir to extend the language. While implementing `macros` is not a code smell in itself, this meta-programming mechanism should only be used when absolutely necessary. Whenever a macro is implemented, and it was possible to solve the same problem using functions or other pre-existing Elixir structures, the code becomes unnecessarily more complex and less readable. Because `macros` are more difficult to implement and understand, their indiscriminate use can compromise the evolution of a system, reducing its maintainability. 1762 | 1763 | * __Example:__ The code shown below is an example of this smell. The `MyMath` module implements the `sum/2` macro to perform the sum of two numbers received as parameters. While this code has no syntax errors and can be executed correctly to get the desired result, it is unnecessarily more complex. By implementing this functionality as a macro rather than a conventional function, the code became less clear and less objective: 1764 | 1765 | ```elixir 1766 | defmodule MyMath do 1767 | defmacro sum(v1, v2) do 1768 | quote do 1769 | unquote(v1) + unquote(v2) 1770 | end 1771 | end 1772 | end 1773 | ``` 1774 | 1775 | ```elixir 1776 | # ...Use examples... 1777 | 1778 | require MyMath 1779 | # MyMath 1780 | 1781 | MyMath.sum(3, 5) 1782 | # 8 1783 | 1784 | MyMath.sum(3 + 1, 5 + 6) 1785 | # 15 1786 | ``` 1787 | 1788 | * __Refactoring:__ To remove this code smell, the developer must replace the unnecessary macro with structures that are simpler to write and understand, such as named functions. The code shown below is the result of the refactoring of the previous example. Basically, the `sum/2` macro has been transformed into a conventional named function. Note that the `require` command is no longer needed: 1789 | 1790 | ```elixir 1791 | defmodule MyMath do 1792 | # <= macro became a named function! 1793 | def sum(v1, v2) do 1794 | v1 + v2 1795 | end 1796 | end 1797 | ``` 1798 | 1799 | ```elixir 1800 | # ...Use examples... 1801 | 1802 | # No need to require anymore! 1803 | 1804 | MyMath.sum(3, 5) 1805 | # 8 1806 | 1807 | MyMath.sum(3 + 1, 5 + 6) 1808 | # 15 1809 | ``` 1810 | 1811 | This example is based on the description provided in Elixir's official documentation. Source: [link](https://hexdocs.pm/elixir/main/library-guidelines.html#avoid-macros) 1812 | 1813 | 1814 | 1815 | ## Dynamic atom creation 1816 | 1817 | * __Category:__ Low-level concerns smells. 1818 | 1819 | * __Note:__ This smell emerged from a study with mining software repositories (MSR). 1820 | 1821 | * __Problem:__ An `atom` is a basic data type of Elixir whose value is its own name. They are often useful to identify resources or to express the state of an operation. The creation of an `atom` do not characterize a smell by itself; however, `atoms` are not collected by Elixir's Garbage Collector, so values of this type live in memory while an application is executing, during its entire lifetime. Also, BEAM limit the number of `atoms` that can exist in an application (`1_048_576`) and each `atom` has a maximum size limited to 255 Unicode code points. For these reasons, the dynamic atom creation is considered a code smell, since in this way the developer has no control over how many `atoms` will be created during the execution of the application. This unpredictable scenario can expose an app to unexpected behavior caused by excessive memory usage, or even by reaching the maximum number of `atoms` possible. 1822 | 1823 | * __Example:__ The code shown below is an example of this smell. Imagine that you are implementing a code that performs the conversion of `string` values into `atoms` to identify resources. These `strings` can come from user input or even have been received as response from requests to an API. As this is a dynamic and unpredictable scenario, it is possible for identical `strings` to be converted into new `atoms` that are repeated unnecessarily. This kind of conversion, in addition to wasting memory, can be problematic for an application if it happens too often. 1824 | 1825 | ```elixir 1826 | defmodule Identifier do 1827 | # ... 1828 | 1829 | def generate(id) when is_bitstring(id) do 1830 | String.to_atom(id) #<= dynamic atom creation!! 1831 | end 1832 | end 1833 | 1834 | #...Use examples... 1835 | 1836 | string_from_user_input = "my_id" 1837 | # "my_id" 1838 | 1839 | string_from_API_response = "my_id" 1840 | # "my_id" 1841 | 1842 | Identifier.generate(string_from_user_input) 1843 | # :my_id 1844 | 1845 | # <= atom repeated was created! 1846 | Identifier.generate(string_from_API_response) 1847 | # :my_id 1848 | ``` 1849 | 1850 | When we use the `String.to_atom/1` function to dynamically create an `atom`, it is created regardless of whether there is already another one with the same value in memory, so when this happens automatically, we will not have control over meeting the limits established by BEAM. 1851 | 1852 | * __Refactoring:__ To remove this smell, as shown below, first you must ensure that all the identifier `atoms` are created statically, only once, at the beginning of an application's execution: 1853 | 1854 | ```elixir 1855 | # statically created atoms... 1856 | _ = :my_id 1857 | _ = :my_id2 1858 | _ = :my_id3 1859 | _ = :my_id4 1860 | ``` 1861 | 1862 | Next, you should replace the use of the `String.to_atom/1` function with the `String.to_existing_atom/1` function. This will allow string-to-atom conversions to just map the strings to atoms already in memory (statically created at the beginning of the execution), thus preventing repeated `atoms` from being created dynamically. This second part of the refactoring is presented below. 1863 | 1864 | ```elixir 1865 | defmodule Identifier do 1866 | # ... 1867 | 1868 | def generate(id) when is_bitstring(id) do 1869 | String.to_existing_atom(id) #<= just maps a string to an existing atom! 1870 | end 1871 | end 1872 | 1873 | #...Use examples... 1874 | 1875 | Identifier.generate("my_id") 1876 | # :my_id 1877 | 1878 | Identifier.generate("my_id2") 1879 | # :my_id2 1880 | 1881 | Identifier.generate("non_existent_id") 1882 | # ** (ArgumentError) errors were found at the given arguments: 1883 | # * 1st argument: not an already existing atom 1884 | ``` 1885 | 1886 | Note that in the third use example, when a `string` different from an already existing `atom` is given, Elixir shows an error instead of performing the conversion. This demonstrates that this refactoring creates a more controlled and predictable scenario for the application in terms of memory usage. 1887 | 1888 | This example and the refactoring are based on the Elixir's official documentation. Sources: [1](https://hexdocs.pm/elixir/String.html#to_atom/1), [2](https://hexdocs.pm/elixir/String.html#to_existing_atom/1) 1889 | 1890 | ## About 1891 | 1892 | This catalog was proposed by Lucas Vegi and Marco Tulio Valente, from [ASERG/DCC/UFMG](http://aserg.labsoft.dcc.ufmg.br/). 1893 | 1894 | For more info see the following paper: 1895 | 1896 | * [Code Smells in Elixir: Early Results from a Grey Literature Review](https://doi.org/10.48550/arXiv.2203.08877), International Conference on Program Comprehension (ICPC), 2022. [[slides]](https://github.com/lucasvegi/Elixir-Code-Smells/blob/main/etc/Code-Smells-in-Elixir-ICPC22-Lucas-Vegi.pdf) [[video]](https://youtu.be/3X2gxg13tXo) [[podcast]](http://elixiremfoco.com/episode?id=lucas-vegi-e-marco-tulio) 1897 | 1898 | Please feel free to make pull requests and suggestions ([Issues](https://github.com/lucasvegi/Elixir-Code-Smells/issues) tab). 1899 | 1900 | ## Acknowledgments 1901 | 1902 | We are supported by __[Finbits](https://www.finbits.com.br/)__, a Brazilian Elixir-based fintech. 1903 | 1904 |
1905 | 1906 |

1907 |
1908 | 1909 | Our research is also part of the initiative called __[Research with Elixir](http://pesquisecomelixir.com.br/)__ (in portuguese). 1910 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 35 | Survey version selection 36 | 37 | 38 |

You are being directed to the survey. If the survey does not open automatically, click the button below:

39 |
40 | 41 | 42 | -------------------------------------------------------------------------------- /docs/survey_questions_version_a.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasvegi/Elixir-Code-Smells/2c2ed33646a9c2bb9d8122c71cb2fafffbae6ac0/docs/survey_questions_version_a.pdf -------------------------------------------------------------------------------- /etc/2022-icpc-era.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasvegi/Elixir-Code-Smells/2c2ed33646a9c2bb9d8122c71cb2fafffbae6ac0/etc/2022-icpc-era.pdf -------------------------------------------------------------------------------- /etc/2023-emse-code-smells-elixir.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasvegi/Elixir-Code-Smells/2c2ed33646a9c2bb9d8122c71cb2fafffbae6ac0/etc/2023-emse-code-smells-elixir.pdf -------------------------------------------------------------------------------- /etc/Code-Smells-in-Elixir-ICPC22-Lucas-Vegi.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasvegi/Elixir-Code-Smells/2c2ed33646a9c2bb9d8122c71cb2fafffbae6ac0/etc/Code-Smells-in-Elixir-ICPC22-Lucas-Vegi.pdf -------------------------------------------------------------------------------- /etc/finbits.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasvegi/Elixir-Code-Smells/2c2ed33646a9c2bb9d8122c71cb2fafffbae6ac0/etc/finbits.png -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule ElixirCodeSmellsCatalog.Mixfile do 2 | use Mix.Project 3 | 4 | @project_description """ 5 | Catalog of Elixir-specific code smells 6 | """ 7 | 8 | @version "0.0.1" 9 | @source_url "https://github.com/lucasvegi/Elixir-Code-Smells" 10 | 11 | def project do 12 | [ 13 | app: :elixir_code_smells_catalog, 14 | version: @version, 15 | elixir: "~> 1.0", 16 | build_embedded: Mix.env() == :prod, 17 | start_permanent: Mix.env() == :prod, 18 | docs: docs(), 19 | description: @project_description, 20 | source_url: @source_url, 21 | package: package(), 22 | deps: deps() 23 | ] 24 | end 25 | 26 | def application do 27 | [applications: [:logger]] 28 | end 29 | 30 | defp deps do 31 | [] 32 | end 33 | 34 | defp docs() do 35 | [ 36 | source_ref: "v#{@version}", 37 | main: "readme", 38 | extras: [ 39 | "README.md": [title: "README"] 40 | ] 41 | ] 42 | end 43 | 44 | defp package do 45 | [ 46 | name: :elixir_code_smells_catalog, 47 | maintainers: ["Lucas Vegi", "Marco Tulio Valente"], 48 | licenses: ["MIT-License"], 49 | links: %{ 50 | "GitHub" => @source_url 51 | } 52 | ] 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /traditional/README.md: -------------------------------------------------------------------------------- 1 | # [Catalog of Traditional Code Smells in Elixir][Traditional Smells] 2 | 3 | [![GitHub last commit](https://img.shields.io/github/last-commit/lucasvegi/Elixir-Code-Smells)](https://github.com/lucasvegi/Elixir-Code-Smells/commits/main) 4 | 5 | ## Table of Contents 6 | 7 | * __[Traditional code smells](#traditional-code-smells)__ 8 | * [Comments](#comments) 9 | * [Long Parameter List](#long-parameter-list) 10 | * [Long Function](#long-function) 11 | * [Primitive Obsession](#primitive-obsession) 12 | * [Shotgun Surgery](#shotgun-surgery) 13 | * [Duplicated Code](#duplicated-code) 14 | * [Feature Envy](#feature-envy) 15 | * [Divergent Change](#divergent-change) 16 | * [Inappropriate Intimacy](#inappropriate-intimacy) 17 | * [Large Class](#large-class) 18 | * [Speculative Generality](#speculative-generality) 19 | * [Switch Statements](#switch-statements) 20 | * __[Elixir-specific code smells][Elixir Smells]__ 21 | 22 | ## Traditional code smells 23 | 24 | [Fowler and Beck][FowlerAndBeckCatalog] coined the term *__code smell__* to name suboptimal code structures that can harm software maintenance and evolution. In addition to coining the term, they cataloged 22 traditional code smell. Although these smells were proposed in the nineties for the object-oriented programming paradigm, some of them are also discussed in the Elixir context nowadays. In this section, 12 traditional smells are explained and exemplified in this context. 25 | 26 | We present these smells using the following structure for each one: 27 | 28 | * __Name:__ A name is important to facilitate communication between developers; 29 | * __Problem:__ How the code smell can harm code quality and the impacts it can have on software maintenance, comprehension, and evolution; 30 | * __Example:__ Code and textual descriptions to illustrate the occurrence of the code smell; 31 | * __Treatments:__ How to remove a code smell in a *__disciplined way__* with the assistance of *__refactoring strategies__*. When a refactoring should be used alone, it is listed in its own bullet point (*i.e.*, •). Conversely, when a refactoring is part of a sequence of operations to assist the removal, it is listed using a pipeline to define its order (*e.g.*, Refactoring1 |> Refactoring2 |> Refactoring3 |> etc.). This disciplined way to refactor a smell will help you change your code one small step at a time, thus minimizing the chances of introducing bugs or altering the original behavior of the system. All the refactoring strategies mapped to the code smells are part of our [Catalog of Elixir Refactorings](https://github.com/lucasvegi/Elixir-Refactorings). 32 | 33 | [▲ back to Index](#table-of-contents) 34 | 35 | ___ 36 | 37 | ### Comments 38 | 39 | * __Problem:__ This smell occurs when a ``function`` is documented using explanatory comments instead of ``@doc``. 40 | 41 | * __Example:__ 42 | 43 | ```elixir 44 | defmodule ModuleA do 45 | #@doc """ 46 | # construct for documentation not used! 47 | #""" 48 | def foo do 49 | # explanatory comments used for documentation 50 | ... 51 | ... 52 | # explanatory comments used for documentation 53 | ... 54 | ... 55 | end 56 | end 57 | ``` 58 | 59 | * __Treatments:__ 60 | 61 | * [Extract function](https://github.com/lucasvegi/Elixir-Refactorings?#extract-function) 62 | * [Extract expressions](https://github.com/lucasvegi/Elixir-Refactorings?#extract-expressions) 63 | * [Extract constant](https://github.com/lucasvegi/Elixir-Refactorings?#extract-constant) 64 | * [Rename an identifier](https://github.com/lucasvegi/Elixir-Refactorings?#rename-an-identifier) 65 | * [Typing parameters and return values](https://github.com/lucasvegi/Elixir-Refactorings?#typing-parameters-and-return-values) 66 | * [Add type declarations and contracts](https://github.com/lucasvegi/Elixir-Refactorings?#add-type-declarations-and-contracts) 67 | 68 | [▲ back to Index](#table-of-contents) 69 | ___ 70 | 71 | ### Long Parameter List 72 | 73 | * __Problem:__ In a functional language like Elixir, ``functions`` tend to be pure and therefore do not access or manipulate values outside their scopes. Since the input values of these ``functions`` are just their parameters, it is natural to expect that ``functions`` with a long list of parameters be created to keep them pure. However, when this list has more than three or four parameters, the function's interface becomes confusing and prone to errors during use. 74 | 75 | * __Example:__ 76 | 77 | ```elixir 78 | defmodule Library do 79 | def loan(user_name, email, password, user_alias, book_name, book_ed, active) do 80 | # ... loan/7 81 | # ... too many parameters that can be grouped in structs! 82 | end 83 | end 84 | ``` 85 | 86 | * __Treatments:__ 87 | 88 | * [Add or remove a parameter](https://github.com/lucasvegi/Elixir-Refactorings?#add-or-remove-a-parameter) 89 | * [Reorder parameter](https://github.com/lucasvegi/Elixir-Refactorings?#reorder-parameter) 90 | * [Grouping parameters in tuple](https://github.com/lucasvegi/Elixir-Refactorings?#grouping-parameters-in-tuple) |> [From tuple to struct](https://github.com/lucasvegi/Elixir-Refactorings?#from-tuple-to-struct) 91 | 92 | [▲ back to Index](#table-of-contents) 93 | ___ 94 | 95 | ### Long Function 96 | 97 | * __Problem:__ Poorly cohesive ``function``, made up of too many lines of code that group together many different responsibilities that could be separated into smaller functions with a single responsibility each. Generally, any function longer than ten lines should make you start asking questions. 98 | 99 | * __Example:__ ``print/2`` is a long function (low-cohesion), so is necessary to separate it in blocks and add comments to understand. This is a bad practice. 100 | 101 | ```elixir 102 | defmodule Report do 103 | def print(user, purchase_order) do 104 | # Company data 105 | print_company_data() 106 | 107 | # User data 108 | IO.puts("Name: #{user.first_name} #{user.last_name}") 109 | 110 | # Order data (with filters and calculations) 111 | purchase_order.items 112 | |> Enum.filter(&(&1.status == 3)) 113 | |> Enum.map(fn item -> 114 | IO.puts("Item: #{item.name}") 115 | IO.puts("Price: #{item.price}") 116 | IO.puts("Amount: #{item.amount}") 117 | total = item.price * item.amount 118 | IO.puts("Total: #{total}") 119 | end) 120 | end 121 | end 122 | ``` 123 | 124 | This example is based on code written by Elaine Watanabe ([@elainenaomi](https://github.com/elainenaomi)). Source: [link][ElaineYoutube] 125 | 126 | * __Treatments:__ 127 | 128 | * [Extract function](https://github.com/lucasvegi/Elixir-Refactorings?#extract-function) 129 | * [Transform nested "if" statements into a "cond"](https://github.com/lucasvegi/Elixir-Refactorings?#transform-nested-if-statements-into-a-cond) 130 | * [Folding against a function definition](https://github.com/lucasvegi/Elixir-Refactorings?#folding-against-a-function-definition) 131 | * [Remove dead code](https://github.com/lucasvegi/Elixir-Refactorings?#remove-dead-code) 132 | * [Simplifying checks by using truthness condition](https://github.com/lucasvegi/Elixir-Refactorings?#simplifying-checks-by-using-truthness-condition) 133 | * [Generalise a function definition](https://github.com/lucasvegi/Elixir-Refactorings?#generalise-a-function-definition) 134 | * [Introduce pattern matching over a parameter](https://github.com/lucasvegi/Elixir-Refactorings?#introduce-pattern-matching-over-a-parameter) 135 | * [Replace pipeline with a function](https://github.com/lucasvegi/Elixir-Refactorings?#replace-pipeline-with-a-function) 136 | * [Default value for an absent key in a Map](https://github.com/lucasvegi/Elixir-Refactorings?#default-value-for-an-absent-key-in-a-map) 137 | * [Defining a subset of a Map](https://github.com/lucasvegi/Elixir-Refactorings?#defining-a-subset-of-a-map) 138 | * [Pipeline using "with"](https://github.com/lucasvegi/Elixir-Refactorings?#pipeline-using-with) 139 | * [Remove redundant last clause in "with"](https://github.com/lucasvegi/Elixir-Refactorings?#remove-redundant-last-clause-in-with) 140 | * [Modifying keys in a Map](https://github.com/lucasvegi/Elixir-Refactorings?#modifying-keys-in-a-map) 141 | * [Remove nested conditional statements in function calls](https://github.com/lucasvegi/Elixir-Refactorings?#remove-nested-conditional-statements-in-function-calls) 142 | * [Splitting a definition](https://github.com/lucasvegi/Elixir-Refactorings?#splitting-a-definition) 143 | * [Merging match expressions into a list pattern](https://github.com/lucasvegi/Elixir-Refactorings?#merging-match-expressions-into-a-list-pattern) 144 | * [Convert nested conditionals to pipeline](https://github.com/lucasvegi/Elixir-Refactorings?#convert-nested-conditionals-to-pipeline) |> [Replace function call with raw value in a pipeline start](https://github.com/lucasvegi/Elixir-Refactorings?#replace-function-call-with-raw-value-in-a-pipeline-start) 145 | 146 | [▲ back to Index](#table-of-contents) 147 | ___ 148 | 149 | ### Primitive Obsession 150 | 151 | * __Problem:__ This code smell can be felt when Elixir basic types (e.g., ``integer``, ``float``, and ``string``) are abusively used in function parameters and code variables, rather than creating specific composite data types (e.g., ``protocols``, ``structs``, and ``@type``) that can better represent a domain. 152 | 153 | * __Example:__ Use a ``float`` value to represent ``Money`` or use a ``string`` to represent an ``Address``. ``Money`` and ``Address`` are more complex structures than a simple primitive value. 154 | 155 | * __Treatments:__ 156 | 157 | * [Grouping parameters in tuple](https://github.com/lucasvegi/Elixir-Refactorings?#grouping-parameters-in-tuple) |> [From tuple to struct](https://github.com/lucasvegi/Elixir-Refactorings?#from-tuple-to-struct) 158 | * [Add type declarations and contracts](https://github.com/lucasvegi/Elixir-Refactorings?#add-type-declarations-and-contracts) 159 | 160 | [▲ back to Index](#table-of-contents) 161 | ___ 162 | 163 | ### Shotgun Surgery 164 | 165 | * __Problem:__ Code with responsibilities spread across many Elixir ``modules``, so that modifications require many small changes to many different ``modules`` at the same time. These changes are susceptible to errors, since they may be forgotten, producing incorrect behavior. 166 | 167 | * __Example:__ Imagine the scenario of an e-commerce, where depending on the type of user's subscription, special shipping conditions, discounts and subscription renewal fees are charged. In the two modules below (``ShoppingCart`` and ``Subscription``), only two signature types are handled (``id: 3`` and ``id: 4``), however, if a new signature type is created (``id: 8``), many small changes in different parts of these two modules will need to be performed to meet this new single requirement. This happens due to the spreading of responsibilities of a feature by the code. 168 | 169 | ```elixir 170 | defmodule ShoppingCart do 171 | def calculate_shipping(zip_code, %{id: 3}), do: 0.0 172 | def calculate_shipping(zip_code, %{id: 4}), do: 0.0 173 | # def calculate_shipping(zip_code, %{id: 8}), do: 0.0 <-- small change 174 | 175 | def calculate_shipping(zip_code, _) do 176 | 10.0 * Location.calculate(zip_code) 177 | end 178 | 179 | def apply_discount(total, %{id: 3}), 180 | do: total * 0.95 181 | 182 | def apply_discount(total, %{id: 4}), 183 | do: total * 0.9 184 | 185 | #def apply_discount(total, %{id: 8}), <-- small change 186 | # do: total * 0.85 187 | 188 | def apply_discount(total, _), 189 | do: total 190 | end 191 | ``` 192 | 193 | ```elixir 194 | defmodule Subscription do 195 | def renew(%{id: id}, user) do 196 | case id do 197 | 3 -> Billing.perform(user, id, 100.0) 198 | 4 -> Billing.perform(user, id, 95.0) 199 | #8 -> Billing.perform(user, id, 90.0) <-- small change 200 | end 201 | end 202 | end 203 | ``` 204 | 205 | This example is based on code written by Elaine Watanabe ([@elainenaomi](https://github.com/elainenaomi)). Source: [link][ElaineYoutube] 206 | 207 | * __Treatments:__ 208 | 209 | * [Moving a definition](https://github.com/lucasvegi/Elixir-Refactorings?#moving-a-definition) 210 | 211 | [▲ back to Index](#table-of-contents) 212 | ___ 213 | 214 | ### Duplicated Code 215 | 216 | * __Problem:__ This smell occurs when two or more code fragments look almost identical. Duplicated code can cause maintenance issues especially when changes are needed. Since multiple code fragments will have to be updated, there is the risk that one of them will not be changed, causing unwanted system behavior. 217 | 218 | * __Example:__ 219 | 220 | ```elixir 221 | defmodule ShoppingCart do 222 | def calculate_shipping(zip_code, subscription) do 223 | if (Enum.member?([3, 4], subscription.id)) do # <-- duplicated code! 224 | 0 225 | else 226 | 10.0 * Location.calculate(zip_code) 227 | end 228 | end 229 | 230 | def apply_discount(total, subscription) do 231 | if (Enum.member?([3, 4], subscription.id)) do # <-- duplicated code! 232 | total * 0.9 233 | else 234 | total 235 | end 236 | end 237 | end 238 | ``` 239 | 240 | This example is based on code written by Elaine Watanabe ([@elainenaomi](https://github.com/elainenaomi)). Source: [link][ElaineYoutube] 241 | 242 | * __Treatments:__ 243 | 244 | * [Defining a subset of a Map](https://github.com/lucasvegi/Elixir-Refactorings?#defining-a-subset-of-a-map) 245 | * [Modifying keys in a Map](https://github.com/lucasvegi/Elixir-Refactorings?#modifying-keys-in-a-map) 246 | * [Folding against a function definition](https://github.com/lucasvegi/Elixir-Refactorings?#folding-against-a-function-definition) 247 | * [Extract expressions](https://github.com/lucasvegi/Elixir-Refactorings?#extract-expressions) 248 | * [Extract function](https://github.com/lucasvegi/Elixir-Refactorings?#extract-function) 249 | * [Reducing a boolean equality expression](https://github.com/lucasvegi/Elixir-Refactorings?#reducing-a-boolean-equality-expression) 250 | * [Generalise a function definition](https://github.com/lucasvegi/Elixir-Refactorings?#generalise-a-function-definition) 251 | * [Turning anonymous into local functions](https://github.com/lucasvegi/Elixir-Refactorings?#turning-anonymous-into-local-functions) 252 | * [Merging multiple definitions](https://github.com/lucasvegi/Elixir-Refactorings?#merging-multiple-definitions) 253 | * [Move expression out of case](https://github.com/lucasvegi/Elixir-Refactorings?#move-expression-out-of-case) 254 | * [Remove redundant last clause in "with"](https://github.com/lucasvegi/Elixir-Refactorings?#remove-redundant-last-clause-in-with) 255 | * [Static structure reuse](https://github.com/lucasvegi/Elixir-Refactorings?#static-structure-reuse) 256 | * [Introduce import](https://github.com/lucasvegi/Elixir-Refactorings?#introduce-import) 257 | * [Widen or narrow definition scope](https://github.com/lucasvegi/Elixir-Refactorings?#widen-or-narrow-definition-scope) 258 | * [Introduce Enum.map/2](https://github.com/lucasvegi/Elixir-Refactorings?#introduce-enummap2) |> [Transform to list comprehension](https://github.com/lucasvegi/Elixir-Refactorings?#transform-to-list-comprehension) |> [List comprehension simplifications](https://github.com/lucasvegi/Elixir-Refactorings?#list-comprehension-simplifications) 259 | 260 | [▲ back to Index](#table-of-contents) 261 | ___ 262 | 263 | ### Feature Envy 264 | 265 | * __Problem:__ This smell occurs when a ``function`` accesses more data or calls more functions from another ``module`` than from its own. The presence of this smell can make a ``module`` less cohesive and increase code coupling. 266 | 267 | * __Example:__ In the following code, all the data used in the ``calculate_total_item/1`` function comes from the ``OrderItem`` module. So if ``calculate_total_item/1`` were moved to ``OrderItem``, the ``Order`` module could become more cohesive and the coupling would decrease. 268 | 269 | ```elixir 270 | defmodule Order do 271 | def calculate_total_item(id) do 272 | item = OrderItem.find_item(id) 273 | total = (item.price + item.taxes) * item.amount 274 | discount = OrderItem.find_discount(item) 275 | 276 | unless is_nil(discount) do # <-- all data comes from OrderItem! 277 | total - total * discount 278 | else 279 | total 280 | end 281 | end 282 | end 283 | ``` 284 | 285 | This example is based on code written by Elaine Watanabe ([@elainenaomi](https://github.com/elainenaomi)). Source: [link][ElaineYoutube] 286 | 287 | * __Treatments:__ 288 | 289 | * [Extract function](https://github.com/lucasvegi/Elixir-Refactorings?#extract-function) |> [Moving a definition](https://github.com/lucasvegi/Elixir-Refactorings?#moving-a-definition) 290 | * [Remove import attributes](https://github.com/lucasvegi/Elixir-Refactorings?#remove-import-attributes) 291 | 292 | [▲ back to Index](#table-of-contents) 293 | ___ 294 | 295 | ### Divergent Change 296 | 297 | * __Problem:__ This code smell is the opposite of [Shotgun Surgery](#shotgun-surgery). While in Shotgun Surgery many ``modules`` need to be modified at the same time to complete a single code change, in Divergent Change a single ``module`` is modified constantly, in multiple parts, due to unrelated reasons. Modules that bundle together many ``functions`` that do not share a common goal are a cause of Divergent Change. 298 | 299 | * __Example:__ ``Account`` module has at least two different and unrelated reasons that may justify modifications to this module. This smell can be removed by creating new cohesive modules and moving related functions into them. 300 | 301 | ```elixir 302 | defmodule Account do 303 | # Reason to modify (1): Sign-in policies! 304 | def signin_with_google(google_data) do 305 | # ... 306 | end 307 | def signin_with_apple(apple_data) do 308 | # ... 309 | end 310 | 311 | # Reason to modify (2): Billing policies! 312 | def charge_subscription(id) do 313 | # ... 314 | end 315 | def calculate_subscription_discount(id) do 316 | # ... 317 | end 318 | end 319 | ``` 320 | 321 | This example is based on code written by Elaine Watanabe ([@elainenaomi](https://github.com/elainenaomi)). Source: [link][ElaineYoutube] 322 | 323 | * __Treatments:__ 324 | 325 | * [Splitting a large module](https://github.com/lucasvegi/Elixir-Refactorings?#splitting-a-large-module) |> [Rename an identifier](https://github.com/lucasvegi/Elixir-Refactorings?#rename-an-identifier) 326 | * [Moving a definition](https://github.com/lucasvegi/Elixir-Refactorings?#moving-a-definition) 327 | * [Behaviour extraction](https://github.com/lucasvegi/Elixir-Refactorings?#behaviour-extraction) 328 | 329 | [▲ back to Index](#table-of-contents) 330 | 331 | ___ 332 | 333 | ### Inappropriate Intimacy 334 | 335 | * __Problem:__ In Elixir, this code smell can be felt in ``impure functions``. These functions can access internal values of other ``modules`` that are not received via parameters, generating excessive coupling. Particularly, the values accessed by impure functions of a ``module`` “A”, can be internal details of a ``module`` “B”, obtained by calling functions of ``module`` “B”, inside functions of ``module`` “A”. 336 | 337 | * __Example:__ ``Order.charge/2`` function is impure. This function creates an overly tight coupling because it knows internal details that should be encapsulated in the ``User``, ``Card``, and ``Gateway`` modules. 338 | 339 | ```elixir 340 | defmodule Order do 341 | def charge(total, user) do 342 | card = User.find_credit_card(user) # <-- access to internal details! 343 | if (card.status == 3) do 344 | gateway = Card.find_payment_gateway(card) # <-- access to internal details! 345 | Gateway.charge(gateway, card, total) # <-- rule should be encapsulated! 346 | end 347 | end 348 | end 349 | ``` 350 | 351 | This example is based on code written by Elaine Watanabe ([@elainenaomi](https://github.com/elainenaomi)). Source: [link][ElaineYoutube] 352 | 353 | * __Treatments:__ 354 | 355 | * [Closure conversion](https://github.com/lucasvegi/Elixir-Refactorings?#closure-conversion) |> [Add or remove a parameter](https://github.com/lucasvegi/Elixir-Refactorings?#add-or-remove-a-parameter) 356 | * [Moving a definition](https://github.com/lucasvegi/Elixir-Refactorings?#moving-a-definition) 357 | * [Splitting a large module](https://github.com/lucasvegi/Elixir-Refactorings?#splitting-a-large-module) |> [Rename an identifier](https://github.com/lucasvegi/Elixir-Refactorings?#rename-an-identifier) 358 | 359 | [▲ back to Index](#table-of-contents) 360 | ___ 361 | 362 | ### Large Class 363 | 364 | * __Problem:__ Although Elixir does not support classes, ``functions`` can be grouped by ``modules`` in a similar way. When a ``module`` is not cohesive, it deals with multiple distinct business rules, tending to have many functions and lines of code, becoming large. This makes maintenance difficult. [Duplicated code](#duplicated-code) can contribute to the increase in the size of the modules, thus showing that some code smells can also be related. 365 | 366 | * __Example:__ ``ShoppingCart`` module is unnecessarily large due to its lack of cohesion. It groups functions for many distinct and not directly related business rules. Therefore, these functions could be extracted to other modules, thus increasing the overall cohesion of the system. 367 | 368 | ```elixir 369 | defmodule ShoppingCart do 370 | # Rule 1 371 | def calculate_total(items, subscription) do 372 | # ... 373 | end 374 | 375 | # Rule 2 376 | def calculate_shipping(zip_code, %{id: 3}), do: 0.0 377 | def calculate_shipping(zip_code, %{id: 4}), do: 0.0 378 | def calculate_shipping(zip_code, _), do 379 | 10.0 * Location.calculate(zip_code) 380 | end 381 | 382 | # Rule 3 383 | def apply_discount(total, %{id: 3}), do: total * 0.9 384 | def apply_discount(total, %{id: 4}), do: total * 0.9 385 | def apply_discount(total, _), do: total 386 | 387 | # Rule 4 388 | def send_message_subscription(%{id: 3}, _), do: nil 389 | def send_message_subscription(%{id: 4}, _), do: nil 390 | def send_message_subscription(subscription, user), 391 | do: Subscription.send_email_upgrade(subscription, user) 392 | 393 | # Rule 5 394 | def print(user, order) do 395 | # ... 396 | end 397 | end 398 | ``` 399 | 400 | This example is based on code written by Elaine Watanabe ([@elainenaomi](https://github.com/elainenaomi)). Source: [link][ElaineYoutube] 401 | 402 | * __Treatments:__ 403 | 404 | * [Splitting a large module](https://github.com/lucasvegi/Elixir-Refactorings?#splitting-a-large-module) |> [Rename an identifier](https://github.com/lucasvegi/Elixir-Refactorings?#rename-an-identifier) 405 | * [Behaviour extraction](https://github.com/lucasvegi/Elixir-Refactorings?#behaviour-extraction) 406 | * [Moving a definition](https://github.com/lucasvegi/Elixir-Refactorings?#moving-a-definition) 407 | 408 | [▲ back to Index](#table-of-contents) 409 | ___ 410 | 411 | ### Speculative Generality 412 | 413 | * __Problem:__ This smell occurs when a code has fragments that were implemented in order to give it flexibility in the future, but in practice they end up not being useful because they are never used. In Elixir, this smell can be felt in ``modules`` not used anywhere in the code, ``functions`` that are never called or even in functions with ``parameters`` defined with default values and which do not have calls that inform different values for these parameters. Code with this smell is unnecessarily large and more difficult to maintain. 414 | 415 | * __Example:__ ``apply_discount/1`` function performs an extraction on a struct ``%Product{}`` received via a parameter to obtain its ``category``. When this extraction was designed, the objective was to make ``apply_discount/1`` more flexible for future evolutions, facilitating the application of different discounts according to the category of each product. In practice, this speculation was unnecessary because the system always applied the same discount percentage for all product categories. 416 | 417 | ```elixir 418 | defmodule ShoppingCart do 419 | def apply_discount(%{category: category} = product) do 420 | case category do 421 | _ -> product.price * 0.8 # <-- Speculative Generality! 422 | end 423 | end 424 | end 425 | ``` 426 | 427 | This example is based on code written by Elaine Watanabe ([@elainenaomi](https://github.com/elainenaomi)). Source: [link][ElaineYoutube] 428 | 429 | * __Treatments:__ 430 | 431 | * [Inline function](https://github.com/lucasvegi/Elixir-Refactorings?#inline-function) 432 | * [Inline macro](https://github.com/lucasvegi/Elixir-Refactorings?#inline-macro) 433 | * [Add or remove a parameter](https://github.com/lucasvegi/Elixir-Refactorings?#add-or-remove-a-parameter) 434 | * [Rename an identifier](https://github.com/lucasvegi/Elixir-Refactorings?#rename-an-identifier) 435 | * [Behaviour inlining](https://github.com/lucasvegi/Elixir-Refactorings?#behaviour-inlining) 436 | * [Remove dead code](https://github.com/lucasvegi/Elixir-Refactorings?#remove-dead-code) 437 | * [Eliminate single branch](https://github.com/lucasvegi/Elixir-Refactorings?#eliminate-single-branch) 438 | 439 | [▲ back to Index](#table-of-contents) 440 | ___ 441 | 442 | ### Switch Statements 443 | 444 | * __Problem:__ It can be felt when the same sequence of conditional statements, whether via ``if/else``, ``case``, ``cond`` or ``with`` appear duplicated by the code, forcing changes in multiple parts of the code whenever a new check needs to be added to their conditional statements. 445 | 446 | * __Treatments:__ 447 | 448 | * [Replace conditional with polymorphism via Protocols](https://github.com/lucasvegi/Elixir-Refactorings?#replace-conditional-with-polymorphism-via-protocols) |> [Converts guards to conditionals](https://github.com/lucasvegi/Elixir-Refactorings?#converts-guards-to-conditionals) 449 | * [Introduce pattern matching over a parameter](https://github.com/lucasvegi/Elixir-Refactorings?#introduce-pattern-matching-over-a-parameter) 450 | * [Introduce overloading](https://github.com/lucasvegi/Elixir-Refactorings?#introduce-overloading) 451 | * [Extract function](https://github.com/lucasvegi/Elixir-Refactorings?#extract-function) |> [Moving a definition](https://github.com/lucasvegi/Elixir-Refactorings?#moving-a-definition) |> [Generalise a function definition](https://github.com/lucasvegi/Elixir-Refactorings?#generalise-a-function-definition) |> [Folding against a function definition](https://github.com/lucasvegi/Elixir-Refactorings?#folding-against-a-function-definition) 452 | 453 | [▲ back to Index](#table-of-contents) 454 | 455 | 456 | [Traditional Smells]: https://github.com/lucasvegi/Elixir-Code-Smells/tree/main/traditional 457 | [Elixir Smells]: https://github.com/lucasvegi/Elixir-Code-Smells 458 | [Elixir]: http://elixir-lang.org 459 | [ASERG]: http://aserg.labsoft.dcc.ufmg.br/ 460 | [MultiClauseExample]: https://syamilmj.com/2021-09-01-elixir-multi-clause-anti-pattern/ 461 | [ComplexErrorHandleExample]: https://elixirforum.com/t/what-are-sort-of-smells-do-you-tend-to-find-in-elixir-code/14971 462 | [JoseValimExamples]: http://blog.plataformatec.com.br/2014/09/writing-assertive-code-with-elixir/ 463 | [dimitarvp]: https://elixirforum.com/u/dimitarvp 464 | [MrDoops]: https://elixirforum.com/u/MrDoops 465 | [neenjaw]: https://exercism.org/profiles/neenjaw 466 | [angelikatyborska]: https://exercism.org/profiles/angelikatyborska 467 | [ExceptionsForControlFlowExamples]: https://exercism.org/tracks/elixir/concepts/try-rescue 468 | [DataManipulationByMigrationExamples]: https://www.idopterlabs.com.br/post/criando-uma-mix-task-em-elixir 469 | [Migration]: https://hexdocs.pm/ecto_sql/Ecto.Migration.html 470 | [MixTask]: https://hexdocs.pm/mix/Mix.html#module-mix-task 471 | [CodeOrganizationByProcessExample]: https://hexdocs.pm/elixir/master/library-guidelines.html#avoid-using-processes-for-code-organization 472 | [GenServer]: https://hexdocs.pm/elixir/master/GenServer.html 473 | [UnsupervisedProcessExample]: https://hexdocs.pm/elixir/master/library-guidelines.html#avoid-spawning-unsupervised-processes 474 | [Supervisor]: https://hexdocs.pm/elixir/master/Supervisor.html 475 | [Discussions]: https://github.com/lucasvegi/Elixir-Code-Smells/discussions 476 | [Issues]: https://github.com/lucasvegi/Elixir-Code-Smells/issues 477 | [LargeMessageExample]: https://samuelmullen.com/articles/elixir-processes-send-and-receive/ 478 | [Agent]: https://hexdocs.pm/elixir/1.13/Agent.html 479 | [Task]: https://hexdocs.pm/elixir/1.13/Task.html 480 | [GenServer]: https://hexdocs.pm/elixir/1.13/GenServer.html 481 | [AgentObsessionExample]: https://elixir-lang.org/getting-started/mix-otp/agent.html#agents 482 | [ElixirInProduction]: https://elixir-companies.com/ 483 | [WorkingWithInvalidDataExample]: https://hexdocs.pm/elixir/master/library-guidelines.html#avoid-working-with-invalid-data 484 | [ModulesWithIdenticalNamesExample]: https://hexdocs.pm/elixir/master/library-guidelines.html#avoid-defining-modules-that-are-not-in-your-namespace 485 | [UnnecessaryMacroExample]: https://hexdocs.pm/elixir/master/library-guidelines.html#avoid-macros 486 | [ApplicationEnvironment]: https://hexdocs.pm/elixir/1.13/Config.html 487 | [AppConfigurationForCodeLibsExample]: https://hexdocs.pm/elixir/master/library-guidelines.html#avoid-application-configuration 488 | [CredoWarningApplicationConfigInModuleAttribute]: https://hexdocs.pm/credo/Credo.Check.Warning.ApplicationConfigInModuleAttribute.html 489 | [Credo]: https://hexdocs.pm/credo/overview.html 490 | [DependencyWithUseExample]: https://hexdocs.pm/elixir/master/library-guidelines.html#avoid-use-when-an-import-is-enough 491 | [ICPC-ERA]: https://conf.researchr.org/track/icpc-2022/icpc-2022-era 492 | [preprint-copy]: https://doi.org/10.48550/arXiv.2203.08877 493 | [jose-valim]: https://github.com/josevalim 494 | [syamilmj]: https://github.com/syamilmj 495 | [Complex-extraction-in-clauses-issue]: https://github.com/lucasvegi/Elixir-Code-Smells/issues/9 496 | [Alternative-return-type-issue]: https://github.com/lucasvegi/Elixir-Code-Smells/issues/6 497 | [Complex-else-clauses-in-with-issue]: https://github.com/lucasvegi/Elixir-Code-Smells/issues/7 498 | [Large-code-generation-issue]: https://github.com/lucasvegi/Elixir-Code-Smells/issues/13 499 | [ICPC22-PDF]: https://github.com/lucasvegi/Elixir-Code-Smells/blob/main/etc/Code-Smells-in-Elixir-ICPC22-Lucas-Vegi.pdf 500 | [ICPC22-YouTube]: https://youtu.be/3X2gxg13tXo 501 | [Podcast-Spotify]: http://elixiremfoco.com/episode?id=lucas-vegi-e-marco-tulio 502 | [to_atom]: https://hexdocs.pm/elixir/String.html#to_atom/1 503 | [to_existing_atom]: https://hexdocs.pm/elixir/String.html#to_existing_atom/1 504 | [Finbits]: https://www.finbits.com.br/ 505 | [ResearchWithElixir]: http://pesquisecomelixir.com.br/ 506 | [FowlerAndBeckCatalog]: https://books.google.com.br/books?id=UTgFCAAAQBAJ 507 | [ElaineYoutube]: https://youtu.be/3-1PCurON4Q 508 | --------------------------------------------------------------------------------