Fluxus [ˈfluːk.sus] is a simple way to bring use cases to your code. The library uses what Ruby can provide and a little taste of pure object-oriented concepts.
This library takes inspiration from the Clean Architecture concepts of use cases.
You can add it to your project as a dependency:
gem 'fluxus'
An use case is a set of business instructions that will be carefully followed by the runtime code and its dependencies to achieve a great purpose. The level of complexity can vary, but Fluxus is here to create readability and scope around it!
Fluxus always tries to deliver more from an object-oriented paradigm than from showing itself. This means Fluxus is more about directions than dependency. And that's why we call our definition classes as Fluxus::Object
and Fluxus::SafeObject
.
class IsEven < Fluxus::Object
def call!(number)
return Failure(result: "#{number} is odd") if number.odd?
Success(result: "#{number} is even")
end
end
IsEven.call!(2)
class IsEven < Fluxus::SafeObject
def call!(number)
return Failure(result: "#{number} is odd") if number.odd?
Success(result: "#{number} is even")
end
end
IsEven.call!(2)
Object
and SafeObject
While Fluxus::Object
preserves the actual Ruby code autonomy to dictate the flow breakers like error management. SafeObject
stays in the middle and defines safe positions by respecting the Fluxus
contract and delivering the Fluxus::Results::Result
interface.
This approach could be useful for some error recovery systems. Since the application will always recover from it by using the error as a dependency not a runtime flow control.
Fluxus::Results
The expected Fluxus
contract expects a Fluxus::Results::Result
interface compatible as a return value.
While Object
(and SafeObject
) are responsible for delivering a place to hold the actual logic and control this code runtime. The Results::Result
contract will be responsible for delivering back the necessary hooks and their processed data to the application.
We natively support two kinds of Fluxus::Results::Result
the Success
and Failure
. Using this idiom is closer to the natural conception of use cases, when they fail or are successful.
Success(result: true)
Success(type: :shinning, result: true)
Failure(result: false)
Failure(type: :dusty, result: false)
Fluxus::Results::Result
contractAll results share the same (abstract) ancestor definitions which mean they are interchangeable. This brings more coherence in your code when you are handling the most crucial part of the flow, their results.
The Success
and Failure
public contracts could be defined by
success_object = Success(type: :ok, result: 1+1)
success_object.success? # => true
success_object.failure? # => false
success_object.unknown? # => false
success_object.type # => :ok
success_object.data # => 2
failure_object = Failure(type: :fail, result: 1-1)
failure_object.success? # => false
failure_object.failure? # => true
failure_object.unknown? # => false
failure_object.type # => :fail
failure_object.data # => 0
result
and data
relationshipYou already noticed the Success
and Failure
receiving result:
but getting the data from data
. In fact, inside the Fluxus::Results::Result,
the actual contract handles data
directly. But result
is a wrapper defined by Fluxus
to bring more meaning inside the use case concept.
type
importanceThe type
is a way to categorize the major events inside the use case. Your use case can hold a variety of Success
and Failure
and depend on how the business is defined different paths are taken.
The Fluxus
also defines a default value to bring simplicity to small use cases. The default value is :ok
for Success
and :error
for Failure
.
Prefer using symbols
for handling those types.
Was described that Fluxus::Results::Result
are responsible for handling the (obviously) result. This means, this also needs to control the post-use case without leaking data to the runtime void.
With this in mind, the Result implements chainable methods. You can call them hooks if you wish.
Each hook represents one of the expected states.
Fluxus::Results::Result#on_success
Fluxus::Results::Result#on_failure
Fluxus::Results::Result#on_exception
The on_exception
is a contract for SafeObject
, but they are basically a Failure
with a specialized eye for Exception
looking.
All hooks are essentially blocks, and data
is available there. This means, you can define the code routine that will handle the data
, and data
is immutable. Each hook handles its own version of data
, and this belongs there.
self
All hook implementation preserves the Success
or Failure
instance. And this brings a powerful feature to your pipeline. Use cases can chain various conclusions.
Using the IsEven
simple use case, let's implement a fully covered Fluxus::Object
, so you can fully understand how to build your own use cases.
class IsEven < Fluxus::Object
def call!(number)
return Failure(result: "#{number} is odd") if number.odd?
Success(result: "#{number} is even")
end
end
def my_use_case(number)
IsEven
.call!(number)
.on_success { |data| puts data << '!' }
.on_failure { |data| puts 'Why? ' << data }
end
my_use_case(2) #=> 2 is even!
my_use_case(3) #=> Why? 3 is odd
my_use_case(nil) #=> NoMethodError
class IsEven < Fluxus::Object
def call!(number)
return Failure(type: :zero, result: 'you got zero') if number.zero?
return Failure(type: :odd, result: "#{number} is odd") if number.odd?
Success(type: :even, result: "#{number} is even")
end
end
def my_use_case(number)
IsEven
.call!(number)
.on_success(:even) { |data| p data << '!' }
.on_failure(:odd) { |data| p 'Why? ' << data }
.on_failure(:zero) { |data| p data }
end
my_use_case(0) #=> you got zero
my_use_case(2) #=> 2 is even!
my_use_case(3) #=> Why? 3 is odd
my_use_case(nil) #=> NoMethodError
class IsEven < Fluxus::SafeObject
def call!(number)
return Failure(type: :zero, result: 'you got zero') if number.zero?
return Failure(type: :odd, result: "#{number} is odd") if number.odd?
Success(type: :even, result: "#{number} is even")
end
end
def my_use_case(number)
IsEven
.call!(number)
.on_success(:even) { |data| p data << '!' }
.on_failure(:odd) { |data| p 'Why? ' << data }
.on_failure(:zero) { |data| p data }
.on_exception(NoMethodError) { |data| p }
end
my_use_case(0) #=> you got zero
my_use_case(2) #=> 2 is even!
my_use_case(3) #=> Why? 3 is odd
my_use_case(nil) #=> Failure with exception: data
Bug reports and pull requests are welcome on GitHub at https://github.com/Rynaro/fluxus.
The gem is available as open source under the terms of the MIT License.