I like my services to define their own inner exceptions like this:
class Service
InvalidState = Class.new(StandardError)
InvalidTransition = Class.new(StandardError)
def call
#...
end
end
So that you easily see what you rescue:
begin
Service.new.call
rescue Service::InvalidState
#...
end
Now, if I what to move Service
to the container, how should the usage look like? I see some possibilites, none being fully satisfying:
-
begin
main_container['service'].new.call
rescue main_container['service']::InvalidState
#...
end
you need to initialize the service outside the cointainer
-
begin
main_container['service'].call
rescue Service::InvalidState
#...
end
you hardcode the service’s name, therefore lose the advantage of containers
-
begin
main_container['service']
rescue main_container['errors.invalidstate']
#...
end
you lose the composition
Are there any good practises or advice?
If I have a service with several implementations, I define interface as a module or abstract class, and expect each of concrete implementations must implement this interface. Thus, public interface consist of method signatures and exceptions.
For example, thats how interface for sending SMS’s may looks like:
# @abstract subclass and define +send_sms+ method
module SmsDispatcher
DeliveryError = Class.new(StandardError)
TimeoutError = Class.new(DeliveryError)
ExternalServiceError = Class.new(DeliveryError)
# @param msisdn [String] with country code
# @param message [String]
# @return [String, nil] message id in external system
# @raise [TimeoutError] when recoverable error happens
# @raise [ExternalServiceError] when external service failed to execute request
# @examlpe
# dispatcher = SMPPDispatcher.new(config)
# dispatcher.send_sms('+1777777777', 'Hi, how are you?')
#
def send_sms(msisdn, message)
end
end
So, I don’t depend on unknown service. I depend on SmsDispatcher
interface. To emphasise this, you may register dependency using the name of the interface. Like this:
register('SmsDispatcher') { SMPPDispatcher.new }