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

The Big Ball of Nouns

The Big Ball of Nouns

A talk about modular software design in Elixir given at CodeBEAM Lite Amsterdam 2018

Maciej Kaszubowski

November 30, 2018
Tweet

More Decks by Maciej Kaszubowski

Other Decks in Programming

Transcript

  1. mkaszubowski94 Each module can be: understood in isolation modified in

    isolation tested in isolation replaced or removed
  2. mkaszubowski94 create a new job name image url price publish

    John Alchemist create new job manage jobs find a job logout description date
  3. 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
  4. 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
  5. mkaszubowski94 start this job John Alchemist create new job manage

    jobs find a job logout Job Title I need something done start a job https://example.com/jobs/1234 100$
  6. 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
  7. 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
  8. 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 :slug, :string field :datetime, :utc_datetime belongs_to :creator, ModularElixir.User belongs_to :contractor, ModularElixir.User end end
  9. mkaszubowski94 JobContext - fetch_by_id() - search() - find_by_user_id() - start()

    - accept() - cancel() - publish() - create() - find_by_slug()
  10. mkaszubowski94 create a new job name image url price continue

    John Alchemist create new job manage jobs find a job logout description date
  11. 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
  12. 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
  13. 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
  14. 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 :notification_sent, :boolean field :price, :integer field :datetime, :utc_datetime belongs_to :creator, ModularElixir.User belongs_to :contractor, ModularElixir.User end end
  15. mkaszubowski94 JobContext - fetch_by_id() - search() - find_by_user_id() - start()

    - accept() - cancel() - publish() - create() - find_by_slug() - send_reminder()
  16. mkaszubowski94 jobs JobContext User Context users join (jobs_count) Payments JobSubtask

    Context preload jobs Chat Context JOB USER CONVERSATION MESSAGE PAYMENT SUBTASK
  17. mkaszubowski94 In most projects: • few nouns / entities •

    a lot of interactions between them • logic is organised around the nouns • a lot of coupling as a result
  18. 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 :notification_sent, :boolean field :price, :integer field :datetime, :utc_datetime belongs_to :creator, ModularElixir.User belongs_to :contractor, ModularElixir.User end end
  19. mkaszubowski94 1. what is the desired outcome? 2. what data

    do I need to do this? 3. how can I get this data?
  20. mkaszubowski94 1. what is the desired outcome? 2. what data

    do I need to do this? 3. how can I get this data? 4. what should be exposed in the interface?
  21. 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
  22. mkaszubowski94 def show(conn, %{"job_id"  slug}) do job = slug

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

     SeoContext.fetch_id_by_slug("job")  JobContext.fetch_by_id() #  end Plug?
  24. mkaszubowski94 defp put_slugs(jobs) do ids = Enun.map(jobs, & &1.id) slugs

    = SEOContext.fetch_slugs_by_ids(id) Enum.map(jobs, fn job  Map.put(job, :slug, slugs[job.id]) end) end
  25. 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 :notification_sent, :boolean field :price, :integer field :datetime, :utc_datetime belongs_to :creator, ModularElixir.User belongs_to :contractor, ModularElixir.User end end
  26. mkaszubowski94 JobContext - fetch_by_id() - search() - find_by_user_id() - start()

    - accept() - cancel() - publish() - create() - find_by_slug() - send_reminder()
  27. mkaszubowski94 SEO Context • slugs for other resources - easy

    to do • multiple slugs - easy to do • slugs are resolved at the controller level • no slugs in domain code
  28. 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 or j.slug  ^job_id, # … ) /api/jobs/1/subtasks
  29. 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
  30. mkaszubowski94 Job Reminders upcoming_jobs - put_job() - remove_job() - register_device()

    devices Interface: It’s hard to use (you have to remember to remove the job after starting)
  31. 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
  32. mkaszubowski94 defmodule Notifications do def send_notifications(datetime) do jobs = JobContext.search(…)

    already_sent = fetch_already_sent() sender = FirebaseAdapter :ok = Logic.send( jobs, datetime, already_sent, sender ) end end
  33. mkaszubowski94 defmodule Notifications.Logic do def send(jobs, datetime, already_sent, sender) do

    Enum.each(jobs, fn job -> if starting_soon?(job) && not_sent_yet?(job, already_sent) do sender.send_notification(job) end end) end end
  34. 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 :notification_sent, :boolean field :price, :integer field :datetime, :utc_datetime belongs_to :creator, ModularElixir.User belongs_to :contractor, ModularElixir.User end end
  35. mkaszubowski94 JobContext - fetch_by_id() - search() - find_by_user_id() - start()

    - accept() - cancel() - publish() - create() - find_by_slug() - send_reminder()
  36. mkaszubowski94 JobContext • adding / publishing jobs • searching for

    available jobs • managing ongoing jobs • archive / history • …
  37. mkaszubowski94 JobContext - fetch_by_id() - search() - find_by_user_id() - start()

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

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

    structure in params controls the rules for publishing
  40. 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 :notification_sent, :boolean field :price, :integer field :datetime, :utc_datetime belongs_to :creator, ModularElixir.User belongs_to :contractor, ModularElixir.User end end
  41. mkaszubowski94 JobContext - fetch_by_id() - search() - find_by_user_id() - start()

    - accept() - cancel() - publish() - create() - find_by_slug() - send_reminder()
  42. mkaszubowski94 JobContext - fetch_by_id() - search() - find_by_user_id() - start()

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

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

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

    isolation • can be used alongside other modules
  46. mkaszubowski94 JobBoard - fetch_by_id() - search() - find_by_user_id() - start()

    - accept() - cancel() - publish() - create() - find_by_slug() - send_reminder()
  47. 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 :notification_sent, :boolean field :price, :integer field :datetime, :utc_datetime belongs_to :creator, ModularElixir.User belongs_to :contractor, ModularElixir.User end end
  48. mkaszubowski94 JobBoard - fetch_by_id() - search() - find_by_user_id() - start()

    - accept() - cancel() - publish() - create() - find_by_slug() - send_reminder()
  49. mkaszubowski94 JobContext • no clear responsibility • hard to change

    and understand • hard to test in isolation • grows with each new feature • hard to delete old features
  50. 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
  51. mkaszubowski94 the flow of the app is visible in the

    module structure and dependencies
  52. 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? • …
  53. mkaszubowski94 Make each module • independent • easy to test

    • easy to understand • with a strong interface