Consider the following conversation:
* at le Starbucks *
Friend: “Hey Hudson! What are you up to today?”
Me: “Not much, it’s an easy day today. Just working on a simple web app. I’ll probably grab lunch with some friends this afternoon and read a bit this evening. What about you?”
Friend: “Sounds fun. A few friends and I are going to see <recently released movie> tonight; do you want to come?”
Me: “Sure, I’ve been wanting to go see it.”
Friend: “Great! I’ll text you the details later.”
Ignoring the fact that I dislike staring at screens outside of writing / programming, that’s a conceivable conversation. Now consider this one:
* at le Starbucks *
Friend: “Hey Hudson! What are you up to today?”
Me: “Quite a lot. I got out of bed at 4:23am this morning, cooked breakfast and showered, realized I needed gas, drove to the gas station, swiped my debit card and entered my pin, filled up the gas tank, and went to work. Now I’m taking a break from editing line 105 ofsearch-form.component.ts
on my current project. I’ll probably grab lunch with some friends this afternoon, then go home and read with a cup of decaf coffee made in my French press.
Friend: “Sounds fun. A few friends and I are driving to the theater to purchase tickets for <recently released movie>, find seats in the theater in which it’s playing, then fixate our eyes on the screen therein until it ends; do you want to come?”
Me: “Sure, I’ve been wanting to go to the theater and watch it.”
Friend: “Great! I’ll send you the details via SMS later.”
Imagine if every conversation were like that (the horror…)
The first interaction is easy to follow because it stays at the same level of abstraction. Work, eat lunch, read, see a movie – these are high-level, simple statements about what my friend and I are doing. The second interaction is hard to follow because it uses several different levels of abstraction. 4:23am and using SMS are very low-level, specific details about how I did something; getting lunch and mentioning a few friends are descriptive and don’t involve details.
Our code should look like the first conversation and stay at a consistent level of abstraction. Instead, it often looks like the second. It forces the reader to context switch constantly and makes understanding the code needlessly difficult.
To help you refactor the “conversation 2 kind of code, here are 3 levels of abstraction that I’ve been using recently.
1. The primitive level
Ints, strings, booleans, pointers, etc. Functions and methods that operate on primitive types, properties, or class members are very low-level. Put another way, these functions should do only the “grunt work of the application. Working at this level can get a little dull because it’s usually easy – that’s probably a sign that you’re doing it right.
2. The problem space level
This depends on what you’re building. In general, the problem space level operates on objects in the realm of your client. For instance, if you’re building a library application, the problem space level might contain Book
s, Patron
s, ConferenceRoom
s, and Computer
s. If you’re building a course registration system, the problem space level might contain Course
s, Professor
s, and Classroom
s. This is probably the lowest level of abstraction you’ll use when talking to your client.
The problem space level should consist of classes that simply group related values and functions. Classes shouldn’t need to talk to each other very much; that’s what the component level is for.
3. The component level
Components operate on items from the problem space level. To use the library example again, a CheckoutComponent
might create a Checkout
record from a Patron
and a Book
. To use the course registration example, a SignUpForm
might allow a Student
to sign up for a Course
. In general, the component level does the interesting work that makes you money.
Conclusion
I realize this will be an almost childish review for some people, but thinking in these three concrete levels has helped me tremendously lately.
Are there additional levels of abstraction you tend to use? Different abstraction approaches altogether? Let me know.
Top comments (5)
Is this separation also somehow visible in your code(/-structure)? Or how do you communicate about those levels in the team?
I'm asking this because I'm having a hard time wrapping my head around how to apply this to a big react/redux project with lots of abstraction levels.
I would like it to be visible -- a lot of my work recently has been refactoring to make this structure more clear.
I'm not sure what the default structure is for a React app (I work primarily in Angular), but my
src/app/
directory looks something like this:Hope that's helpful!
ASP.Net MVC also has a directory structure that reflects the architecture. Like:
I've grown to dislike that approach since things get cluttered easily. For every 'topic' in your application you'll spread files among those folders.
Since you're storing your project on a filesystem, you get 1 hierarchy to choose.
If you go for the architectural divide. It helps you to familiarize with the architecture. And it looks neat with the folders collapsed.
If you go for the topic divide, you'll have all related files together whenever you work on them. That's pretty convenient.
Let's try and demonstrate this with a made up project folder:
Where
.xyz
is your language of choice.[disclaimer]
Maybe I'm comparing apples with pears... Bit unsure if I know enough about React to tell.
[/disclaimer]
While not being familiar with angular, it looks like this nicely aligns with the project structure.
I guess it would need some naming convention or documentation/knowledge sharing to "ensure" this.
Maybe somebody else can suggest something?
If you're not already familiar with it you might be interested in Domain Driven Design. Your ideas seem to resonate with it.
And kudos for the 'conversation comparison' example. I might refer back to it sometime :)