Represent All Concepts
Let's continue our exploration of how the MicroObjects technical practices support the principle of representing all concepts that exist in the code as objects in the code.
In the first post we covered the same practices my hour long presentation covers.
- No Getters / No Setters
- if Only as a Guard Clause
- Isolate Their Code
The last post covered the rest of the practices
- Never null
- No New Inline
- Composition, Not Inheritance
- Be Immutable
- No Primitives
- Extract Cohesion
- No Public Statics
- Never Reflection
Why do we want to do these practices? Why do we want to represent all of the concepts that exist in the code as objects in the code?
Well... Let me share my thoughts
Why Represent All Concepts
OK - There's how my practices tie into representing all of the concepts that exist in the code. But why?
As I mentioned that I put in my email; There's a few well recognized benefits that this brings into the code base.
Near Zero Bugs
This is the biggest value from using these practices to represent the concepts. It forces single responsibility, to the extreme, in such a way that the style doesn't lend itself to complex code. The code becomes simple. Simple code just doesn't have many bugs.
We use Cyclomatic Complexity as an indicator of where bugs hide; 25,000 lines and my average method cyclomatic complexity was almost 1. Meaning that less than 1/2 of my methods had branching.
The methods have A SINGLE FLOW. Not a lot of places for bugs to hide. My tests show it works... it works. There's no edge cases I've missed.
This is a huge part of why my colleague wants to talk about these practices - How can we reduce bugs in our software. The honest response is, "by writing software differently". Which these practices help us do. What I get from applying all these practices is all of the concepts in my code represented as distinct/unique objects.
Fast delivery is great, but if it increases bugs, it's horrible. Near Zero Bugs is first for this reason.
We can deliver software faster because the behaviors we want in in our system are accessible as classes. We don't have to write that behavior around some data - a class represents that behavior which we can then use. This drives to the heart of what code reuse is. We can build up the new functionality through existing concept representations with reduced addition of net new code. We can go faster because we have to write less code.
This is why we can go faster. It's largely accomplished by the application of No Getters. The application of the rest of the practices improves on how little code we actually have to write.
Less code is less opportunity for bugs. Not just less overall code; I won't argue that happens. One of my data points had less code, by about 20% LoC. It's not the overall that I want smaller. It's the method size that I want to see smaller. I'll take larger LoC for smaller methods. The smaller methods give code less places to hide. A 1 line method isn't going to be very complex. It'll do A thing. (I'm ignoring 'obfuscation' or just making it all one line - Don't be terrible).
This one thing the method does can EASILY be tested fully.
It's small, simple, direct, and fully tested. This means there are 'no bugs'. It does it's one thing and does it well.
When our entire application is written using these small methods that have no place for bugs to hide, our entire system has no place for bugs to hide.
We can build complexity from the interactions of simplicity. Simple methods coordinate to deliver complex behaviors.
Now I'll argue that we will have less over all LoC. When we develop complex systems by representing all concepts of that system, we have to write less code. Fundamentally less code has to be written because we never write the same behavior twice. If we implement the same behavior twice; there's a concept we're clearly missing in our code. Give it a representation and it will never be written again.
This process prevents us from having large methods, large classes, complexity. It forces our application to have very little actionable statements.
In the 20,000 lines of code Windows Store app - There were about 7000 executable lines. Visual Studio's code analysis gave me that number, I didn't do it manually. Unfortunately I don't know how to get a similar value out of Java or Swift, so I can't compare that; just raw LoC.
Which means though, of those 20,000 lines; 13,000 were basically boiler plate code. 2/3 of the LoC in the project was non-code. We didn't write comments. It's straight C# bloat.
I really want to do this style in a terse language. :)
You have to write less code, it's writing coordination between objects; actual Object Oriented Programming.
Unless you do obfuscation to get less code, less code lends itself to less complexity. When you don't have to write code to get a behavior, that means the code you're writing doesn't need to know the HOW, just the WHERE. This makes new code less complex. It just knows about objects and contracts, not implementation. Simplicity.
Along the less code path; when we have smaller methods, they are less complex. They have less cognitive load. It's going to be pretty objectively determinable that a method with 1 line is less complex than a method with 10 lines.
Working with these other objects, we don't have to know what they do or how they do it. We can look at the contract and assume they take care of the rest. A lot of development is done in such a way we need to dig into the objects and methods to see how they behave, what arguments they can take, what exceptions they might throw. These practices drive the need for those concerns out of the code. We can trust the objects we interact are well behaved. That they do their very best to handle things for us and allow us to focus on the interactions we need to perform our behavior.
We don't have to know as much about the code to create the complex behaviors; we just use the contracts they implement.