- All right, now that we have that concrete implementation,
let's go ahead and look at how we can decouple this code
from change.
Now, one of the very first things we want to look at
is the lower level API.
The lower level API was this, it was pull and store.
What we need to do is decouple pull from the concrete.
Nothing that pull knows about
should be based on the concrete.
It has to be based on what?
Decoupling our interface.
To understand what interface we're supposed to use,
I don't have to guess.
I already have an implementation of the lower level API.
Remember, I've implemented Xenia,
and Xenia knows how to pull.
I've implemented Pillar and Pillar knows how to store.
Look at this, my concrete implementation has provided us
the behavior that we're going to use
to decouple us from the concrete.
This is why I don't want you to start
with the behavior or the interfaces,
we start with the concrete implementation.
Let's go ahead and lay in those interfaces now.
I'm going to come in here
and I'm going to add two interfaces.
There it is, the Puller interface that knows how to pull,
and the Storer interface that knows how to store.
I'm not guessing, I already have a strong implementation
of pull and store, we've worked this out,
we've got a working program in production.
We know these APIs are good.
We also know everything was fast enough.
I didn't have to worry about my primitive layer
being too slow, remember, it was fast enough, life is good.
Now we can go ahead and decouple.
Let's go into our lower level API,
replace Xenia with Puller,
and now we can use the Puller interface
to execute the behavior.
We have the highest level of decoupling
we can achieve in Go, it is thin, and it is precise.
We're asking for any piece of concrete data,
any value or any pointer that knows how to pull,
and we do it again with store.
Give me any piece of concrete data, any value,
any pointer that knows how to store,
and we do that through the API.
Look, all I've done is replaced my lower level API
with a layer of decoupling, and guess what?
Boom, now I can create other concrete types
for other systems and that lower level API
can work with those other systems.
Now, we're still not done yet.
We've got to decouple all the way through main,
and the only code you should have to change
is main before Copy.
Copy right now is working with the concrete type System.
It would be nice to also decouple Copy,
so it could work with any System
that knows how to pull and store, right?
Let's go ahead and now go ahead
and we'll add a third interface.
What's cool about this is,
we can also leverage composition at the interface level.
Look at that, that is the PullStorer interface,
and it's a very common naming convention,
it's very idiomatic in Go to,
when we compose two other interfaces,
to use that PullStorer, kind of,
we see it with read closures and write closures,
and here we are, the PullStorer.
Now, through composition embedding,
now, any piece of concrete data
that knows how to pull and store is also a PullStorer.
Isn't System already a PullStorer
thanks to the composition of Xenia and Pillar?
It absolutely is.
Our concrete type system already knows
how to pull and store.
Look at what we're able to do.
We're able to now pull System out of the API,
another refactor, say PullStorer,
System already implements the PullStorer interface,
which now means that in main,
and when I pass a copy of the System pointer,
it already implements PullStorer, and this code now,
at least from the lower level and the higher level API,
is completely decoupled from change.
But there's something very interesting in Copy
that might concern you.
It concerns me initially.
I want you to look at the calls to pull and store.
You may not realize this, but we're passing the PS variable
into pull, and the PS variable into store.
This should take some alarm.
Whoa, whoa, whoa, whoa, whoa, whoa, Bill.
Pull isn't looking for a PullStorer.
Pull's looking for a puller, store's looking for a storer,
PS is not a puller or a storer.
Why is this code compiling?
Well, let's do the following.
Let's walk through this code
and I'll explain to you why this code's able to compile.
We start with our system, here it is.
Remember, our system is the embedding of Xenia and Pillar.
We know Xenia knows how to pull, Pillar knows how to store,
so our system knows how to pull,
and our system knows how to store.
Right, we're getting all of this
through the inter type promotion.
Now, we take the address of System on the call to Copy,
and when we get to Copy,
remember Copy's completely decoupled,
it has the PullStorer interface,
remember, interface values are valueless,
interface types are valueless.
They're saying, give me any concrete type, any value,
any pointer, any piece of concrete data, sorry,
any value, any pointer
that implements the PullStorer interface.
We know System does.
Now, what happens is, we get our PS,
this says I got a pointer to a system,
and we're storing, through pointer semantics,
our system value.
Great, now, here's the interesting part.
We're able to pass PS into pull and store,
because, remember, pull is not asking for a value
of type puller.
It does not exist, there are no such things as type puller.
It's saying, give me any piece of concrete data
that knows how to pull.
Store is not asking for a storer,
any concrete piece of data that knows how to store.
Here's the cool part.
We're now passing PS to pull and PS to store
because they don't exist, right.
PS doesn't exist, get this in your head.
PS is valueless, it doesn't exist.
What exists?
The concrete data.
What we're really passing to pull is not PS.
We're passing what is stored inside of PS,
which is the concrete data.
Which means that when we call pull,
what we're able to do is pass the concrete data through.
Look at what we've done.
Now, the memory models are the same,
but what's more important is, is I'm not passing PS,
I'm passing the System address.
Boom, and the same thing with Storer.
I'm not passing PS, I'm passing the concrete data
that was stored inside.
Oh, so get this in your head.
We're always moving and copying concrete data,
whether that data is directly copied by you
or it's being copied indirectly from the interface,
the valueless interface, right, yeah, yeah.
I mean, this interface now has a value,
thanks to the storage of this concrete type.
We're always moving the concrete type across the boundaries,
and in this case,
the data that we're moving is the address of System.
That's why we're able to do this.
Now we could call this implicit interface conversion,
if you want, but I'd rather really focus on the idea
that it's the concrete data that's being moved around,
not the interface value.
Again, the interface value is valueless.
Now, we're not done yet, because I already said
that we're going to have to decouple
all the way through main.
Then when our APIs are decoupled here, the reality is that,
if I want to use Alice or Bob, I can't right now.
We have this type, a concrete type called System.
System is the embedding of Xenia and Pillar.
If I want to use Bob or Alice,
I either have to break System, which is not good,
or maybe what I can do is to find another system type,
maybe I can do this.
Right?
But think about this for a second.
If I do this, if this is not scalable,
System 2 is a PullStorer, I mean, this will work.
But what happens for all the different combinations
of Xenia, Pillar, Bob, Alice, all of that?
This isn't scalable,
this is really not the direction we want to go in.
That being said, how can we solve this problem then?
Well, let me ask you a question.
If System wasn't the embedding of Xenia and Pillar,
but the embedding of our two interface,
if I can type this right, interface types.
Whoa, what does this mean?
Think about it.
I have just defined a concrete type
where I can inject other concrete data
based on that data's behavior.
Now, all I need is one system type, because the system,
we can inject concrete data inside of it.
Look, so how do we take this now and make these changes?
Well, let's go in here and say this,
a System now says Puller, and this is now Storer,
and we've got to use our pointer semantics
because we've implemented the interfaces
with pointer semantics.
Now look at what we've done.
We're injecting Xenia, we're injecting Pillar.
Later on, we can inject Bob if we want.
We can do that all through this one concrete type.
Which means we can still pass System through
and everything should work.
Let's see, if I did, there it is.
Look at that.
We're now able to inject data into our concrete type
and we have a full layer of decoupling,
we can define a system to be almost anything
as long as the other concrete data
implements the right behavior.
Now, it's very important to me,
after we get a piece of code working,
that we do a code readability review.
Code readability reviews are critical.
There's really two types of code reviews
you should be doing.
There is the readability review,
and you should be doing this
every single time you get a piece of code working.
The readability review there is to make sure
that we're consistent with our variables,
with our API designs, with our semantics.
These things must be in place, right?
We want to make sure that our mental models
of our code is always in place.
Treat the readability review as a way
of validating our mental models of every function,
every method, the location of things, the namings of things.
Then the next review after that is going to be
more of our technical review.
There we could also look at API design,
we could look at algorithm efficiencies,
we can look at things that maybe we want to improve.
But the code readability review is critical.
When I start doing a code readability review,
I realized one thing here.
I'm never going to have more than one System type.
The System type, thanks to the embedding of the interface,
is already fully decoupled.
Since I'm not going to have more than one System type,
this interface is now pollution.
Look, I can go back and work in the concrete,
because I'm never going to have a second implementation
of PullStorer.
Having it there is a waste of time.
I can now go back and use our concrete type,
which means that I can go ahead and remove code.
Oh, it's a great day when you can do something like this.
There it is, I can remove this code.
I change my higher level API back to the concrete type,
passing System.
This code works and I'm still at the highest level
of decoupling that I can achieve in this program
because of the data injection.
Now, I do another code readability review,
and I realize something else.
This idea of data injection is super cool.
But guess what?
It's clever.
Because this API isn't as precise as it could be.
I mean, honestly, wouldn't this be a more precise API?
Look at this.
Think about this for a second.
Wouldn't this be a more precise API?
I mean, System is really hiding the cost of initialization.
We don't really know what a System is when we look at Copy.
We don't realize we need these two behaviors,
at least somebody new to the code base.
But now with this new API definition
we've reduced fraud, we're going to make this API
easier to use, easier to test, because it's exactly saying,
give me concrete data that implements Puller,
give me concrete data that implements Storer.
We know exactly what Copy needs and what it does
and much more than when we use this idea of a system.
Again, I want to be as precise as we can be.
Now, if we replace our API design to use puller and storer,
now what we're going to do is pass our Puller,
pass our Storer, that will fix that problem.
Even main cleans up,
because if you really think about it now,
we can go back and just construct the concrete type.
The concept of the interface is completely
gone from initialization, let's clean this up,
let's get rid of this.
We're going to use the special Go func to clean that up.
Now what we're going to do is share Xenia
and share Pillar with Copy,
and even our main program here has cleaned up tremendously
because we're focused, again, on what's important,
which is what?
The concrete.
Right here, during initialization,
we focus on the concrete data,
Xenia and Pillar are the problem.
We pass Xenia and Pillar into Copy, a higher level API,
our higher API is already decoupled now.
Give me anything that implements Puller,
anything that implements Storer, and we now have decoupling
throughout the entire application.
We've got an API now that really is going to limit
and almost reduce all misuse and fraud for this software.
Notice that this was a refactoring,
and I don't want you to be afraid of refactoring.
If you're not refactoring, you're not improving.
If you're not refactoring,
I think you're doing things wrong.
Look at the approach we took here,
layering the API, validating every layer was testable,
and we're thinking about tests from a data point of view,
not from a decoupling point of view.
I don't want hear, "Oh Bill, we've got to write tests,
"and we've got to decouple code,
"because we got to write tests.
"Let's throw an interface at it."
No, no, no, no, no, no, no, no, no.
Layering, testing through data,
building on top of each other,
implementing a concrete solution first,
and then asking, what needs to be decoupled
and decoupling the program
through more layers of refactoring
and then doing code readability reviews
to tighten up these APIs and asking ourselves,
where is there potential fraud and misuse,
where can we be more precise
that we know the problem a little bit better
and then putting those changes in.