Leaking domain objects
Let's begin with defining a simple command object that will interact with your domain. We'll do a good and a bad example right at the beginning to compare them.
#### BAD ####
module Communications
class SendEmailNotification
def initialize(email:, notification_id:)
raise WrongEmailFormat unless email.is_a?(Communications::Email)
@email = email
@notification_id = notification_id
end
end
end
#### GOOD ####
module Communications
class SendEmailNotification
def initialize(email:, notification_id:)
@email = Email.new(email)
@notification_id = notification_id
end
end
end
At first, the difference might look insignificant. The only thing different is that the first command validates whether the object passed through email
parameter is an instance of Communications::Email
- an internal Value Object of the Communications
module that represents an email address. While this looks innocent, and may even look better at first (because we validate whether the passed parameter is really an email) it brings maintenance complexities.
The biggest problem it introduces here is that the external users of the Communications
module will be forced to reach into the Communications
module and build this Value Object before they can use the command. They will have to rummage around your domain and to use its internal objects. This will make it harder for you to introduce changes as you fit to the Email
class. You'll either have to introduce versioning or deprecation, and this will quickly become a nuisance, especially in new domains where you still discover things, you change your objects constantly to represent the business concepts etc. Commands should be more stable and not so much prone to how you handle some business behavior internally.
So! The main lesson here is that it's better to use primitives or some objects specifically created for passing them into the command instead of using the internal objects representing your domain.