Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Como organizar seu projeto Elixir?

Como organizar seu projeto Elixir?

Essa palestra irá explorar várias práticas de organização de diretórios e arquivos em projetos Elixir. Vamos explorar alguma práticas comuns não só em Elixir como no desenvolvimento de software em geral, como por exemplo, MVC, Camadas, Contextos, arquitetura hexagonal e componentes. Aqui, iremos discutir vantagens e desvantagens de cada uma das práticas e evidenciar o que os projetos Elixir Open Source tem adotado. No final da palestra, você será capaz de comparar e decidir melhor que práticas adotar no seu projeto em Elixir.

50e713934ed341675bf1fa73127ec260?s=128

Ulisses Almeida

November 06, 2020
Tweet

Transcript

  1. How to Organize your Elixir Projects?

  2. Summary — Code organization? — Architecture patterns & Elixir —

    Layers — MVC — View Components — Bounded Context — Architecture |> Hexagonal |> Onion |> Clean |> ??? — Pragmatic Elixir
  3. I'm Ulisses Almeida — Work for The Coingaming Group —

    living in Estonia — I'm also author of Learn Functional Programming with Elixir
  4. Code organization — Files and folders — Modules, functions, and

    their relationships — Web developer / Server side / Monolith perspective
  5. Book table of contents

  6. Fiction book

  7. Academic book

  8. Elixir project . ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── LICENSE ├──

    README.md ├── _build ├── config ├── deps ├── doc ├── lib ├── mix.exs ├── mix.lock └── test
  9. Elixir project docs

  10. Your project architecture should help people find what they are

    looking for and express what the application does
  11. Architecture patterns Your application is not that special

  12. Layers We need to start somewhere

  13. Layers — They organically emerges — Top to bottom dependency

    and dataflow
  14. Layers in Phoenix/Elixir lib/hello_web lib/hello 1. Web Layer: hello_web 2.

    Domain Layer: hello
  15. Layers in Phoenix/Elixir — Web Layer depends on Domain —

    Domain layer should not depend on parent layer # hello/application.ex children = [ # ... # Start the Endpoint (http/https) HelloWeb.Endpoint # ... ] ! oops
  16. Layers — Data layer: put your queries ☑ — Business

    layer: put your business logic — Presentation layer: put your HTML and stuff — How each layer should be organized? — Where I put external service wrappers?
  17. MVC A structured presentation layer

  18. MVC — Model-View-Controller & variations — User interface centric

  19. MVC In Phoenix/Elixir ├── assets (V) ├── lib │ └──

    hello (M) │ └── hello_web ├── channels (C) ├── controllers (C) ├── templates (V) ├── views (v) ├── endpoint.ex (C) ├── gettext.ex (V) ├── router.ex (C)
  20. MVC In Phoenix/Elixir (please don't) ├── lib └── hello (model)

    └── hello_web ├── controllers ├── endpoint.ex ├── router.ex ├── channels ├── requests (old controlers) ├── views ├── assets ├── gettext.ex ├── templates ├── posts ├── post_view.ex ├── index.html.eex — would it make a newcomer life easier or harder to understand?
  21. MVC — Model: put your business and queries — Controllers:

    put your event orchestration logic(sometimes business logic " ) ☑ — Views: put your HTML and stuff ☑ — How your model should be organized? — Where I put external service wrappers?
  22. View Components A new (and old) way of organizing your

    presentation layer
  23. View components — Small, independent and self-contained "presentation" files

  24. View components in Elixir — React, VueJS, and etc lives

    in assets folder — Phoenix LiveView in lib/yourapp_web/live defmodule UserComponent do use Phoenix.LiveComponent def render(assigns) do ~L""" <div id="user-<%= @id %>" class="user"><%= @user.name %></div> """ end end — Check out Surface! — Does it fix the accidental complexity of controllers, views, templates, and assets organization?
  25. View components — View Component: put a little bit of

    everything there — How your domain should be organized? — Where I put external service wrappers?
  26. Bounded Context Let's give some ❤ to your domain

  27. Bounded Context

  28. Bounded Context — Organize a large domain in smaller groups

    (bounded context) — Same entity name can appear in different contexts — Why? A unified domain model for whole organization isn't cost- effective https://martinfowler.com/bliki/BoundedContext.html
  29. Bounded Context vs Elixir/Phoenix contexts — We have Phoenix contexts

    (isn't by the book as Bounded Context) — The context module is work as public API/entrypoint — Works like any other Elixir project ( ?)
  30. Like any other Elixir project ( ! ?) — Has

    public modules, and private modules: @moduledoc false
  31. Expanding our domain model ├── lib └── hello.ex └── hello

    └── accounts └── user.ex └── sales └── customer.ex └── product.ex └── territory.ex └── support └── customer.ex └── product.ex └── ticket.ex └── accounts.ex └── sales.ex └── support.ex └── repo.ex
  32. Personally, I like the entrypoint modules inside their folder ├──

    lib └── hello └── hello.ex └── repo.ex └── accounts └── accounts.ex └── user.ex └── sales └── sales.ex └── customer.ex └── product.ex └── territory.ex └── support └── customer.ex └── product.ex └── ticket.ex
  33. "Namespacing" with contexts Hello.Accounts Hello.Accounts.User Hello.Sales.Customer Hello.Support.Customer Remember that Elixir

    doesn't have any feature related to namespace.
  34. How context modules looks like: @doc """ Gets a user

    by email and password. """ @spec get_user_by_email_and_password(String.t(), String.t()) :: User.t | nil def get_user_by_email_and_password(email, password) when is_binary(email) and is_binary(password) do user = Hello.Repo.get_by(User, email: email) if User.valid_password?(user, password) and User.active?(user), do: user end # for public functions, but internal to context @doc false
  35. @doc """ Gets a user by email and password. """

    @spec get_user_by_email_and_password(String.t(), String.t()) :: User.t | nil def get_user_by_email_and_password(email, password) when is_binary(email) and is_binary(password) do user = Hello.Repo.get_by(User, email: email) if User.valid_password?(user, password) and User.active?(user), do: user end — Note that context module can dispatch logic its "internal" modules — But the internal module should not depend on context module (risky of compilation deadlock) — Common shareable logic inside of a context needs to create a sibling internal module — A context can depend on sibling context modules like "Repo", or "Sales" — But you should avoid cyclic dependencies (compilation deadlock)
  36. How far should we go with contexts? — Should all

    schema/struct modules be hidden? Answer: Remember Ecto.Changeset, Ecto.Query... — Should my AppWeb only be able to access App module? Answer: Remember Ecto.Changeset, Ecto.Query... — How breakdown a huge context file? Answer: Try to distribute pure functions logic on schemas, and maybe create a subcontext.
  37. How far should we go with contexts? — How avoid

    contexts cyclic dependency? Answer: New context, or a new domain entrypoint function (hello.ex) — What about your GenServers, Agents, Supervisors? Answer: Depends, maybe your context can be also the entrypoint of the supervision tree or you can split in a different context/module
  38. Contexts and the Web dir? └── hello_web ├── controllers ├──

    user_controller.ex HelloWeb.UserController # router.ex scope "/", HelloWeb do pipe_through :browser resources "/users", UserController end
  39. Should be like this? └── hello_web ├── controllers ├── user.ex

    # router.ex scope "/", HelloWeb.Controllers do pipe_through :browser resources "/users", User end
  40. Or like this? └── hello_web ├── users ├── controller.ex ├──

    view.ex ├── templates.ex # router.ex scope "/", HelloWeb do pipe_through :browser resources "/users", Users.Controller end
  41. Remember: your project organization should help people find what they

    are looking for Tip: Keep the default !
  42. Phoenix Contexts — Domain folder: how to organize your business

    — It's not about where to put your HTML, and stuff — Where should I put my queries? # — Where I put external service wrappers?
  43. Hexagonal Architecture & Variations Isolate your application core logic

  44. Hexagonal Architecture — New thoughts how organize your app —

    Application vs outside world — Alistair Cockburn: https://alistair.cockburn.us/hexagonal- architecture/
  45. ⬢ Hexagonal Elixir ├── lib └── hello (application) └── hello_web

    (external)
  46. Growing with ⬢ Hexagonal Elixir ├── lib └── hello (application)

    └── hello_web (external) └── mix (external) └── paypal (external) └── twitter (external) └── tesla_extensions.ex ("utils" module for Tesla) — The adapters for external services can live outside of the application core folder — Mix custom commands or other libraries resuable and shareable utilities can live in lib
  47. ⬢ Hexagonal Elixir vs Repo — The Ecto.Repo is an

    excellent database adapter — Should MyApp.Repo be in or out of your application? ├── lib └── hello (application) └── hello_web (external) └── hello_repo.ex (external ?) defmodule HelloRepo do use Ecto.Repo, otp_app: :payments_flask, adapter: Ecto.Adapters.Postgres end
  48. Onion Architecture — More layers in the "area" — Coupling

    towards to center — Jefrey Palermo https://jeffreypalermo.com/2008/07/the-onion- architecture-part-1/
  49. ! Onion Elixir ├── lib └── hello (app + domain

    services + model) └── hello_web (user interface) └── mix (user interface) └── paypal (infrastructure) └── twitter (infrastructure) └── tesla_extensions.ex ("library, utils")
  50. Clean Architecture — Different naming but follow the same concepts

    Robert C. Martin, https://blog.cleancoder.com/uncle-bob/2012/08/13/the- clean-architecture.html
  51. ✨ Clean Elixir ├── lib └── hello (use cases +

    entities) └── hello_web (web framework) └── mix (ui) └── paypal (external interface) └── twitter (external interface) └── tesla_extensions.ex ("library, utils") — What happens with Repo?
  52. Repo & Queries in ✨ Clean Elixir — All queries

    shouldn't belong to entities or contexts Repo.insert(changeset) # good in contexts modules Repo.get_by(User, email: email) # good in contexts modules # not good in context modules or schema modules query = from p in Post, select: p.title MyRepo.all(query)
  53. One solution: Create dynamic queries in Repo defmodule MyApp.Repo do

    # ... def select_titles_from(queryable) do query = from p in queryable, select: p.title all(query) end end # in module context: Repo.select_titles_from(Post) — Note, the Repo module might get overloaded with many queries. You might want to breakdown in public submodules.
  54. Hexagonal Architecture — Application: put your business services/orchestration — Don't

    mix your HTML, and stuff in application — Don't mix your queries in application — Don't mix your service wrappers in application — Isn't clear the dependency between the components
  55. Pragmatic Elixir My personal opinions !

  56. Pragmatic Elixir — "Elixir library" mindset is good to keep

    in mind: Which inner modules are supposed to be public or private? — Coupling towards to the center of your application — Preserving a pure funcional core
  57. Pragmatic Elixir & Frameworks ├── assets ├── lib └── hello

    └── hello.ex └── application.ex └── repo.ex └── hello_web # ... — Don't fight against your chosen framework — It's fine do some experiments, moving one thing or another per time
  58. Pragmatic Elixir & Lib folder ├── lib └── hello (domain)

    └── hello_web (ui, phoenix) └── mix (ui) └── paypal (external) # sometimes can be a overkill └── twitter (external) └── tesla_extensions.ex ("library utils") — Use more your "lib" folder — Keep independent external code outside of your domain — Treat them as "libraries" — Avoid god modules called "utils"
  59. Pragmatic Elixir & Domain hello └── hello.ex └── repo.ex └──

    user_registration.ex └── accounts └── accounts.ex └── user.ex └── sales └── sales.ex └── customer.ex └── product.ex └── territory.ex └── report.ex └── support └── customer.ex └── product.ex └── ticket.ex
  60. Pragmatic Elixir & Domain hello └── user_registration.ex — Not all

    contexts need structs/schemas, they can have only functions — Your contexts doesn't always need to hide the structs/schemas
  61. Pragmatic Elixir & Domain └── sales └── report.ex — It's

    fine starting your queries as private functions in your context modules — If the queries get too big, you might want to create inner modules, like report.ex
  62. Pragmatic Elixir & Domain — It's kinda ok cross references

    of schema modules between contexts, but be careful with cyclic dependency — Try to keep your functions cohesive and related code together
  63. Pragmatic Elixir — lib/yourapp_web -> framework MVC to organize your

    HTML — lib/yourapp ->:bounded/phoenix contexts & dat — lib/ -> practice hexagonal architecture for independent external stuff code
  64. https://twitter.com/jdvogt/status/1319065447346573313

  65. Sometimes you need to break the consistency rules to maintain

    clarity!
  66. Thanks!

  67. References — https://hexdocs.pm/ecto/Ecto.html — https://vuejs.org/v2/guide/ — https://hexdocs.pm/phoenixliveview/Phoenix.LiveView.html — https://hex.pm/packages/phoenix —

    https://alistair.cockburn.us/hexagonal-architecture/ — https://jeffreypalermo.com/2008/07/the-onion-architecture-part-1/ — https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean- architecture.html — https://martinfowler.com/bliki/BoundedContext.html