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

Modular Design in Elixir

Modular Design in Elixir

A talk about modular software design given at 9th Poznań Elixir Meetup

Maciej Kaszubowski

November 08, 2018
Tweet

More Decks by Maciej Kaszubowski

Other Decks in Programming

Transcript

  1. mkaszubowski94 if we don’t accept shared state for simple objects,

    why it’s ok to do this on a system level?
  2. mkaszubowski94 Each module can be understood in isolation modified in

    isolation tested in isolation refactored in isolation
  3. mkaszubowski94 create a new job name image url price publish

    John Alchemist create new job manage jobs find a job logout description date
  4. mkaszubowski94 your jobs John Alchemist create new job manage jobs

    find a job logout my awesome job accept my other job cancel accept cancel yet another one publish and one more accepted this didn’t go well canceled
  5. mkaszubowski94 browse jobs John Alchemist create new job manage jobs

    find a job logout my awesome job my other job yet another one and one more filters 100$ 50$ 3500$ 99$ xxxx yyy zzz search
  6. mkaszubowski94 start this job John Alchemist create new job manage

    jobs find a job logout my awesome job Don’t ask, you’ll get paid start a job https://example.com/jobs/1234
  7. mkaszubowski94 defmodule ModularElixir.Job do use Ecto.Schema schema "jobs" do field

    :name, :string field :description, :string field :image_url, :string field :price, :integer field :datetime, :utc_datetime belongs_to :creator, ModularElixir.User belongs_to :contractor, ModularElixir.User end end
  8. mkaszubowski94 create a new job name image url price continue

    John Alchemist create new job manage jobs find a job logout description date
  9. mkaszubowski94 pay for job pay 110$ John Alchemist create new

    job manage jobs find a job logout my awesome job job price………….………..100$ fee (10%)…………………….10$ total……………………………..110$ Don’t ask, you’ll get paid Your job will be published after the payment
  10. mkaszubowski94 defmodule ModularElixir.Job do use Ecto.Schema schema "jobs" do field

    :name, :string field :description, :string field :image_url, :string field :paid, :boolean field :price, :integer field :datetime, :utc_datetime belongs_to :creator, ModularElixir.User belongs_to :contractor, ModularElixir.User end end
  11. mkaszubowski94 start this job John Alchemist create new job manage

    jobs find a job logout my awesome job Don’t ask, you’ll get paid start a job https://example.com/jobs/my-awesome-job
  12. mkaszubowski94 browse jobs John Alchemist create new job manage jobs

    find a job logout my awesome job my other job yet another one and one more filters 100$ 50$ 3500$ 99$ xxxx yyy zzz search Upcoming job Job starting in 54 minutes ! x
  13. mkaszubowski94 defmodule ModularElixir.Job do use Ecto.Schema schema "jobs" do field

    :name, :string field :description, :string field :image_url, :string field :paid, :boolean field :slug, :string field :started, :boolean field :price, :integer field :datetime, :utc_datetime belongs_to :creator, ModularElixir.User belongs_to :contractor, ModularElixir.User end end
  14. mkaszubowski94 JobService - fetch_by_id() - search() - find_by_user_id() - accept()

    - cancel() - publish() - create() - find_by_slug() - send_reminder()
  15. mkaszubowski94 jobs JobService UserService users join (jobs_count) Payments JobSubtask Service

    preload jobs Chat Service JOB USER CONVERSATION MESSAGE PAYMENT SUBTASK
  16. mkaszubowski94 In most projects: • few nouns • a lot

    of interactions between them • logic is organised around the nouns • a lot of coupling as a result
  17. mkaszubowski94 defmodule ModularElixir.Job do use Ecto.Schema schema "jobs" do field

    :name, :string field :description, :string field :image_url, :string field :paid, :boolean field :slug, :string field :started, :boolean field :price, :integer field :datetime, :utc_datetime belongs_to :creator, ModularElixir.User belongs_to :contractor, ModularElixir.User end end
  18. mkaszubowski94 1. what is the desired outcome? (what should happen?)

    2. what data do I need to do this? 3. how can I get this data? 4. what should be exposed in the interface
  19. mkaszubowski94 start this job John Alchemist create new job manage

    jobs find a job logout my awesome job Don’t ask, you’ll get paid start a job https://example.com/jobs/my-awesome-job
  20. mkaszubowski94 SEO Context - register_slug() - fetch_id_by_slug() - fetch_slug_by_id() slug

    resource_id resource_type my-awesome-job 1 job 1 1 job other-job 2 job 2 2 job slugs
  21. mkaszubowski94 def show(conn, %{"job_id"  slug}) do job = slug

     SeoContext.fetch_id_by_slug("job")  JobService.fetch_by_id() #  end
  22. mkaszubowski94 def show(conn, %{"job_id"  slug}) do job = slug

     SeoContext.fetch_id_by_slug("job")  JobService.fetch_by_id() #  end Plug?
  23. mkaszubowski94 defmodule ModularElixir.Job do use Ecto.Schema schema "jobs" do field

    :name, :string field :description, :string field :image_url, :string field :paid, :boolean field :slug, :string field :started, :boolean field :price, :integer field :datetime, :utc_datetime belongs_to :creator, ModularElixir.User belongs_to :contractor, ModularElixir.User end end
  24. mkaszubowski94 JobService - fetch_by_id() - search() - find_by_user_id() - accept()

    - cancel() - publish() - create() - find_by_slug() - send_reminder()
  25. mkaszubowski94 SEO Context • multiple slugs - easy to do

    • slugs for other resources - easy to do • slugs are resolved at the controller level • no slugs in domain logic
  26. mkaszubowski94 no slugs in domain logic /api/jobs/my-awesome-job/subtasks from( s in

    Subtask, join: j in Job, on: s.job_id  j.id where: j.id  ^job_id, # … ) /api/jobs/1/subtasks
  27. mkaszubowski94 browse jobs John Alchemist create new job manage jobs

    find a job logout my awesome job my other job yet another one and one more filters 100$ 50$ 3500$ 99$ xxxx yyy zzz search Upcoming job Job starting in 54 minutes ! x
  28. mkaszubowski94 JobService - fetch_by_id() - search() - find_by_user_id() - accept()

    - cancel() - publish() - create() - find_by_slug() - send_reminder()
  29. mkaszubowski94 defmodule Notifications.Worker do use GenServer def handle_info(:work, state) do

    Notifications.send_notifications(now()) Process.send_after(self(), :work, @interval) {:noreply, state) end end
  30. mkaszubowski94 defmodule Notifications do defmodule Logic do def send(jobs, datetime,

    already_sent) do Enum.each(jobs, fn job -> if starting_soon?(job) && not_sent_yet?(job, already_sent) do send_notification(job) end end) end end def send_notifications(datetime) do jobs = JobBoard.search(…) already_sent = fetch_already_sent() :ok = Logic.send(jobs, datetime, already_sent) end end
  31. mkaszubowski94 JobService - fetch_by_id() - search() - find_by_user_id() - accept()

    - cancel() - publish() - create() - find_by_slug() - send_reminder()
  32. mkaszubowski94 defmodule JobBoard do defmodule Job do @keys [:name, :description,

    ] @enforce_keys @keys defstruct @keys end def publish(%Job{} = job) do #  end end
  33. mkaszubowski94 Job Publishing jobs Job Board jobs publish enforces job

    structure in params decides when the job is published
  34. mkaszubowski94 defmodule ModularElixir.JobBoard.Job do use Ecto.Schema @schema_prefix “job_board” schema "jobs"

    do field :name, :string field :description, :string field :image_url, :string field :paid, :boolean field :slug, :string field :started, :boolean field :price, :integer field :datetime, :utc_datetime belongs_to :creator, ModularElixir.User belongs_to :contractor, ModularElixir.User end end
  35. mkaszubowski94 JobService - fetch_by_id() - search() - find_by_user_id() - accept()

    - cancel() - publish() - create() - find_by_slug() - send_reminder()
  36. mkaszubowski94 JobService - fetch_by_id() - search() - find_by_user_id() - accept()

    - cancel() - publish() - create() - find_by_slug() - send_reminder()
  37. mkaszubowski94 JobBoard - fetch_by_id() - search() - find_by_user_id() - accept()

    - cancel() - publish() - create() - find_by_slug() - send_reminder()
  38. mkaszubowski94 JobBoard • can be tested in isolation (without mocking

    payments) • linear logic (without conditionals) • no risk of showing unpublished jobs
  39. mkaszubowski94 JobPublishing • rules for publishing can be changed in

    isolation • can be used alongside other modules (e.g. Admins can publish jobs without paying)
  40. mkaszubowski94 JobBoard - fetch_by_id() - search() - find_by_user_id() - accept()

    - cancel() - publish() - create() - find_by_slug() - send_reminder()
  41. mkaszubowski94 defmodule ModularElixir.JobBoard.Job do use Ecto.Schema @schema_prefix “job_board” schema "jobs"

    do field :name, :string field :description, :string field :image_url, :string field :paid, :boolean field :slug, :string field :started, :boolean field :price, :integer field :datetime, :utc_datetime belongs_to :creator, ModularElixir.User belongs_to :contractor, ModularElixir.User end end
  42. mkaszubowski94 JobBoard - fetch_by_id() - search() - find_by_user_id() - accept()

    - cancel() - publish() - create() - find_by_slug() - send_reminder()
  43. mkaszubowski94 JobService • multiple responsibilities • hard to change and

    understand • hard to test in isolation • grows with each new feature • hard to delete old features
  44. mkaszubowski94 Smaller modules • clear responsibilities for each module •

    easy to change, refactor, understand • easy to test in isolation • tend to stay small • trivial to delete if no longer necessary
  45. mkaszubowski94 defmodule ModularElixir.Job do use Ecto.Schema schema "jobs" do field

    :name, :string field :description, :string field :image_url, :string field :paid, :boolean field :slug, :string field :started, :boolean field :price, :integer field :datetime, :utc_datetime belongs_to :creator, ModularElixir.User belongs_to :contractor, ModularElixir.User end end
  46. mkaszubowski94 the flow of the app is visible in the

    module structure and dependencies
  47. mkaszubowski94 Important decisions • what are the rules for publishing

    jobs? • when/what notifications are sent? • which jobs are visible on job board? • how is a contractor chosen for each job? • what happens after a job is started? when the job ends? • how payments work? (when creator pays for job, how the contractor receives the money? do we allow coupons? credit cards? money transfers?) • …
  48. mkaszubowski94 Make each module • independent • easy to test

    • easy to understand • with a strong interface