Skip to main content

Services with `or` conditionals

Convenient Service shines very brightly when you have and conditional logic.

It is very easy to construct such behavior using the step DSL.

Check out the following service:

class RefreshSubscription
include ::ConvenientService::Standard::Config

attr_reader :user, :subscription_id

step IsSubscriptionOwner,
in: [:user, :subscription_id]

step FindSubscription,
in: :subscription_id,
out: :subscription

step RegenerateSubscriptionTokens
in: :subscription,
out: :tokens

step UploadSubscriptionTokens
in: :subscription,
out: :tokens

def initialize(:user, :subscription_id)
@user = user
@subscription_id = subscription_id
end
end

Let's use the pseudo notation below to describe a sequence of steps from RefreshSubscription as and conditionals:

`IsSubscriptionOwner`
and `FindSubscription`
and `RegenerateSubscriptionTokens`
and `UploadSubscriptionTokens`.

Pretty simple and straightforward.

But, but, but 😢...

What should we do when we have some or conditionals?

For example, two additional requirements are introduced to the one we had before:

  • Super admin can refresh any user subscriptions.

  • A user with a special ability can refresh other user's subscriptions.

  • User can refresh its own subscription (already existing requirement).

In our pseudo notation it will look like this:

`IsSuperAdmin` or `HasAbility`or `IsSubscriptionOwner`
and `FindSubscription`
and `RegenerateSubscriptionTokens`
and `UploadSubscriptionTokens`

It is not the time to give up 😭.

The list of to-do actions is not so long:

  1. Create a new service which name generalizes all or conditions, for instance - IsAuthorizedForSubscriptionRefresh.

Then the updated pseudo notation can be displayed as:

`IsAuthorizedForSubscriptionRefresh`
and `FindSubscription`
and `RegenerateSubscriptionTokens`
and `UploadSubscriptionTokens`

Now, we can utilize all the step DSL s usual.

class RefreshSubscription
# ...

step IsAuthorizedForSubscriptionRefresh,
in: [:user, :subscription_id]

step FindSubscription,
in: :subscription_id,
out: :subscription

step RegenerateSubscriptionTokens
in: :subscription,
out: :tokens

step UploadSubscriptionTokens
in: :subscription,
out: :tokens

# ...
end
  1. Prepare a common template for a service with or conditionals.

Here it is for our particular case:

class IsAuthorizedForSubscriptionRefresh
include ::ConvenientService::Standard::Config

attr_reader :user, :subscription_id

def initialize(:user, :subscription_id)
@user = user
@subscription_id = subscription_id
end

def result
# TODO: Implement
end

private

def is_super_admin_result
@is_super_admin_result ||= IsSuperAdmin.result(user: user)
end

def has_ability_result
@has_ability_result ||= HasAbility.result(user: user, ability: :refresh_someone_else_subscription)
end

def is_subscription_owner_result
@is_subscription_owner_result ||= IsSubscriptionOwner.result(user: user, subscription_id: subscription_id)
end
end
  1. Implement the result method.

Before we move on to the final polished chuck of code, let's discuss the common anti-patterns.

Please, welcome deeply nested if statements and multiple success calls.

Zombie version 🧟​

# bad - zombie
def result
if is_super_admin_result.not_success?
if has_ability_result.not_success?
if is_subscription_owner_result.not_success?
error(
"All conditions are not satisfied:"
+ " and "
+ is_super_admin_result.message
+ " and "
+ has_ability_result.message
+ " and "
+ is_subscription_owner_result.message
)
else
success
end
else
success
end
else
success
end
end

Although, this code is "perfect" from the performance point of view and it works exactly as needed according to the requirements...

Please, do not commit it.

Just imagine what will happen with an additional fourth or fifth condition.

How can we make it simpler and more readable?

Use or_step.

class CreateButton
include ConvenientService::Standard::Config

step CreateWebButtonFactory,
in: :app,
out: :button

or_step CreateAndroidButtonFactory,
in: :app,
out: {action: :button}

or_step CreateIosButtonFactory,
in: :app,
out: {control: :button}

or_step CreateDesktopButtonFactory,
in: :app,
out: {knob: :button}

step :result,
in: :app,
out: :button

attr_reader :app

def initialize(:app)
@app = app
end

def result
success(button: button)
end
end