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

The evolution of service objects

The evolution of service objects

Rostislav Zhuravsky

April 15, 2021
Tweet

More Decks by Rostislav Zhuravsky

Other Decks in Programming

Transcript

  1. Agenda • What does the term business logic even mean?

    • Write a small piece of logic for an online store • Discuss and highlight pros and cons of each approach
  2. Reasons • Maintainability/Extensibility - how easy and fast to implement

    new features • Testability - how easy to set up all data and cover all cases, fake or substitute dependencies • Readability - how easy to navigate and supposes where a responsible code piece is placed
  3. Our task has the following steps: 1. Find a user

    by a auth token 2. Validate address info 3. Calculate a total of all line items in a cart 4. Try to charge the corresponded amount from the user account 5. Prepare order params and save them into DB 6. Send an email to the user that order is created
  4. What might go wrong: 1. There is no user with

    a sent token 2. Address info isn’t fully completed 3. A user doesn’t have enough amount of money
  5. Agreements • Callbacks and validations in models are evil •

    Skip authorization layer • All our dependencies are separate objects
  6. Problems • The business operation logic is splitted. We find

    a user in controller and than we call the method on it. • Error messages are not descriptive.
  7. Measures and metrics • LOC • How easy to test

    a solution • How easy to orient in a solution • How easy to add new features • Something else
  8. Testing • Easy to set up required data, only create

    a user and pass required params • Dependencies on real constants
  9. Readability • The business logiс looks clear by itself •

    A single simple business operation takes roughly 80 lines of code • Secondary private methods
  10. Extensibility • It’s easy to reuse the entire operation in

    background jobs, rake tasks • It’s hard to use intermediate pieces of logic in other parts of the app • Depending on real constants forces you to create if/else branches Imagine the situation when you need to send emails via other mailer or get/put data from other sources. Or validating data by other rules.
  11. Testing • Still depending on real consts • Hard to

    set up env to call because of binding with HTTP layer. It’s easy to do using rails helpers for testing. But anyway, to build a single operation your test env should set up HTTP stuff.
  12. Readability • The business logiс looks clear by itself •

    A single simple business operation takes roughly 70 lines of code • Secondary private methods • It forces you to create custom actions in your controllers
  13. Extensibility • Still depending on real constants forces you to

    create if/else branches • Hard to reuse the entire operation in other parts of the application, for example, CLI, background jobs. • It’s hard to compose/chain with other pieces of an application. For example call controller methods in other places.
  14. All kinds of service objects I’ve seen Usage Mutable/ Immutable

    Using instance variables for keeping data Real constants/ Dependency Injection Thread-safe in Puma/Sidekiq Thread-safe by themselves Service.call(data) Mutable Yes Real constants No No Service.new(data).call/ Service.new.call(data) Mutable Yes Real constants Yes No Service.call(data) Immutable No Real constants Yes Yes Service.call(dependencies, data) Immutable No Dependency Injection Yes Yes Service.new(dependencies) .call(data) Immutable No Dependency Injection Yes Yes
  15. Service Objects are business rules. Controllers, background workers, rake tasks

    are gateways of your application. They mustn’t contain business rules they can only preprocess your data to pass into your service object and postprocess result to create an appropriate response.
  16. • Diffs • Service • Controller Usage Mutable/ Immutable Using

    instance variables for keeping data Real constants/ Dependency Injection Thread-safe in Puma/Sidekiq Thread-safe by themselves Service.call(data) Mutable Yes Real constants No No
  17. Testing • Easy to setup env test, everything you need

    is to pass input data and check output data • Still depending on real consts
  18. Readability • The business logiс looks clear by itself •

    Easy to see which steps the operation consists of. • Each private method is a black box. You don’t know what it needs to perform correctly.
  19. Extensibility • Easy to reuse the entire operation in other

    parts of the application, for example, CLI, background jobs. • It’s easy to compose/chain with other pieces of an application. Private methods can be separated and moved to another class. • Still depending on real constants forces you to create if/else branches
  20. Service.new.call(data) • Diffs • Service • Controller Usage Mutable/ Immutable

    Using instance variables for keeping data Real constants/ Dependency Injection Thread-safe in Puma/Sidekiq Thread-safe by themselves Service.new(data).c all/ Service.new.call(da ta) Mutable Yes Real constants Yes No Service.new(data).call • Diffs • Service • Controller
  21. Testing • Easy to setup env test, everything you need

    is to pass input data and check output data • Still depending on real consts
  22. Readability • The business logiс looks clear by itself •

    Easy to see which steps the operation consists of. • Each private method is a black box. You don’t know what it needs to perform correctly.
  23. Extensibility • Easy to reuse the entire operation in other

    parts of the application, for example, CLI, background jobs. • It’s easy to compose/chain with other pieces of an application. Private methods can be separated and moved to another class. • Still depending on real constants forces you to create if/else branches
  24. • Diffs • Service • Controller Usage Mutable/ Immutable Using

    instance variables for keeping data Real constants/ Dependency Injection Thread-safe in Puma/Sidekiq Thread-safe by themselves Service.call(data) Immutable No Real constants Yes Yes
  25. Testing • Easy to setup env test, everything you need

    is to pass input data and check output data • Still depending on real consts
  26. Readability • The business logiс looks clear by itself •

    Easy to see which steps the operation consists of. • Each private method knows what we need and returns only value it creates.
  27. Extensibility • Easy to reuse the entire operation in other

    parts of the application, for example, CLI, background jobs. • It’s easy to compose/chain with other pieces of an application. Private methods can be separated and moved to another class. • Still depending on real constants forces you to create if/else branches
  28. • Diffs • Service • Controller Usage Mutable/ Immutable Using

    instance variables for keeping data Real constants/ Dependency Injection Thread-safe in Puma/Sidekiq Thread-safe by themselves Service.call(depe ndencies, data) Immutable No Dependency Injection Yes Yes
  29. Testing • Easy to setup env test, everything you need

    is to pass input data and check output data • We can substitute our dependencies to avoid making HTTP calls, calling real mailers, doing DB queries
  30. Readability • The business logiс looks clear by itself •

    Easy to see which steps the operation consists of. • Each private method knows what we need and returns only value it creates.
  31. Extensibility • Easy to reuse the entire operation in other

    parts of the application, for example, CLI, background jobs. • It’s easy to compose/chain with other pieces of an application. Private methods can be separated and moved to another class. • Now, we need to use another mailer, or fetch data from other source everything we need is to pass another object as a dependency.
  32. But, there is a couple of things we can do

    better • Now, in each controller we need to setup dependencies. But in most cases your dependencies will be the same. • Using raising exception for controlling your business flow is not a good idea. Exceptions, they are for unexpected situation. It forces the Ruby to jmp on call call stack. It’s like a hack. It works. But it was intended for other purpose. • Private methods look ugly. They accept extra params.
  33. But, there is a couple of things we can do

    better • Now, in each controller we need to setup dependencies. But in most cases your dependencies will be the same. • Using raising exception for controlling your business flow is not a good idea. Exceptions, they are for unexpected situation. It forces the Ruby to jmp on call call stack. It’s like a hack. It works. But it was intended for other purpose. • Private methods look ugly. They accept extra params.
  34. But, there is a couple of things we can do

    better • Now, in each controller we need to setup dependencies. But in most cases your dependencies will be the same. • Using raising exception for controlling your business flow is not a good idea. Exceptions, they are for unexpected situation. It forces the Ruby to jmp on call call stack. It’s like a hack. It works. But it was intended for other purpose. • Private methods look ugly. They accept extra params.
  35. Let’s replace the dependencies as params with dependency injection •

    Diffs • Service • Controller Usage Mutable/ Immutable Using instance variables for keeping data Real constants/ Dependency Injection Thread-safe in Puma/Sidekiq Thread-safe by themselves Service.new(depend encies).call(data) Immutable No Dependency Injection Yes Yes
  36. But, there is a couple of things we can do

    better • Now, in each controller we need to setup dependencies. But in most cases your dependencies will be the same. • Using raising exception for controlling your business flow is not a good idea. Exceptions, they are for unexpected situation. It forces the Ruby to jmp on call call stack. It’s like a hack. It works. But it was intended for other purpose. • Private methods look ugly. They accept extra params.