Al giorno d'oggi un buon programmatore non lo distingui solo dalle competenze che ha sul singolo linguaggio ma dalla capacità di sapere scrivere codice leggibile e manutenibile. Un metodo per affinare questa capacità può essere costruito da alcune pratiche base che ogni buon professionista dello sviluppo software dovrebbe portare nella propria cassetta degli attrezzi. In questo talk vi presento un case study su come si può imparare un nuovo linguaggio di programmazione (Elixir) avendo come supporto all'apprendimento le pratiche del Clean Code e del Test-Driven Development.
Powerful Google developer tools for immediate impact! (2023-24 C)
Joe Bew - Apprendi un nuovo linguaggio sfruttando il TDD e il Clean Code - Codemotion Milan 2017
1. Ho imparato qualcosa di Elixir
Il Test-Driven Development e il Clean Code come supporto per apprendere
un nuovo linguaggio di programmazione
CODEMOTION MILAN - SPECIAL EDITION
10 – 11 NOVEMBER 2017
2. Ho imparato qualcosa di Elixir
Il Test-Driven Development e il Clean Code come supporto per apprendere un
nuovo linguaggio di programmazione
10. Faremo manutenzione …
(prima o poi)
● Fix a bug
● Add new features
● Other Changes:
Because changes happens
https://static.pexels.com/photos/1599/building-vehicle-motorbike-motorcycle.jpg
23. .
Finished in 0.02 seconds
1 test, 0 failures
Randomized with seed 877741
24. HelloWorldTest
* test should return a greeting (1.3ms)
Finished in 0.03 seconds
1 test, 0 failures
Randomized with seed 689156
25. CHAPTER TWO:
test the documentation
seguire la documentazione scrivendo prima i test
26. defmodule GreetingTest do
use ExUnit.Case, async: true
test "should return a greeting" do
pid = spawn(Greeting, :loop, [])
send pid, {self(), :greet}
assert_receive "hello world"
end
end
27. defmodule Greeting do
def loop do
receive do
{from_pid, :greet} ->
send from_pid, "hello world"
loop()
end
end
end
28. defmodule GreetingTest do
use ExUnit.Case, async: true
test "should return a greeting" do
pid = spawn(Greeting, :loop, [])
send pid, {self(), :greet}
assert_receive "hello world"
end
end
29. defmodule GreetingTest do
use ExUnit.Case, async: true
test "should return a greeting" do
pid = Greeting.start
response = Greeting.greet(pid)
assert "hello world" == response
end
end
30. defmodule Greeting do
def start do
spawn(fn -> loop() end)
end
def greet(pid) do
send pid, {self(), :greet}
receive do
reply -> reply
end
end
...
31. test "should return a greeting in spanish" do
pid = Greeting.start(:spanish)
response = Greeting.greet(pid)
assert "hola mundo" == response
end
32. defmodule Greeting do
def start(language nil) do
spawn(fn -> loop(language) end)
end
defp loop(language) do
receive do
{from_pid, :greet} ->
send from_pid, greet_message_for(language)
loop(language)
end
end
defp greet_message_for(:spanish), do: "hola mundo"
defp greet_message_for(_), do: "hello world"
end
34. The Bank Account Application
TODO
- Posso creare un nuovo account
- Posso eliminare un account
- Posso controllare il saldo corrente
per ciascun account
- Posso depositare un importo
- Posso prelevare un importo
DOING
35. The Bank Account Application
TODO
- Posso creare un nuovo account
- Posso eliminare un account
- Posso controllare il saldo corrente
per ciascun account
- Posso depositare un importo
- Posso prelevare un importo
DOING
36. test "should create a new account" do
bank_pid = Bank.start()
created_response = Bank.create_account(bank_pid, "non_existing_account")
assert {:ok, :account_created} == created_response
end
37. defmodule Bank do
def start() do
nil
end
def create_account(_pid, _name) do
{:ok, :account_created}
end
end
38. test "should return an error when the account exists" do
bank_pid = Bank.start()
Bank.create_account(bank_pid, "non_existing_account")
response = Bank.create_account(bank_pid, "non_existing_account")
assert {:error, :account_already_exists} == response
end
39. defmodule Bank do
def start() do
spawn(fn -> loop(%{}) end)
end
def create_account(pid, name) do
send pid, {self(), {:create, name}}
receive do
reply -> reply
end
end
...
...
defp loop(accounts) do
receive do
{from_pid, {:create, name}} ->
{updated_accounts, message} = _create_account(name, accounts)
send from_pid, message
loop(updated_accounts)
end
end
defp _create_account(name, accounts) do
case Map.has_key?(accounts, account) do
true -> {accounts, {:error, :account_already_exists}}
_ -> {Map.put(accounts, name, nil), {:ok, :account_created}}
end
end
end
40. test "should delete an existing account" do
bank_pid = Bank.start()
not_exists_response = Bank.delete_account(bank_pid, "an_account")
Bank.create_account(bank_pid, "an_account")
deleted_response = Bank.delete_account(bank_pid, "an_account")
assert {:error, :account_not_exists} == not_exists_response
assert {:ok, :account_deleted} == deleted_response
end
41. defp loop(accounts) do
receive do
{from_pid, message} ->
{updated_accounts, reply} = handle_message(message, accounts)
send from_pid, reply
loop(updated_accounts)
end
end
defp handle_message({:create, name}, accounts)
case Map.has_key?(accounts, name) do
true -> {accounts, {:error, :account_already_exists}}
_ -> {Map.put(accounts, name, nil), {:ok, :account_created}}
end
end
defp handle_message({:delete, name}, accounts)
case Map.has_key?(accounts, name) do
true -> {Map.delete(accounts, name), {:ok, :account_deleted}}
_ -> {accounts, {:error, :account_not_exists}}
end
end
42. test "should return the current balance of an existing account" do
bank_pid = Bank.start()
Bank.create_account(bank_pid, "an_account")
response = Bank.check_balance(bank_pid, "an_account")
assert {:ok, 0} == response
end
43. def check_balance(pid, name) do
send pid, {self(), {:check_balance, name}}
receive do
reply -> reply
end
end
defp handle_message({:check_balance, name}, accounts)
case Map.has_key?(accounts, name) do
false -> {accounts, {:error, :account_not_exists}}
_ -> {accounts, {:ok, Map.get(accounts, name)}}
end
end
44. BankTest
* test when account exists we are able to deposit positive amounts (0.04ms)
* test when account exists we are not able to withdraw if the amount is greater than current balance (0.01ms)
* test when account exists we are able to delete an account (0.01ms)
* test when account does not exists we are able to create a new account (0.00ms)
* test when account exists we are not able to deposit negative amounts (0.01ms)
* test when account exists we are not able to withdraw a negative amount (0.01ms)
* test when account does not exists we are not able to check current balance (0.00ms)
* test when account does not exists we are not able to deposit (0.00ms)
* test when account exists we are able to withdraw if the amount is lower or equal than current balance (0.01ms)
* test when account does not exists we are not able to delete an account (0.00ms)
* test when account exists we are able to check the current balance (0.01ms)
* test when account exists we are not able to create an account with the same name (0.00ms)
* test when account exists we are able to perform actions on different accounts (0.02ms)
* test when account does not exists we are not able to withdraw (0.00ms)
Finished in 0.05 seconds
14 tests, 0 failures
45. Se provassimo a estrarre un processo per ogni account?
Questo è refactoring.
46. defp handle_message({:create, name}, accounts)
case Map.has_key?(accounts, name) do
true -> {accounts, {:error, :account_already_exists}}
_ ->
bank_account = BankAccount.start()
{Map.put(accounts, name, bank_account), {:ok, :account_created}}
end
end
47. E poi tante altre cose ...
Images from http://learnyousomeerlang.com
defmodule Bank do
use Application
def start(_type, _args) do
Bank.Supervisor.start_link([])
end
end
defmodule Bank.Supervisor do
use Supervisor
def start_link(opts) do
Supervisor.start_link(__MODULE__, :ok, opts)
end
def init(:ok) do
children = [
Bank,
AccountSupervisor
]
Supervisor.init(children, strategy: :one_for_one)
end
end
48. The Bank Account Application
TODO
- Posso creare un nuovo account
- Posso eliminare un account
- Posso controllare il saldo corrente
per ciascun account
- Posso depositare un importo
- Posso prelevare un importo
DOING
- Voglio esporre una API HTTP
49. The Bank Account Application
TODO
- Posso creare un nuovo account
- Posso eliminare un account
- Posso controllare il saldo corrente
per ciascun account
- Posso depositare un importo
- Posso prelevare un importo
DOING
- Voglio esporre una API HTTP
50. CHAPTER FOUR (the last one):
a RESTful API
esporre l’applicazione appena creata in RESTful
51. defmodule Http.HelloWorldTest do
use ExUnit.Case, async: true
use Plug.Test
@opts Http.HelloWorld.init([])
test "should return a greeting" do
conn =
conn(:get, "/greet")
|> Http.HelloWorld.call(@opts)
assert 200 == conn.status
assert "hello world" == conn.resp_body
end
end
52. defmodule Http.HelloWorld do
use Plug.Router
plug :match
plug :dispatch
get "/greet" do
send_resp(conn, 200, "hello world")
end
end
53. defmodule Http.RouterTest do
use ExUnit.Case, async: true
use Plug.Test
import Mock
@router Http.Router
@opts @router.init([])
test "should return 201 when a new account is created" do
with_mock Bank.Admin, [create_account: fn("joe") -> {:ok, :account_created} end] do
conn = do_request(:post, "/accounts/joe")
assert 201 == conn.status
assert "/accounts/joe" == conn.resp_body
end
end
defp do_request(verb, endpoint, payload) do
conn(verb, endpoint, payload)
|> @router.call(@opts)
end
end
54. defmodule Http.Router do
use Plug.Router
plug :match
plug :dispatch
post "/accounts/:account_name" do
{:ok, :account_created} = Bank.Admin.create_account(account_name)
send_resp(conn, 201, "/accounts/" <> account_name)
end
end
55. defmodule Http.RouterTest do
use ExUnit.Case, async: true
use Plug.Test
import Mox
@router Http.Router
@opts @router.init([])
test "returns 201 when a new account is created" do
expect(Bank.AdminMock, :create_account, fn("joe") -> {:ok, :account_created} end)
conn = do_request(:post, "/accounts/joe")
assert 201 == conn.status
assert "/accounts/joe" == conn.resp_body
verify! Bank.AdminMock
end
defp do_request(verb, endpoint, payload) do
conn(verb, endpoint, payload)
|> @router.call(@opts)
end
end
56. defmodule Http.Router do
use Plug.Router
plug :match
plug :dispatch
@bank_admin Application.get_env(:bank, :bank_admin)
post "/accounts/:account_name" do
{:ok, :account_created} = @bank_admin.create_account(account_name)
send_resp(conn, 201, "/accounts/" <> account_name)
end
end
57. defmodule Bank.Supervisor do
use Supervisor
def start_link(opts) do
Supervisor.start_link(__MODULE__, :ok, opts)
end
def init(:ok) do
children = [
Bank,
AccountSupervisor,
Plug.Adapters.Cowboy.child_spec(:http, Http.Router, [], [port: 4000])
]
Supervisor.init(children, strategy: :one_for_one)
end
end
58. # checkout the project
git clone https://github.com/joebew42/elixir_bank_account.git
cd elixir_bank_account/
mix test
iex -S mix
# create an account
curl -v -H "auth: joe" -X "POST" http://localhost:4000/accounts/joe
# deposit an amount
curl -v -H "auth: joe" -X "PUT" http://localhost:4000/accounts/joe/deposit/100
# check current balance
curl -v -H "auth: joe" -X "GET" http://localhost:4000/accounts/joe
{ "current_balance": 1100 }
# delete an account
curl -v -H "auth: joe" -X "DELETE" http://localhost:4000/accounts/joe
67. “Learn to build working software
by playing our preferred videogame …
… Writing code!” - @joebew42
68. This presentation is released under
Attribution-NonCommercial-ShareAlike 3.0 Unported (CC BY-NC-SA 3.0)
https://creativecommons.org/licenses/by-nc-sa/3.0/