Improving Your Object Design with Dependency Inversion
Usually when I hear a conversation about Dependency Inversion, one of the SOLID principles of object-oriented design, a question comes up about its name. It’s often summarized as “depend on abstractions” and that makes sense—but why is it called dependency inversion?
This isn’t just trivia. Understanding the “inversion” in “dependency inversion” has the potential to transform your abstractions into powerful design tools.
What are dependencies?
First, a brief refresher on dependencies. Class A depends on class B if a change in B could cause a change in A. There are several reasons changes can propagate, including:
- Association: if an instance of class A sends messages to an instance of class B, class A depends on class B. If class B’s method signatures or behavior change, A may need to change.
- Extension: if class A extends class B, class A depends on class B. Again, if method signatures or behavior in class B change, A may need to change. In this post, the form of extension we’ll discuss is implementing an abstraction. (In your programming language, abstractions might be called “interfaces,” “protocols,” or “duck types”.)
Next, let’s see an example of what it means to change a dependency on a concrete class into a dependency on an abstraction.
Say we have a blog app. We have a business logic class,
CreateBlogPost, that handles all the necessary tasks for when a blog post is created. It delegates to other objects to perform detailed tasks, such as asking an instance of
TwitterService to tweet about the new post:
class CreateBlogPost # initializer, attr_accessors, etc. def run # create the post # do some other steps TwitterService.new.add_status(message, username, password) # more steps end end
CreateBlogPost instance sends a message to a
TwitterService instance. There are a few different ways you can describe this relationship:
CreateBlogPostis a client of
TwitterService—it makes use of functionality
CreateBlogPosthas an association with
To visualize this dependency, it’s helpful to use a class diagram, where association is represented by a V-shaped arrow from the sender to the receiver:
Ideally, we’d also like the option to post notifications to social networks other than Twitter. The core business logic around creating a blog post would be the same, so we would want to reuse
CreateBlogPost instead of duplicating its code. But we can’t if it depends specifically on
TwitterService. The problem stems from the fact that
CreateBlogPost depends on a “concrete” class—a class with a specific implementation. Any time we get a
CreateBlogPost object, we are forced to take a
Adding an Abstraction
When we want to be able to reuse a class but our design prevents it, that’s a design problem—and it’s the problem dependency inversion solves.
To get out of this bind, let’s introduce an abstraction between the two. Let’s call it
SocialMediaService for now:
Extension is represented in a class diagram with a white triangular arrow from the child to the parent.
SocialMediaService is an abstraction, and
TwitterService is a concrete class that implements that abstraction. Now
CreateBlogPost isn’t associated to a
TwitterService per se, but rather to some kind of
SocialMediaService—and that’s all it knows. (Making this work in code often involves passing a
SocialMediaService implementation to
CreateBlogPost when it’s instantiated.)
Take a look at the direction of the arrows. The arrow connecting to
CreateBlogPost still goes left-to-right. So why does the arrow connecting to
TwitterService go right-to-left? Doesn’t the flow of control go from left to right? Yes, but the arrows don’t represent the flow of control; they represent the flow of change.
TwitterService both depend on
SocialMediaService, because if the signature of a method on
SocialMediaService abstraction changed, both of them might need to change. On the other hand, changes to
TwitterService won’t propagate on
SocialMediaService. It is the contract through which they communicate; as long as the contract stays the same,
TwitterService can change all they want.
Is It Inversion?
We’re depending on an abstraction now…but we still haven’t discovered why it’s called “inverting” the dependency. At first the dependency arrow from
TwitterService went from left to right:
After introducing our abstraction, there’s still an arrow that direction, but now there’s also an arrow from right-to-left: from
If we wanted to make it look more like an inversion, we could change how we draw the diagram. Let’s draw
SocialMediaService close to
CreateBlogPost. The longest dependency line points left, so it looks more like an inversion:
But if we draw the abstraction close to
TwitterService we’re right back where we started: the longest dependency line points right!
Does this mean our diagram is totally arbitrary?
No. It’s actually a way to visualize two design alternatives: we can think about the abstraction either as more closely associated with
CreateBlogPost or with
TwitterService. In other words, the abstraction could either be owned by the caller or the implementor.
Our goal is to invert the dependency and make
CreateBlogPost independent of
TwitterService. Our diagram suggests that, to accomplish this,the caller
CreateBlogPost should own the abstraction
SocialMediaService. “The caller should own the abstraction” is more commonly phrased as as “the client owns the interface.” The abstraction should be in the same “package” (namespace, module, or group) as the client, not the implementor:
Drawing the package on the diagram makes it easier to see that the dependency has been inverted:
TwitterService depends on the client package, which includes
Designing for the Role
Just saying the client owns the abstraction doesn’t change anything: we also need to let the client define the abstraction in terms of the role it needs that abstraction to play.
Think about the abstraction’s name:
CreateBlogPost doesn’t care about social media per se; what it cares about is notifying people about a new blog post. That’s the need that should drive the definition of the abstraction, leading to a name like
PostNotifier, and the corresponding implementation
SocialMediaService might receive a message like:
social_media_service.add_status(message, username, password)
A message like this restricts
CreateBlogPost: it can only to adding statuses to social media platforms that take a message, username, and password. But a
PostNotifier is more likely to receive a message like:
The parameter it receives is simply the newly-created blog post itself, and the
PostNotifier takes care of any details about how to send a notification: what account to post it to, what the status message should say, etc. This message design makes the
CreateBlogPost business logic class even more independent of the implementation details of its collaborators.
So we’ve accomplished the goal of making our business logic class more reusable. And we really have inverted the dependency! At first
CreateBlogPost depended on
TwitterPostNotifier depends on the package including
Class diagrams have helped us see that depending on abstractions doesn’t automatically invert the direction of dependencies: you need to make sure the client defines the abstraction. Doing so keeps your business logic classes from being encumbered with implementation details.
Take a look at the abstractions in your code and see if you can refactor them to focus more on the role they play for the client. And maybe even try drawing a class diagram to see if it helps!
If you’d like to know more about Dependency Inversion, take a look at the original Dependency Inversion Principle article, or the original book on the SOLID principles, Agile Software Development: Principles, Patterns, and Practices. And you can learn more about role-based design from Rebecca Wirfs-Brock and Alan McKean in Object Design: Roles, Responsibilities, and Collaborations.