When projects grow they become hard to change. One aspect that is not often highlighted is dependency direction. I haven’t found much material on the topic, maybe the best ideas came from this talk by Sandi Metz “Less, the path to a better design”.
Some of the main points of Sandi’s talk
The purpose of design is to reduce the cost of change, anything else is not design.
Managing dependencies is at the heart of design.
According to the Stable Dependencies Principle
[The dependency] should be in the direction of the stability.
“Stable” roughly means “hard to change”
But then:
if you don’t know what types of changes are likely, it is best to wait and see what happens as the system evolves.
Sandi’s main point in her talk is that dependency direction is a choice, and:
[17:55] Uncertainty is not a license to guess, it’s a directive to decouple.
And the last pill of wisdom:
Don’t guess what changes will come, guess what will change.
Which, quickly explained here, is about applying the open / closed principle when the code you’re writing might change.
Every class used in your application can be ranked along a scale of how likely it is to undergo a change relative to all other classes.
- Sandi Metz POODR, Chapter 3, pg 54
My suggestions to choosing navigability
The class diagram of the app can express navigability with the slim arrow (->
). The navigability determines the dependency direction. When in doubt about a dependency direction, we can follow the class diagram.
- If
Post belong_to User
,User owns Post
, the navigability isPost -> User
and you should consider favouring depending onUser
inPost
, rather than the other way around; - Ask yourself: “Can
Post
exist withoutUser
?” (and vice-versa);User
makes sense even withoutPost
, but it’s unlikely that aPost
can exist without aUser
, so the navigability should bePost -> User
; - Avoid
User <-> Post
, if you do it you will be unable to useUser
without aPost
and vice-versa; - Classes with many associations should not hold methods about them; Failing to do so will break the SRP;
- Divide your application into modules, and apply strict dependency direction between modules; E.g.:
Reports -> Users
means strictly no methods likeuser.daily_report
; - Add the dependency to the lower level object, so that the parent stays clean. This spreads the logic more evenly in classes who are usually more specific about the logic being added.
An example
######################
# Less stable solution
class Controller
def action
purchase.cost
end
end
class Purchase
has_many :line_items, inverse_of: :purchase
# `cost` is an external dependency
def cost
line_items.sum(:cost)
end
end
class LineItem
belongs_to :purchase, inverse_of: :line_items
end
######################
# More stable solution
class Controller
def action
LineItem.total_cost_of(purchase)
end
end
class Purchase
has_many :line_items, inverse_of: :purchase
end
class LineItem
belongs_to :purchase, inverse_of: :line_items
# Only dealing with internal dependencies
def self.total_cost_of(purchase)
where(purchase: purchase).sum(:cost)
end
end
Most projects will have two god classes: User and whatever the focus happens to be for that application. In a blog application, it will be User and Post. – Thoughtbot, How much should I refactor
Instead of having a class User
that knows about a bunch of unrelated concepts like posts, notifications, friends etc, you can easily picture a small User
class that other resources depend on.
Conclusion
Either you do or don’t agree with this idea, I hope we all agree that choosing the dependency direction is an important factor to improve an app maintainability.
Dependency direction is a choice, and whether you noticed it or not, you just made one
- Sandi Metz
This post is overlooking dependency injection, interfaces stable dependency principle on purpose.