Encapsulation for objects is, IMO, how well the data is hidden. Encapsulation is only acting via behavior from an object. Never getting data and doing something.
A well encapsulated object is also a very cohesive object. These traits tend to enhance each other. µObjects encourage this.
How do µObjects encourage this? As an industry, we're pretty lacking on encapsulating things. At the most basic level; µObjects want a single behavior. If we only have one behavior; pretty damn encapsulated.
µObjects encapsulate implementation details
This isn't about only exposing behavior. This is about hiding how things are done. If we have a User
object, How's the user populated? I don't care. You don't care. No one cares; and no one knows (ok, someone knows)...
The User
class has dependencies that have dependencies that.... until there's a class that knows how to get data. Network, database, disk? Who cares. There's only a limit number of classes that have any idea about that. From there; the data is somehow available for the behavior on the User
.
The emphasis here is that we NEVER ask the DAL for a User
. We may new up a User
with an ID - but that User
knows how to get their data. We don't give them the data.
I've built an entire app that behaves like this. When I want to display the users's name; I simply new up a User
(in a class constructor; one of the only places object instantiation happens). I'll then provide the User
instance a UI control and ask the User
to put the name into it.
The implementation details are completely removed. It's amazing not knowing the HOW when I need to work with an object.
µObjects only expose behavior
Here's where the crux of encapsulation happens. We only interact with the object through behavior. In the example above; I pass a UIElement into the User
. The User
then writes data into the UIElement. There is no way to "ask" the user for it's data.
We don't get to ask for data, look at it, and decide what the object should do. We ask the object. It will make the decision and do it for us.
With µObjects; these decisions are very small. Even working procedurally with encapsulation violation, we're making a decision on A PIECE of data. We then do A THING based off that.
We'll write up code procedural and violating encapsulatino all the time; but it's a huge method (7 lines!!!!). That's something that needs to be broken down. As we break the method into objects; we find way to encapsulate behavior around data into the object that owns the data. We cover our "violation" with behavior.
µObjects are all interface
I've mentioned before that the idea of always program to an interface never made sense until I started µObjects.
We use interfaces to ensure that we only expose behavior. An interface is a quick and easy way to look and identify if we're getting data or working with behavior.
Interfaces are about protecting the object against errant changes. When you change an interface, it can have much broader impacts to the code base - impacts you probably don't want.
Interfaces are the behaviors exposed to the world.
An example of this is our UWP UI pages. Our UI is nearly logic less. Moving towards less and less logic all the time. What our UI does is return controls. Sorta.
It doesn't return a UI control directly; that'd violate Clean Architecture. It returns the UI Element wrapped in a behavior exposing control. If we want to change the visibility; we have an IVisibility
interface.
public interface IVisibility{
void Show();
void Hide();
void ShowIf(bool shouldShow);
}
When we need to adjust visibility; the control is wrapped and returned as this interface. (See Interface Overloading for an early example)
The consumer then ONLY has behavior to act on.
If we want to write to the control - IWriteString
.
public interface IWriteString{
void Write(string value);
}
We can only write into it. We have no other access. We're limiting the behavior to only and exactly what we'll need.
When dealing with µObjects, it's critical that interfaces are also µ. We limit what an object can do to the minimum set we can get away with it having. Cohesion is critical.
µObjects are simple
µObjects are simple. Their simplicity makes the system easy to understand. We can then build complex behavior from simple understandable pieces. There's no code to decipher to figure out what's happening.
With µObjects being simple; it's easy to maintain encapsulation. When there's not a lot of internals to a class; there's not a lot consumers will be trying to get to.
This makes it simpler for us as developers to expose behavior. We don't have to think of all the ways all the data could be used... to give up and just let it out.
A simple object only has a few (at most) pieces of data. Initially there's exposure. Not actual encapsulation violation; because there's no behavior initially. As soon as the data is used for something; then there's behavior. Then it transforms. The data is hidden and a behaviorial method is created.
This is part of the discipline of µObjects - You must be vigilant about hiding all data; and exposing only behaviors.
This is aided by the simplicity of the objects.
µObjects are immutable
The idea object is immutable. That's not a typo; I do mean the large concept of "object". Immutable is a requirement for µObjects. There will be exceptions; but I've only found them on the UI bookend, and that's due to bad UI frameworks.
Immutability grants us an ability to encapsulate that we can't find any other way. If the data of the class can't change; we can limit our behavior for the class. It's a much smaller set of data when immutability is in effect. We can't set values. No one needs to know WHAT the data is. We get to ask questions; there's no exposure of what we have, or how we do the behavior.
Immutable objects simplify the code as well; which simplifies our need for encapsulation. We don't have "valid data" concerns. The data is valid. We don't need to interogate an object for validity; or wait for another thread to be finished changing it... When we have an object; we can confidently interact with it's behavior while remaining ignorant of the rest of the system.
µObjects emerge into domain objects
This was a bit of a surprise to me while working through µObject projects. We don't design domain objects. We start with data (not a data bag, but exposing data). If I have a phone number on a User
; I get back the string. When I need to format this phone number... Suddenly I have behavior. Now PhoneNumber
is an object. It has behavior that I'm asking it to do for me.
This emergent of objects simplifies the code and development as there's nothing being built you don't need.
If you have a list of objects, and you only display it. A list is fine. If you have a list and you need to do something based off the count - That's behavior. Encapsulate the list; hide it; and give the object that behavior.
The refactoring of "primitives" into objects takes a bit of getting used to; but as long as there's discipline around doing so; the code is clean. You'll avoid formatting strings in 1/2 dozen places.
µObjects see domain objects emerge. This ensures only the behavior needed, when needed. We don't see any behavior happening on data that's not encapsulated in the appropriate representation.
µObjects limit change
With µObjects and encapsulation; the impact of change is massively limited. With all behavior encapsulated into an object; and a very small set of behavior; changes touch very few classes. If it's just an internal change (how a phone number is formatted) then a single class is affected. If we change the args for a behavior; only a few classes using the changed class will be impacted.
µObjects allow us to produce small objects used for very specific tasks. This prevents giant objects that can be used for a lot of things, or a lot of ways.
µObjects limit dependencies through relationships
One of the important practices of µObjects is to recognize when arguments or variables are more closely related to each other than the others. This closer relationship needs to be encapsulated in a class.
This prevents a large number of class variables. What a class is dependent on is very specific to that class; and is unable to be broken into other classes.
The limited dependencies allows the limitation on the impact of change. By continually evaluating and refactoring relationships into new objects; we keep the number of reasons a class can change to the smallest amount we can.
The smaller the number of dependencies an object has, the easier it is to keep immutability. This comes back to increasing the maintainability of the code.
Summary
µObjects encourage encapsulation. There's no way around having good encapsulation for a class following µObject practices.
The focus on behavior for interaction keeps the data from being exposed.
The lack of exposure and focus on encapsulating relationships is going to go a long way towards having maintainable code.