I've been thinking about the affordances of programming languages.
A Little Background
In my previous post, Breaking Up the Behemoth, I posited an explanation for why OO apps so often evolve a few, disproportionally large, unmaintainable, condition-filled classes. Unfortunately, that post didn't offer a cure for this problem; it just gave the less-than-helpful advice that you avoid that situation.
This post continues to explore the problem of classes that get too large. My hope is by that learning to recognize the imminent appearance of the big-class-slippery-slope, you can avoid accidentally sliding down it.
Most of the ideas here are my opinion. Although this post starts out by examining a broad, general idea, I promise that it does, eventually, directly address object-oriented programming. Read on for the general introduction, or skip forward to the OO specifics, as your inclinations dictate.
Consider the Doorknob
So, what are affordances? Merriam-Webster defines affordance as "the qualities or properties of an object that define its possible uses or make clear how it can or should be used." Wikipedia's affordance disambiguation page more concisely suggests "a potential action enabled by an object."
"The affordances of the environment are what it offers the animal, what it provides or furnishes, either for good or ill. The verb to afford is found in the dictionary, the noun affordance is not. I have made it up. I mean by it something that refers to both the environment and the animal in a way that no existing term does. It implies the complementarity of the animal and the environment."
--Gibson (1979, p. 127) 7
(I find myself tickled by his blithe confession that he just plain "made it up". If not for the fact that it would take us on a terrible tangent, I'd pause here and make up a few words myself.)
Here are a few real-world examples to illustrate the meaning of affordance.
The knob in Figure 1 affords grasping by hand, turning to unlatch, and pulling to open. While it clearly offers grasping, turning, and pulling, it supplies no information about whether pulling on the knob will actually open the door. Even though the knob clearly can be pulled, pulling on it might not give you the results you want.
The lever in Figure 2 differs from the knob in that it affords unlatching by pushing up or down by any means, so you can use it even if your hands are occupied or unavailable. I have levers in my house and can highly recommend them. They are way more convenient than round knobs.
Figure 3 is Figure 2 but with an added sign to solve the push-or-pull-to-open-the-door problem. The need for a sign suggests that the design of the hardware fails to offer a complete set of affordances. If the usage were clear from the design, there'd be no need for additional, written directions.
In contrast, here are three other styles of door hardware. The first thing to notice is that these options are just for opening the door, that is, they don't have any responsibility for unlatching.
The reason Figure 5 feels wrong is that it's over-specified. The handgrip suggests pulling so strongly that the sign feels a bit insulting.
The push plate in Figure 6 is what you'd expect to find on the opposite side of the doors shown in Figures 4 and 5. Plates very obviously afford pushing, and so pair naturally with things that want to be pulled.
Different Programming Languages Offer Different Affordances
Just like varying styles of doorknob, different programming languages offer their own unique affordances. Language designers have preconceived ideas about the best way to model reality, and their creations reflect these biases. Thus, programming languages are explicitly designed to "enable" certain kinds of thinking.
I'm talking about something that's deeper than syntax. Languages have points-of-view: they're designed to be used in certain stylized ways. The mere fact that code compiles doesn't mean it's arranged as the language designer intended.
While it's possible to warp most any programming language into use by an alternate way of thinking, working at cross-purposes from your language's intentions is not the most efficient way to write code. Don't roll this rock uphill. If you don't understand your language's affordances, learn them. If your coding inclinations conflict with the designer's biases, yield.
And that brings me to OO, and why big classes evolve.
The Affordances of OO
I believe that OO affords building applications of anthropomorphic, polymorphic, loosely-coupled, role-playing, factory-created objects that communicate by sending messages.
Let's break that down. :-)
Anthropomorphic means "ascribing human characteristics to nonhuman things." This means that we think of our objects as having volition, desires, and agency just as if they were people. We don't write algorithms and arrange them in namespaced classes: instead we create new worlds where things, concepts, and even ideas are virtual beings with whom we can converse.
Polymorphic means "having many forms." Imagine that you (I'm anthorpomorphising here, so by "you" I mean "the application" or "an object in the application") have different classes that provide their own unique responses ("forms") for a common set of messages. Instances of these classes conform to the same API and so play a common "role."
When collaborating with an object that plays a role, you clearly have to know what messages you can send, but you don't need to know anything about what happens "over there" where the message is received. You, as the message sender, know "what" you want, and the role-playing object with whom you are collaborating is responsible for supplying one specific "how".
So, messages provide a level of indirection between what you want to do and how it actually gets done, and polymorphism lets you define alternative ways of doing things.
The next idea to add to your OO mindset is loose-coupling.
An example of tight-coupling is when you know the name of a class with whom you intend to collaborate. This knowledge causes you to depend on that other class name; if it changes, you must change. Even worse, if a new class arises that can play the same role as your current collaborator (i.e. it polymorphically implements that same API), you can't talk to the new one because you're tightly coupled to the original.
The coupling problem is exacerbated when you need to collaborate with a specific player of a role rather than with an instance of a known class. Tight-coupling to a role not only forces you to know many different class names, it also requires that you know how to select the correct one.
Coupling can be loosened by separating the place where objects get created from the place where they get used. For example, instead of creating your own collaborators, someone else could create them and pass them to you. This is called "dependency injection."
Dependency injection is not scary. Think of it as a simple technique that adds a level of indirection so that formerly connected objects can vary independently.
Becoming comfortable with dependency injection requires that you let go of that unseemly desire to know exactly what it is that other objects do. OO asks you to blithely trust others to correctly do their bit. It wants you to strive for ignorance to protect yourself from the consequences of distant changes to other objects.
Of course, in order to have a dependency to inject, someone--somewhere--has to create the right object. If creating the right object requires a conditional, this should happen in a "factory."
Factories? Also not scary. A factory is merely an easily accessible method that knows everything necessary to create the right object for a given situation. Factories allow you to isolate conditionals that would otherwise be duplicated in many places.
Leveraging OO's Affordances to Avoid Big Classes
Here again is what OO affords: anthropomorphic, polymorphic, loosely-coupled, role-playing, factory-created objects that communicate by sending messages.
In my opinion, large, condition-laden classes reveal failures of the OO mindset. The conditionals in these large classes often switch on something that could be thought of as a type. Please note that I'm not asserting that these conditionals actually test against real class names--they often don't. Rather, I'm suggesting that the conditionals exist for a reason, and that many times the reason is a concept or idea that could have been modeled as a real thing within the virtual world of your app. The conditionals about which I'm concerned are those that suggest the existence of model-able abstractions, regardless of whether or not these abstractions have been officially codified into classes.
The OO mindset interprets the bodies of the branches of these "type"-switching conditionals as pleas for you to create objects that polymorphically play a common role.
The OO mindset understands the switching logic of these conditionals to be a petition for you to isolate object creation in a factory.
And the OO mindset regards the mere presence of a type-switching conditional as a heartfelt request that you replace the entire thing with a simple message sent to an injected, factory-created, role-playing object.
This is what OO affords. It wants you to replace your procedural monoliths with collections of small, independent, collaborative objects. The existence of a large, condition-laden class signals that the procedural code has failed you. When you see such an object, it's time to change mindsets.
Public POOD course in May in delightful North Carolina
My next public Practical Object-Oriented Design course will be held in Durham, NC on May 2-4, 2018. Yup, it's time for another POODNC . This is your chance to spend three days with like-minded peers. Join us, and change your mindset about objects.
is licensed under
2 : "Door Handle" by www.trek.today is licensed under CC BY
3 : "pull" by various brennemans is licensed under CC BY-SA
4 : "door push plate" by stu_spivack is licensed under CC BY-SA
5 : "pull" by greenkozi is licensed under CC BY-NC-ND
6 : "door push plate" by stu_spivack is licensed under CC BY-SA
7 : J. J. Gibson (1979). The Ecological Approach to Visual Perception . Houghton Mifflin Harcourt (HMH), Boston.