README.md in simple_ruby_service-1.0.2 vs README.md in simple_ruby_service-1.0.3
- old
+ new
@@ -1,17 +1,17 @@
# Simple Ruby Service
[![Build Status](https://travis-ci.com/amazing-jay/simple_ruby_service.svg?branch=master)](https://travis-ci.com/amazing-jay/simple_ruby_service)
[![Test Coverage](https://codecov.io/gh/amazing-jay/simple_ruby_service/graph/badge.svg)](https://codecov.io/gh/amazing-jay/simple_ruby_service)
-Simple Ruby Service is a lightweight framework for Ruby that makes it easy to create Services and Service Objects (SOs).
+Simple Ruby Service is a lightweight framework for creating Services and Service Objects (SOs) in Ruby.
The framework provides a simple DSL that:
1. Incorporates ActiveModel validations and error handling
2. Encourages a succinct, idiomatic coding style
-3. Ducktypes Service Objects as Procs
+3. Allows Service Objects to ducktype as Procs
## Requirements
* Ruby 1.9.2+
@@ -35,112 +35,146 @@
Source code can be downloaded on GitHub
[github.com/amazing-jay/simple_ruby_service/tree/master](https://github.com/amazing-jay/simple_ruby_service/tree/master)
-### The following examples illustrate how to refactor complex business logic with Simple Ruby Service
+## Quick Start
See [Usage](https://github.com/amazing-jay/simple_ruby_service#usage) & [Creating Simple Ruby Services](https://github.com/amazing-jay/simple_ruby_service#creating-simple-ruby-services) for more information.
+### How to refactor complex business logic with Simple Ruby Service
+
#### ::Before:: Vanilla Rails with a fat controller (a contrived example)
```ruby
# in app/controllers/some_controller.rb
class SomeController < ApplicationController
def show
raise unless params[:id].present?
resource = SomeModel.find(id)
authorize! resource
resource.do_something
value = resource.do_something_related
+ raise unless resource.errors
render value
end
end
```
#### ::After:: Refactored using an SO
```ruby
# in app/controllers/some_controller.rb
class SomeController < ApplicationController
def show
- # NOTE: Simple Ruby Service Objects ducktype as Procs and do not need to be instantiated
- render DoSomething.call(params).value
+ # NOTE: Just one, readable line of code
+ render DoSomething.call!(params)
end
end
+```
+
+#### ::Alternate After:: Refactored using a Service
+```ruby
+# in app/controllers/some_controller.rb
+class SomeController < ApplicationController
+ def show
+ # NOTE: Simple Ruby Service methods can be chained together
+ render SomeService.new(params)
+ .do_something
+ .do_something_related
+ .value
+ end
+end
+```
+### Taking a peek under the hood
+Similar to `ActiveRecord::Base#save!`, `DoSomething.call!(params)`:
+- creates an instance of `DoSomething`
+- initializes `instance.attributes` with `params`
+- raises `SimpleRubyService::Invalid` if `instance.invalid?`
+- sends `instance.call`
+- raises `SimpleRubyService::Failed` if `instance.failed?`
+- returns `instance.value` directly to the caller
+
+
+### Anatomy of a Simple Ruby Service Object
+```ruby
# in app/service_objects/do_something.rb
class DoSomething
include SimpleRubyService::ServiceObject
attribute :id
attr_accessor :resource
- # NOTE: Validations are executed prior to the business logic encapsulated in `perform`
+ # Validations are executed prior to the business logic encapsulated in `perform`
validate do
@resource ||= SomeModel.find(id)
authorize! resource
end
- # NOTE: The return value of `perform` is automatically stored as the SO's `value`
+ # The result of `perform` is automatically stored as the SO's `value`
def perform
- resource.do_something
- resource.do_something_related
+ resource.do_something
+ result = resource.do_something_related
+
+ # Adding any kind of error indicates failure
+ add_errors_from_object resource
+ result
end
end
```
-#### ::Alternate Form:: Refactored using a Service
+### Anatomy of a Simple Ruby Service
```ruby
-# in app/controllers/some_controller.rb
-class SomeController < ApplicationController
- def show
- # NOTE: Simple Ruby Service methods can be chained together
- render SomeService.new(params)
- .do_something
- .do_something_related
- .value
- end
-end
-
# in app/services/do_something.rb
class SomeService
include SimpleRubyService::Service
attribute :id
attr_accessor :resource
- # NOTE: Validations are executed prior to the first service method called
+ # Similar to SOs, validations are executed prior to the first service method called
validate do
@resource ||= SomeModel.find(id)
authorize! @resource
end
+ # Unlike SOs, Services can define an arbitrary number of service methods with arbitrary names
service_methods do
def do_something
- resource.do_something_related
+ resource.do_something
end
- # NOTE: Unlike SOs, `value` must be explicitely set for Service methods
+ # Unlike SOs, `value` must be explicitely set for Service methods
def do_something_related
self.value ||= resource.tap &:do_something_related
+ add_errors_from_object resource
end
end
end
```
+## A special note about Simple Ruby Service Objects, Procs, and Ducktyping
+
+Simple Ruby Service Objects respond to (`#call`) so they can stand in for Procs, i.e.:
+```ruby
+# in app/models/some_model.rb
+class SomeModel < ApplicationRecord
+ validates :some_attribute, if: SomeServiceObject
+ [...]
+```
+_See [To bang!, or not to bang](https://github.com/amazing-jay/simple_ruby_service/tree/master#to-bang-or-not-to-bang) to learn about `.call!` vs. `.call`._
+
## Usage
### Service Objects
Service Object names should begin with a verb and should not include the words `service` or `object`:
- GOOD = `CreateUser`
- BAD = `UserCreator`, `CreateUserServiceObject`, etc.
Also, only one operation should be made public, it should always be named `call`, and it should not accept arguments (except for an optional block).
-_See [To bang!, or not to bang](https://github.com/amazing-jay/simple_ruby_service/tree/master#to-bang-or-not-to-bang) to learn about `.call!` vs. `.call`._
-
#### Short form (_recommended_)
```ruby
result = DoSomething.call!(foo: 'bar')
```
@@ -195,13 +229,10 @@
- GOOD = `UserCreator`
- BAD = `CreateUser`, `UserCreatorService`, etc.
Also, any number of operations may be made public, any of these operations may be named `call`, and any of these operations may accept arguments.
-_See [To bang!, or not to bang](https://github.com/amazing-jay/simple_ruby_service/tree/master#to-bang-or-not-to-bang) to learn about `.service_method_name!` vs. `.service_method_name`._
-
-
#### Short form
_not available for Services_
#### Instance form
@@ -252,11 +283,11 @@
```
## Creating Simple Ruby Services
### Service Objects
-To implement an Simple Ruby Service Object:
+To implement a Simple Ruby Service Object:
1. include `SimpleRubyService::ServiceObject`
2. declare attributes with the `attribute` keyword (class level DSL)
3. declare validations see [Active Record Validations](https://guides.rubyonrails.org/active_record_validations.html)
4. implement the special `perform` method (automatically invoked by `call` wrapper method)
@@ -280,11 +311,11 @@
end
end
```
### Services
-To implement an Simple Ruby Service:
+To implement a Simple Ruby Service:
1. include `SimpleRubyService::Service`
2. declare attributes with the `attribute` keyword (class level DSL)
3. declare validations see [Active Record Validations](https://guides.rubyonrails.org/active_record_validations.html)
4. define operations within a `service_methods` block (each method defined will be wrapped)
@@ -321,13 +352,13 @@
## FAQ
### Why should I use Services & SOs?
-[Click here](https://www.google.com/search?q=service+object+pattern+rails&rlz=1C5CHFA_enUS893US893&oq=service+object+pattern+rails) to learn more about the Services & SO design pattern.
+[LMGTFY](https://www.google.com/search?q=service+object+pattern+rails&rlz=1C5CHFA_enUS893US893&oq=service+object+pattern+rails) to learn more about the Services & SO design pattern.
-**TLDR; fat models and fat controllers are bad! Services and Service Objects help you DRY things up.**
+**TLDR** - Fat models and fat controllers are bad! Services and Service Objects help you DRY things up.
### How is a Service different from an SO?
An SO is just a Service that encapsulates a single operation (i.e. **one, and only one, responsibility**).
@@ -414,8 +445,9 @@
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
## DEVELOPMENT ROADMAP
-1. Create a helper to dynamically generate default SOs for ActiveRecord models (`create`, `update`, and `destroy`) _(when used in a project that includes [ActiveRecord](https://github.com/rails/rails/tree/main/activerecord))_.
-2. Consider isolating validation errors from execution errors (so that invalid? is not always true when failed? is true)
+1. Create a class level DSL to stop before each Service method unless errors.empty?
+2. Create a helper to dynamically generate default SOs for ActiveRecord models (`create`, `update`, and `destroy`) _(when used in a project that includes [ActiveRecord](https://github.com/rails/rails/tree/main/activerecord))_.
+3. Consider isolating validation errors from execution errors (so that invalid? is not always true when failed? is true)