During the Q and A of the Sandi Metz’s last talk from Ruby Conf, she said something interesting:
Polymorphism is fine as long as it’s shallow and narrow and at the edges of your object graph.
I’ve been turning this quote around in my head for a while, especially the phrase “edges of your object graph”. What is an object graph?
A graph is an object with nodes (or vertices), and links that connect those nodes together. Normally, those links are called “edges”, but we’re already using the word “edges”, and I don’t want to complicate the terminology.
So in your app’s world, you have a graph. Your app delegate is probably where it starts. It holds on to the window and the root view controller, which in turn hold on to a bunch of other stuff, and so on. Each of those connection points is one of the “links” in our object graph. static
pointers that hold things like singletons can also be the start of graphs.
But a really good object graph looks more like a tree than a graph. When your object graphs looks like a tree, the data flow is much clearer, and it’s easier to understand. Each object only controls its children, and children can only be changed by themselves or by parents; no other parent from another branch can change them. You might have one big trunk in the middle (ideally, an app coordinator), leading out to smaller branches (child coordinators and child view controllers), and into smaller and smaller components, like your views, models, and the rest of your objects. An aside: this is why coordinators are so valuable: they break up dependencies between your view controllers. The branches of your object tree don’t touch.
Now, what did Sandi mean about the “edges of your object graph”? Let’s not use the word “edge”, since it’s overloaded in the context of graphs. Let’s use the word “boundaries”. She’s talking about the tiniest leaves of our tree. Polymorphic objects are okay by Sandi as long as they are only referred to, instead of doing any referring. And this makes a lot of sense! Polymorphic classes are dangerous because small changes in them ripple to all of their subclasses and users. By making sure your inheritance hierarchies stay small and well managed, you can contain that complexity. When you refer to other objects, you’re more fragile. If you’re only referred to, a stable interface is the only thing you need to keep from breaking anything else.
In addition to your object graph, you also have a dependency graph. Here, your nodes are classes instead of instances, and your links are #imports instead of object ownership. (You can use scripts to analyze your dependency graph.) Recently, I’ve been working on a new extension for the Genius app. And importing even a single model class into the extension project requires a cascading series of imports that eventually sucks most of the app’s classes into the extension. It would have been great to have very clearly defined boundaries, so that I could import a module of code (like, say, the entities, or the authentication module) without dragging a ton of unrelated code along. Like in the object graph, structuring my dependencies like a tree here would have been very helpful.
It makes me think that it would be great to have two types of objects, separated into layers.
One is the “independent” layer: objects in this layer have very few dependencies and just do simple stuff. It’s your business logic, implemented as entities, policies, value objects, etc. It’s also your views and view controllers, since those ideally touch very few other things. But these objects very simple, and they’re easily transferred from project to project. These are the “ pure objects” of the world.
The other is the “dependent” layer: this glues this stuff together at a highly-abstracted level. It doesn’t contain very much code, and the code that it does contain is in the language of the domain. It hooks into the lower level through object ownership, and gets called through blocks, delegates, protocols, and other communication patterns. These are the “coordinators” of the world.
The idea is that you could totally change the coordinators and go from an iPad app to an iPhone app to an extension, without having to change the independent layer at all. You can make classes so small that they and all their friends can fit in your pocket.
It reminds me a lot of an idea that Gary Bernhardt developed (paywall), which is the “functional core”/“imperative shell” split. Andy Matuschak has been pushing for it in our own community. He reveres Swift’s structs because they’re “inert, isolated, and interchangeable”. Having something that can’t change out from under you is very valuable.
It’s also somewhat reminiscent of Uncle Bob’s entity-boundary-control architecture (text). When he writes code for a Rails app, none of his business logic is tied to the framework. Instead, he can move his domain logic to any other framework quite easily, instead of stuffing it all into an ActiveRecord::Base
subclass and being coupled to Rails forever.
When considering the object graph and the dependency graph, fewer links are better. Fewer cycles are better. I’m hoping to write code in the future that minimizes those two things and is all the more portable for it.