I'm working on ways I can share the practices and mindset that allows me to create MicroObjects.
One of the suggestions I've gotten from a co-worker, John, was to create as simple of a project as possible and show my steps to breaking it into MicroObjects.
FizzBuzz as a project is too complicated. There's a lot that goes on through the course of implementing as MicroObJects. Needs to be SIMPLER.
Some really quick brainstorming and we thought that "adding two numbers" would be a pretty simple example. Really basic, really easy to write the test. It still has just enough behavior to drive a few of the practices and foundations of creating MicroObjects.
This is the start of a series for how I can share MicroObjects - Teach Yourself. I'll be creating projects and doing step by step commits of the project while maintaining passing test(s).
The blogs for each of these will be written after, so won't be as exhaustive as a lot of the follow-along style posts I've done. The point isn't for me to tell you everything to do, but for you to try it and see how it matches the final result of what I've produced. Mine isn't "right", it's just what I get when I apply the practices.
The first one is "Add Two Ints".
Add Two Ints
After getting the project and solution set up. We get the Initial Implementation w/passing test
We're taking the simple approach of gettign functionality in place. The test drives the implementation. We're not demonstrating TDD by the book.
With this, we can now start to refactor and microObjectify the code.
Extracting the addition behavior into a class.
The very brute force implementation is to use a method. MicroObjects is all about objects, we want our functionality extracted into an object.
This isn't the final form of our object, but it starts us down the path.
Once we start to create classes we need to be sure we Interface Everything.
One of the practices of MicroObjects is to give everything an interface. There are some exceptions, and we'll see one soon, but the preferred form is to have an interface on everything. This is how we define the behavior contract.
Introduction of Integer Base Class
This is one of the very few examples of a base class without an interface.
The reasons behind this can be found on quinngil.com and the Fluent Types. For now the key points are - This prevents encapsulation violation by requiring a hard cast. No easy to miss method calls.
Sometimes we need the data out, for 3rd party or for sending outside our code. This stucture of an implicit cast allows us to do so without providing methods to violate encapsulation. Using this style will make it very obvious that a primitive is being extracted because there's a hard cast to that primitive type.
Introducing our own representation of "base" types is not MicroObjects, but it simplifies a lot of the implementation of the practices. So much so that I find it critical to effective MicroObjects.
I've talked about these in a previous blog post: Fluent Types.
Once we have the base fluent type, we need to create a way to encapsulate raw data, so we've Added IntergerOf
IntegerOf as a class is how we wrap primitive data values into an object. This allows us to use objects in our code and not pass around data structures.
We are doing object oriented programming, not data structure programming. We remove the data storage aspect by encapsulating the data into a class with controlable behavior.
Other reasons to not use the data structure - God Class. Does MANY things.
Sum is an Integer instead of a custom type.
The Sum class represents an integer. We'll refactor this to extend from Integer instead of implementing ISum. This refactor aligns it with the concept of what it is representing.
This also means that we no longer can, or need to, invoke the Value method in our Act section. We have the implicit conversion that will make it an int.
Our test gets the raw value out so it can compare. This is a behavior. Let's encapsulate that behavior in a new class.
We want to see if something is true or not, which requires a new type, Bool.
Our IntegerEquality class will extend Bool and we will no longer have our test extracting the raw value. The IntegerEquality becomes the only place where the raw values are interacted with. This is how we drive our encapsulation to the extreme. Raw Data is dealt with in very few classes in very limited ways.
Moving Functionality to be on Fluent Type
I call these base types FluentTypes as all of the behavior of the type moves into the base type class.
Instead of newing up an IntegerEquality, we can now call IsEqual on our subject and compare it to our expected. This allows us much smoother and simpler interaction with the types.
Move Add into Integer
Much like the IsEqual, the behavior of adding two integers can be moved onto the Integer class. This allows us to then just Add integers instead of newing up a Sum class.
Moving classes into the file system
File layout is something that needs to be as heavily refactored as the code itself to stay in sync and continue to make sense.
Here the Interger and Bool files have been moved into a Library folder and type specific folder.
While IntegerEquality is a Bool type, it has a stronger association with, and is returned from a method of, the Integer class. It belongs with the other Interger functionality.
Sealed or Abstract
There's a couple commits that deal with sealing classes that weren't. All classes need to be either
sealed. I've not encountered a valid exception yet.
Enhancing SRP of Fluent Type
After adding the Into method onto the Integer. It's clear this is a behavior. It's
going to be less clear to extract this into it's own object so we leave the method as part
of Integer, but need to assign the behavior into it's own interface. This allows us to
write into anything w/o knowing it's specific type.
That's all there is to microObjecting the addition of two integers!