Reason Under Pressure
Back

The Essence of Abstraction

Post thumbnail

Photo by Drew Dizzy Graham on Unsplash

Abstraction.

A word that we hear and speak all the time, and that is ever-present in discussions and technical exchanges among our peers.

A concept so fundamental, that it is at the root of almost every construct in software development, without which we would certainly be lost in a raging sea of complexity.

But what exactly is an abstraction?

One would rightfully think that the very people for whom this word is so dear and by whom it is so often used, when asked this question would promptly provide a clear answer.

But, alas, like many other terms with a similar status in software development, this is simply not the case.

Stop and think for a minute.

How would you define abstraction?

Has anyone ever given you a precise definition of it?

Although concepts can still be remarkably useful even when we lack a precise definition for them, there is a certain power that comes with tracing clear-cut boundaries around concepts.

Dealing with fuzzy concepts is like looking at an object through an unfocused lens: the object seems blurry and we can't quite make out its details, it's hard to tell where it begins and where it ends.

By making our definitions more precise, we're focusing our metaphorical lens and thus sharpening the images we see through it.

So let's do an in-depth exploration of this question, where by doing so we'll understand the essence of abstractions, their properties, their role in managing complexity and their relation with other ubiquitous concepts in software development.

Intuition

This situation where we're familiar with a term or a concept yet we can't quite define it is fairly common in our daily lives. I call it the "I'll know it when I see it" phenomenon and it's particularly noticeable when there is some kind of aesthetics at play.

A picture showcasing an example of interior design.

Photo by R ARCHITECTURE on Unsplash

Look at the picture above.

Although this may not be your preferred style of interior design, I believe we can agree that it is in some sense harmonious and well-designed.

Now, if you're like me and have no expertise in interior design, it's hard to tell what exactly makes it so.

It's remarkable that you could show me a hundred different pictures of interior design and in a few minutes I could easily tell which ones I like and which ones I don't, yet if you asked me to produce even a single example of a design that appeals to me, I would fail spectacularly.

Despite my inability to recognize the patterns that make a design appealing, they are there, and if I wanted to start recognizing these patterns, a reasonable course of action would be to examine several examples of good designs and look for what they have in common. This way, I would be able to separate the essential characteristics of a good design from the incidental ones.

So this is what we'll do with abstractions. We'll start by investigating some constructs that we intuitively recognize as abstractions, and then by gathering their commonalities we'll gradually refine and narrow down our definition.

Functions

The first construct we'll investigate is the function, also known as procedure or subroutine.

Functions are ubiquitous in software development, you can find them in almost every programming language, and some people consider them such a fundamental concept that they've even designed an entire programming paradigm around it (functional programming).

A function is a parameterized block of code encapsulated as a single unit, which can then be called whenever we need to execute that block of code.

When functions first came into being, their primary purpose was to facilitate code reuse while preventing repetition (AKA copying and pasting code), and even nowadays when people first learn about functions, this is what is usually used as the motivation for their existence.

However, people quickly realized that this encapsulation has an interesting side effect: it creates a conceptual separation between what that block of code does and how it does it.

Think about the last time you used a function from a library or a framework, or even one that you implemented yourself a long time ago.

At that time, did you know how exactly that function was implemented, that is, what its source code looked like in detail? Probably not, yet I bet you were still able to use it effectively.

This idea is not exclusive to software development, much on the contrary, it's something we can find everywhere in our daily lives. We're surrounded by things we use all the time, yet we have no idea how they work.

Take a car for example: a lot of people drive cars, but how many of them know how they work? How many of them could "implement" a car, that is, build one from scratch?

Whether in our everyday lives or in software development, this separation between what something does and how it does it is key to a process we call information hiding or conceptual compression.

This process is crucial to managing complexity, as it allows us to reduce the amount of information one needs to deal with in a given context.

For example, the context in which we're using a function requires much less information about the function than the context in which we're implementing that same function, and these contexts are never the same, nor do they overlap.

Like when you call a hashing function, you probably don't really know what are the exact steps that are carried out to produce the hash, you only need to know that it produces a hash (and obviously, what a hash is).

Getting a little bit ahead of myself, all of these concepts will also make an appearance in constructs we'll investigate next, which is a pretty strong indicator that they're intimately related to the essence of abstraction and there's even a quote (which is one of my favorite characterizations of abstraction) that summarizes this idea:

The essence of abstraction is preserving information that is relevant in a given context, and forgetting information that is irrelevant in that context. - John V. Guttag

Back to functions, I want to make this idea of information hiding more tangible through some diagrams.

For starters, considering that programs are sequences of instructions, we're going to represent each instruction as a circle, like so:

What exactly each of these instructions is doesn't really matter for our purposes, they could be a variable definition/assignment, some conditional, a function call, etc.

Each instruction carries some amount of information, which we have to keep in our heads when dealing with that instruction.

With that in mind, we can represent this amount of information with a number, like an "information score" if you will, which we'll represent with a box on the left side of each instruction:

According to this diagram, the resulting information score of our program is 8, which is the sum of the information scores of each instruction.

Now let's extract the instructions I2 and I3 as a function A:

In our diagram, we now have a circle A that represents the function we just extracted and the two instructions I2 and I3 are linked to it, to represent the idea that A is implemented in terms of them.

As we mentioned before, using a function requires less information than implementing it, so in this case let's say that using A requires 2 units of information (this was an arbitrary choice, in practice it depends on each function).

In this case, the block of code represented by I1, A and I4, which previously used to have a combined information score of 8, now has a combined information score of 6, that is, we reduced the amount of information we need to deal with by 2 units.

Here's another example:

In this case, functions are already extracted and we have more than one "layer" of code, but the idea remains the same:

The information score associated with the implementation of A is 7, but the information needed to use it has a score of only 3.

B is implemented in terms of A and I3, so its implementation has an information score of 4, which is the sum of the information score of I3 and the information score associated with the usage of A.

When analyzing these "layers" of code, we see that many times, functions are implemented in terms of other functions, which ultimately means that the "what" of a function is part of the "how" of another.

By breaking up our program into functions, we can limit the amount of information we need to deal with at any given time, that is, we keep complexity in check.

Another important observation is that what a function does is always implied by how it does it, in other words, the "how" of a function must always support its "what". Another way of looking at this is that if you read the source code of a function you should be able to infer what it does.

The converse, however, is not true, that is, a function's "what" does not imply its "how", in the sense that just by looking at what a function does, you can't infer how exactly it is implemented, which is precisely what supports the phenomenon of information hiding.

Although a function's "what" does not imply its "how", it constrains it, in the sense that the "how" must always be compatible with the "what". This relationship is crucial to yet another useful side-effect that arises from this separation between "what" and "how" of a function, which is decoupling.

Coupling is a measure of how much changes to something affect other things that depend on it.

For example, if you have a function A that is implemented in terms of (that is, that makes use of) another function B, changes to B might affect A, depending on the extent and nature of those changes.

If we only change how B works but we keep what it does "intact", then A will be unaffected by those changes, as A does not really care, nor it depends (at least ideally) on the internals of B.

If, however, we change what B does, then A will be affected by those changes.

By carefully selecting what kind of information we'll expose through a function's "what", we can control the extent to which changes to its "how" will affect those who depend on it.

Objects

Next, we'll move to objects.

Objects are amalgamations of data and operations that are closely related to that data, and they serve a lot of different purposes like:

  • Collocating data and behavior for cohesion.

  • Facilitating contextual data sharing among operations.

  • Enforcing data consistency/validity (AKA invariants).

  • Concealing the data's internal representation as a decoupling mechanism.

Of these purposes, we're going to focus on the last one as it is the most relevant to our exploration.

In contrast to plain data records, where the data is accessed directly by clients without any kind of mediation, objects allow us to conceal the actual data stored in them and to expose a representation of that data that does not necessarily map 1-to-1 to it.

For instance, suppose we have a record that represents a person, and that it has 3 data fields: firstName, lastName and dateOfBirth.

When using that record, clients have no choice but to read and write directly to these fields.

If we use an object, however, instead of giving free access to clients to these data fields, we could expose only two methods: getName and getAge, where getName returns the first name concatenated with the last name and getAge returns the elapsed time in years between the person's date of birth and the current time.

Although the object's internal data representation would be the same, that is, it would still have the same three data fields internally, both reads and writes would always be mediated by the object's methods, so that only a particular representation of that data would be known by clients.

Notice that once again the concept of information hiding makes itself present, where besides encapsulating behavior just like functions do, objects also encapsulate state.

This encapsulation gives rise to a division that splits an object into two parts: the interface and the implementation.

An object's interface is composed of the data fields (if any) and methods it exposes, it characterizes the object's behavior, how it can be interacted with and what can be expected from it.

An object's implementation is composed of the object's internal data representation and its inner workings, which includes its methods implementations as well (which is why we consider that objects also encapsulate behavior).

The purpose of this encapsulation is twofold: it reduces the amount of information clients need to know in order to interact with the object and it decouples clients from the object's implementation details so that changing the implementation in a way that's compatible with the interface does not affect clients.

Much in the same way that a function's "how" implies its "what", an object's implementation always implies its interface, in the sense that the information that's present in the interface is always a subset of the information that the implementation contains.

And although an object's interface constrains its implementation, it does not imply it, as there are lots of different implementations that are compatible with the same interface.

For instance, going back to our example where we have an object that represents a person, the reason it's possible to have a getAge method in its interface is because although there's no age field in its implementation, there's a dateOfBirth field from which the person's age can be derived.

On the other hand, if the situation was reversed, that is, if instead of having an internal dateOfBirth field, the object had an age field instead, there would be no way for the interface to expose a getDateOfBirth method, as it's not possible to derive a person's exact date of birth (year, month, day, hour and minute) just from the person's age.

About Interfaces

Before we move on to the next construct, I want to talk a little bit about the term "interface" as it is a term with a very broad meaning and that has been heavily overloaded with time.

Depending on each person's background, this term will mean a completely different thing, which will make things confusing.

For a person that comes from a Java or a C# background, "interface" could mean a specific keyword that exists in the language and that defines a sort of a "blueprint" for objects, in terms of what methods they should have along with the signature of these methods.

interface Bicycle {
    // wheel revolutions per minute
    void changeCadence(int newValue);
    void changeGear(int newValue);
    void speedUp(int increment);
    void applyBrakes(int decrement);
}
interface IPoint
{
// Property signatures:
    int X { get; set; }
    int Y { get; set; }
    double Distance { get; }
}

For someone with a C++ background, "interface" could mean a class that has only pure virtual methods in it.

class IBicycle {
  public:
    virtual void changeCadence(int newValue) = 0;
    virtual void changeGear(int newValue) = 0;
    virtual void speedUp(int increment) = 0;
    virtual void applyBrakes(int decrement) = 0;
}

For people in the Typescript land, "interface" could mean a keyword that defines a type.

interface Bicycle {
  changeCadence: (newValue: number) => void;
  changeGear: (newValue: number) => void;
  speedUp: (increment: number) => void;
  applyBrakes: (decrement: number) => void;
}

And so on.

So, to avoid confusion, I want us to have an agreement on a very specific meaning for this term, so that we all mean the same thing when we use it.

Although all of the above meanings share some common characteristics with the meaning I intend for this term, all of them focus too much on the "shape" of things — they specify what methods or attributes some object should have, but not necessarily their behavior.

For example, there are a million different ways to implement the Bicycle interface in Java, and although these implementations could all be compatible with the interface shape-wise, they could all do completely different things. The fact that two objects have the same methods and these methods have the same signature, it doesn't mean that their behavior is compatible.

So, in the context of this post, whenever you see the word "interface", I want you to think about behavior, about the properties that the interface expects from a given implementation. And by properties, I do not mean data fields or attributes, but rather any characteristic that can be attributed to something.

Notice that when using the term "interface" this way, interfaces give us a much stronger guarantee than when using the term to merely signify a "shape" that something should comply with.

Many times, the behaviors/properties that an interface establishes cannot be captured by the type systems that are usually available. For example, an interface for a Sorter object could establish that implementations must have a time complexity of O(nlog(n))\mathcal{O}(n\log(n)), but this kind of property usually cannot be enforced by type systems.

Or maybe there's a Widget interface that specifies a render method that must be pure (i.e. should always yield the same result for the same parameters and should not have side-effects), which is also not something that's enforceable through a language features alone.

To reiterate, from now on, "interface" refers to the set of all properties objects that implement that interface must have, including those that cannot be enforced by the language's type system, or even the language's runtime.

Application Programming Interfaces (APIs)

Before we start talking about APIs, there's an important discussion we need to have regarding terminology.

The term API is a very broad one and can assume a specific meaning depending on the context and the demographic you're dealing with.

Nowadays, if you're a web developer, the first thing that comes to mind when you hear the term API is probably an HTTP API, which is composed of a set of endpoints that move data around using JSON, or less commonly, XML.

However, the term is much older than HTTP APIs and in its broadest sense signifies a set of operations that are exposed by a system so that other systems can interact with it.

So when we talk about APIs in general, we might be talking about the set of functions and classes that a library or framework exposes, or we might be talking about how a database lets you interact with it, or about the commands a command-line interface accepts, and so on.

Moving forward, when talking about APIs, we'll adopt the term in its broadest sense.

Much like functions and objects, APIs rely heavily on the notion of information hiding, and they encapsulate both behavior and state.

For example, consider an HTTP API. Behind this API there might be a lot of stuff going on, like reverse proxies, load balancers, servers, caches, databases, etc.

However, when you're interacting with this API, you don't need to know about any of that, you only need to know what endpoints are available and what data you need to send to them to get the desired behavior.

Much like functions and objects, this encapsulation has some important consequences:

First, by hiding their inner workings, APIs greatly reduce the amount of information that clients need to know in order to use them.

Second, this encapsulation breaks up APIs into two "parts", a contract which is what can be expected from an API in terms of behavior and the API's internals , which is composed of all its implementation details, where the internals always implies the contract.

Third, although an API's contract establishes limits on the API's internals, it does not imply it, in the sense that just by looking at an API's contract, you cannot infer how exactly it works internally.

Lastly, this encapsulation decouples clients from the APIs internals, hiding these implementation details behind the contract, which is what clients ultimately depend on, so that changing the internals of an API does not break clients as long as these changes are compatible with the existing contract.

Defining Abstraction

Now that we've explored a few constructs that we intuitively recognize as abstractions, let's take a step back, look at what they have in common and come up with some generalizations.

In all constructs we investigated, a recurring theme was information hiding, where this phenomenon was always a result of some kind of encapsulation.

This encapsulation divided each construct into two parts — functions were divided into a "what" and a "how", objects into an interface and an implementation and APIs into a contract and its internals, and although these parts were given different names for each construct, they are analogous and it's easy to notice the correspondences among them.

Then, we saw that one of these parts is external and the other is internal, where the external part always contains less information than the internal one, and it's the one that clients depend on.

Because of that, using the construct always requires less information than building it, which helps to keep complexity in check by preserving information that is relevant to clients while hiding information that is irrelevant to them.

Additionally, the internal part always implies the external one, where the converse is not true.

Lastly, because clients depend on the external part, they are decoupled from the internal one, so that changes to the internal part that are compliant with the existing external part, do not cause clients to break.

With these things in mind, the following definition arises:

An abstraction is a construct that offers a simplified representation of itself, where this representation exposes only a selected subset of the construct's properties.

Also, I propose we generalize the names of the external and internal parts that compose an abstraction, by calling them interface and implementation respectively, just like we do for objects.

This specific was made on the grounds that these terms are widely known and are commonly used to refer to these parts in general.

With that in mind, we can slightly complement our definition as follows:

An abstraction is a construct that offers a simplified representation of itself, where this representation, the interface, exposes only a selected subset of the construct's properties, the implementation.

Although this definition covers every characteristic we've seen so far, there's still room to make it more precise.

To do that, however, we'll need two things:

First, we'll talk about properties in depth, as they are at the core of our definition of abstraction and they are intimately related to the notion of information hiding.

Second, we'll have to up our game because natural language won't suffice as it lacks the precision we're looking for, so we'is information anyway?e world's most precise language: Mathematics.

A Detour Into Properties

So far, we've been talking a lot about information and the notion of information hiding through encapsulation, but what exactly is information anyway?

In the context of abstractions, the answer is simple:

Information is a set of properties.

Previously, we've been using the idea of information because it was easier to deal with intuitively, as it was our initial setting, but whenever we referred to information we were always implicitly referring to a set of properties.

So, from this point onwards, we'll start using the notion of property instead of the notion of information, as the former will help us in our goal of making things more precise.

Defining Properties

Generally speaking, a property is some characteristic that can be attributed to something.

When we say that something is large, that thing has the property of being large, when we say that something is blue, that thing has the property of being blue, and so on.

Properties might have a description that is as simple as "being blue", or as complex as "being a function that given a prime number takes 3 times more time to run than when given a non-prime number", and beyond.

Each thing has a set of properties that uniquely describes it, and if two things have the exact same set of properties, then they are the same thing. Conversely, if two things are different, then there must be at least one property that one has and the other doesn't. This principle is known as the Identity of Indiscernibles.

Properties are easy to grasp intuitively, however, they are quite abstract and it's hard to refer to them directly. Thus, we usually refer to them through predicates instead, as we did a moment ago, and by predicates, I mean a sentence or part of a sentence that describes the property we're referring to.

Although it might seem like predicates and properties are the same thing, that's not the case, because just as there are many different ways of describing the same thing, there are many different predicates (actually infinitely many) that refer to the same property.

Even though this distinction will not be that relevant for the upcoming discussions, I just wanted to let the more attentive reader know that this was not a detail that was overlooked, as we'll be using predicates and properties interchangeably.

So let's see a few statements that illustrate this idea:

  • The number 2 is even.

  • The sky is blue.

  • Pigs don't fly.

  • The function double multiplies a number by 2.

  • The function double takes a single argument.

  • The function double is implemented by adding a number to itself.

  • The GET /users endpoint returns a list of users.

  • The GET /users endpoint never modifies the list of users.

  • The GET /users endpoint cannot be called more than 100 times per minute by a single IP.

  • The Person class has an age field.

  • The Person class is guaranteed to never have a negative age.

  • The Person class has a getAge method.

  • The Person class doesn't have a getDateOfBirth method.

  • The function logout can only be called after the function login has been called.

Each one of the statements above contains a predicate that refers to a specific property.

Going to back to the idea that each thing has a set of properties that uniquely describe it, if we take, for instance, an arbitrary function, there's a set of all the properties this function has and that describes each little thing about it, which corresponds to the entirety of the information that exists about this function, including what it does and how it does it.

The same idea is true for any other construct, be it an object, an API, or pretty much anything else.

Properties and Implication

Moving forward, I want to draw your attention to a key aspect of properties, which is the fact that a set of properties implies other properties.

In other words, if something has a certain set of properties, the properties contained in this set, together, imply other properties that are not necessarily part of the initial set.

For instance, if a cellphone has the property of having a 4G connection, and it also has the property of being within the coverage area of a 4G network, then, by necessity, it also has the property that it can connect to the internet without using wifi.

In another example, if the function double has the property that it takes a single argument, then, by necessity, it also has the property that it doesn't take two arguments.

This notion of implication is going to be important for two reasons:

First, when we talked about an abstraction's implementation implying its interface, it was precisely this notion we were referring to.

Second, it allows us to compare sets of properties in terms of how much information they possess.

Let's delve into this second point.

Intuitively speaking, the amount of information a set of properties has, is directly proportional to how many properties are contained in it, so, the "bigger" the set, the more information it has and vice versa.

In reality, however, the situation is not that straighforward, as even if a set of properties is properly contained in another set, they could have the same amount of information. Similarly, adding more properties to a set, does not necessarily equate to adding more information to it.

The situation is analogous to solving systems of linear equations.

When solving these systems, each equation is a "piece of information", where the more pieces you accumulate, the fewer degrees of freedom your solution will have, and if you have just enough pieces, your solution is a single vector.

However, adding more equations to a system does not necessarily equate to adding more information, in the sense that some equations are "redundant", that is, the information they carry is already implied in previously existing equations in the system (more specifically, when an equation is a linear combination of the other equations).

The same thing happens for sets of properties, where adding properties to a set that are implied by that set does not add more information to it.

With that in mind, this implication relation between sets of properties establishes a way to compare them regarding "how much" information they have, as follows:

  • If two sets imply each other, then they have exactly the same amount of information.

  • If one set imply another, and the second does not imply the first, then it has more information than the second.

  • If one set is implied by another, and the first set does not imply the second, then it has less information than the second.

  • If a set does not imply another set, nor the second implies the first, then there is no way to compare them in terms of "how much" information they have.

If we recall that an abstraction's implementation always implies its interface, then the notion of information hiding arises naturally, as this implication ultimately means that the interface has less information than the implementation.

Considering that we'll be talking a lot about properties, sets of properties and implications between them, I want to introduce some notation that will make the whole process much easier.

For individual properties, we'll use lowercase greek letters, like α\alpha, β\beta, γ\gamma, etc.

For sets of properties, we'll use uppercase greek letters, like Γ\Gamma, Δ\Delta, Σ\Sigma, etc.

And for the implication relation, also called consequence relation, we'll use the turnstile symbol \vdash.

So, if a set of properties Γ\Gamma implies a property α\alpha, we'll write Γα\Gamma \vdash \alpha.

If a set of properties Γ\Gamma implies a set of properties Δ\Delta, we'll write ΓΔ\Gamma \vdash \Delta.

And so on.

There are a few important properties of the consequence relation \vdash that are worth mentioning:

  • \vdash is reflexive, which means that for any property α\alpha, αα\alpha \vdash \alpha, that is, every property implies itself.

  • \vdash is transitive, which means that for any properties α\alpha, β\beta and γ\gamma, if αβ\alpha \vdash \beta and βγ\beta \vdash \gamma, then αγ\alpha \vdash \gamma.

  • \vdash is not symmetric, which means that if αβ\alpha \vdash \beta, not necessarily βα\beta \vdash \alpha.

Deductive Closure

Let's say we have a set of properties Γ={α1,α2,α3,...αn}\Gamma = \{ \alpha_1, \alpha_2, \alpha_3, ... \alpha_n \}.

Then, suppose we start adding other properties to it that are implied by Γ\Gamma, that is, we add a property αn+1\alpha_{n + 1} such that Γαn+1\Gamma \vdash \alpha_{n+1}, then a property αn+2\alpha_{n + 2} such that Γαn+2\Gamma \vdash \alpha_{n + 2} and so on, infinitely, until we end up with a set we'll call Γ\Gamma^* that has all the properties that are implied by Γ\Gamma.

This set has two interesting properties:

First, this set is maximal, in the sense that it is the "biggest" set of properties that has the same amount of information of Γ\Gamma.

Second, because it has all the properties that are implied by Γ\Gamma, any property ϕ\phi that is not in this set is not implied by Γ\Gamma, and thus, has some information that Γ\Gamma itself does not have. This means that adding ϕ\phi to Γ\Gamma (or Γ\Gamma^*) will necessarily increase the amount of information it has.

The set of all properties Γ\Gamma^* implied by a given set Γ\Gamma, is called the deductive closure of Γ\Gamma.

Another way to think about the deductive closure of a set of properties, it through an analogy using train stations.

Let's say there's a set of train stations and a relation between those stations that tells us whether from a station α\alpha you can get to another station β\beta with any number of connections.

The "connection closure" of this set of stations is the set of all stations that you can get to from any station in the original set, that is, the set of all stations that are connected to the original set.

If we replace stations with properties and the connection relation with the consequence relation \vdash, this "connection closure" is analogous to the deductive closure.

Now let's explore a few properties of deductive closures.

As Γ\Gamma^* is the set of all properties that are implied by Γ\Gamma, it follows that ΓΓ\Gamma \vdash \Gamma^*.

Because the consequence relation \vdash is reflexive, the deductive closure Γ\Gamma^* of a set Γ\Gamma contains Γ\Gamma itself, that is, ΓΓ\Gamma \subseteq \Gamma^*.

Also, there's an important theorem that relates implication between sets of properties and their containment relationship, and it states that for any two sets of properties Γ\Gamma and Δ\Delta, Δ\Delta implies Γ\Gamma, if and only if the deductive closure of Δ\Delta contains the deductive closure of Γ\Gamma, that is, ΓΔ\Gamma^* \subseteq \Delta^*.

ΔΓΓΔ\Delta \vdash \Gamma \leftrightarrow \Gamma^* \subseteq \Delta^*

First, let's understand this theorem intuitively and then we'll prove it.

What this theorem tells us is that a set of properties that are implied by another set of properties, is always a selection of properties of the second set.

Conversely, anytime you select some properties from a set of properties, that subset you just carved from the original set is implied by the first set.

Considering that an abstraction's implementation always implies its interface, it follows that the interface is always a selection of the properties contained in the implementation.

Conversely, anytime you take an implementation and select some properties of it, this selection is implied by the implementation and thus it is a suitable interface.

Notice that we abused the language a little bit here, because where the theorem talks specifically about the deductive closure of sets of properties regarding their contaiment relations, we simplified things and talked about the sets of properties themselves.

The main reason we introduced the concept of deductive closure (besides being an interesting concept in itself), was to "justify" this abuse.

Intuitively, one might believe that:

ΔΓΓΔ\Delta \vdash \Gamma \leftrightarrow \Gamma \subseteq \Delta

But that's not really the case, as something like this could happen:

Δ={α}Γ={β}αβ\Delta = \{\alpha\} \\ \Gamma = \{ \beta \} \\ \alpha \vdash \beta

In this case, as αβ\alpha \vdash \beta, then ΔΓ\Delta \vdash \Gamma, however, it's cleary not the case that ΓΔ\Gamma \subseteq \Delta.

So, for us to establish the desired relation while retaining the soundness of our reasoning, we have to consider the deductive closure of sets.

Now let's prove this theorem.

First, let's prove that if Δ\Delta implies Γ\Gamma, then ΓΔ\Gamma^* \subseteq \Delta^*.

ΔΓΓΔ\Delta \vdash \Gamma \rightarrow \Gamma^* \subseteq \Delta^*

Suppose that Δ\Delta implies Γ\Gamma, that is, ΔΓ\Delta \vdash \Gamma.

Then, let's take an arbitrary property ϕ\phi in Γ\Gamma^*.

By definition, if ϕ\phi is in the deductive closure of Γ\Gamma, which is Γ\Gamma^*, then it means that Γ\Gamma implies ϕ\phi, that is, Γϕ\Gamma \vdash \phi.

As the consequence relation \vdash is transitive, and we know that ΔΓ\Delta \vdash \Gamma, then it follows that Δϕ\Delta \vdash \phi.

By definition, this means that ϕ\phi is in the deductive closure of Δ\Delta, that is, ϕ\phi is in Δ\Delta^*.

As ϕ\phi was an arbitrary property in Γ\Gamma^*, this means that for any property ϕ\phi in Γ\Gamma^*, ϕ\phi is also in Δ\Delta^*, which means that ΓΔ\Gamma^* \subseteq \Delta^*. QED.

Now let's prove the reciprocal, that is, if ΓΔ\Gamma^* \subseteq \Delta^*, then ΔΓ\Delta \vdash \Gamma.

ΓΔΔΓ\Gamma^* \subseteq \Delta^* \rightarrow \Delta \vdash \Gamma

Suppose that ΓΔ\Gamma^* \subseteq \Delta^*.

Then, let's take an arbitrary property ϕ\phi in Γ\Gamma.

As we saw before, a set of properties is always contained in its deductive closure, therefore ΓΓ\Gamma \subseteq \Gamma^*.

This means that ϕ\phi is also in Γ\Gamma^*.

Because we know that ΓΔ\Gamma^* \subseteq \Delta^*, then it follows that ϕ\phi is also in Δ\Delta^*.

By definition, because ϕ\phi is in the deductive closure of Δ\Delta, that is, ϕ\phi is in Δ\Delta^*, then it means that Δ\Delta implies ϕ\phi, that is, Δϕ\Delta \vdash \phi.

As ϕ\phi was an arbitrary property in Γ\Gamma, this means that for any property ϕ\phi in Γ\Gamma, Δ\Delta implies ϕ\phi, which means that Δ\Delta implies Γ\Gamma, that is, ΔΓ\Delta \vdash \Gamma. QED.

Ordering Sets of Properties

Now that we established a connection between implication among sets of properties and their containment relationship, we can use that connection to define an ordering between sets of properties.

Before we do that, let's understand what an ordering is, and the different types of orderings that exist.

Intuitively speaking, an ordering is any relation that tells us how to compare two things, in terms of which is "smaller" and which is "bigger".

For a relation << (which is not necessarily to be taken in the usual meaning of this symbol) to be considered an ordering, it needs to satisfy two properties:

  • Transitivity: If a<ba < b, and b<cb < c, then a<ca < c.

  • Asymetry: If a<ba < b, then it is not the case that b<ab < a.

There's the possibility to "exchange" asymetry for antisymetry, which states that if a<ba < b, and b<ab < a, then a=ba = b. In this case, "smaller" actually means "smaller or equal".

When we have an ordering that is asymmetric, it is called a strict ordering, and when we have an ordering that is antisymmetric, it is called a non-strict ordering.

Then, we can subdivide orderings into two other categories: total and partial.

A total ordering, which is also called a linear ordering, is an ordering where every pair of elements can be compared, that is, it satisfies the trichotomy property, which states that for any two elements aa and bb, either aa is smaller than bb, bb is smaller than aa, or aa and bb are the same thing.

Total orderings are the ones we're most familiar with, because that's how numbers are ordered with regards to the << or \leq relations.

They are called linear, because they form a "line", in the sense that each element "divides" the line into two sections, a section where elements are smaller than it, and a section where elements are bigger than it.

A partial ordering, on the other hand, is an ordering where not every pair of elements can be compared.

A concrete example of a partial ordering is the "descends from" relation in a family tree:

In the above picture, arrows depict the "descends from" relation, and as you can see, not every pair of elements are connected by an arrow, which means that those elements are not comparable.

For instance, neither aa descends from bb, nor bb descends from aa, therefore aa and bb are not comparable.

However, it's still an ordering (and in this case, a strict one), as it satisfies both asymmetry, as if someone descends from another person, then that person doesn't descend from the first one, and transitivity, as if a person descends from another person, and that person descends from yet another person, then first descends from the last.

Back to sets of properties and their relations, taking into account that we were using the consequence relation \vdash to compare sets of properties and their information contents, naturally, one is led to believe that \vdash could be used to order sets of properties.

But alas, that's not possible, because although \vdash is transitive, it is not asymmetric nor antisymmetric.

For instance, let's say that there are two different properties α\alpha and β\beta such that αβ\alpha \vdash \beta and βα\beta \vdash \alpha.

In this case, taking Γ={α}\Gamma = \{ \alpha\} and Δ={β}\Delta = \{ \beta\}, it follows that ΓΔ\Gamma \vdash \Delta and ΔΓ\Delta \vdash \Gamma, which already shows us that \vdash is not asymetric.

Also, as αβ\alpha \ne \beta, then ΓΔ\Gamma \ne \Delta, which means that \vdash is also not antisymmetric.

However, there's a small adjustment that we can make to turn \vdash into an ordering relation.

Although \vdash is a not suitable ordering for sets of properties, it is a suitable ordering for the deductive closures of sets, that is, for the sets that are closed under deduction.

When we consider only deductive closures of sets, \vdash is still transitive, but now it also becomes antisymmetric, as we'll prove next.

ΓΔΓ=Δ\Gamma^* \dashv \vdash \Delta^* \rightarrow \Gamma^* = \Delta^*

Suppose that ΓΔ\Gamma^* \dashv \vdash \Delta^*.

Then, let ϕΓ\phi \in \Gamma^* be an arbitrary property of Γ\Gamma^*.

As ΔΓ\Delta^* \vdash \Gamma^*, then in particular, Δϕ\Delta^* \vdash \phi.

By the definition of Δ\Delta^*, it follows that ϕΔ\phi \in \Delta^*.

As ϕ\phi is arbitrary, then ΓΔ\Gamma^* \subseteq \Delta^*.

By replicating this reasoning in an analogous way for Δ\Delta^*, then ΔΓ\Delta^* \subseteq \Gamma^*.

As two sets are equal precisely when one contains the other and vice versa, then Δ=Γ\Delta^* = \Gamma^*. QED.

Thus, by restricting ourselves to deductive closures of sets of properties, \vdash becomes a non-strict partial ordering.

The reason it is partial and not total, is because it is not the case that for any two sets of properties Γ\Gamma^* and Δ\Delta^*, that either ΓΔ\Gamma* \vdash \Delta^* or ΔΓ\Delta^* \vdash \Gamma^*, that is, some sets of properties are not comparable by \vdash.

With that, we have a formal notion of ordering sets of properties according to the amount of information they have.

A Formal Definition

Now that we have a thorough understanding of properties, we can come up with a formal definition for the concept of abstraction.

Without further ado, I propose the following definition:

An abstraction is an ordered pair Γ,Δ\langle \Gamma, \Delta \rangle such that Γ\Gamma, called "the interface", and Δ\Delta, called "the implementation", are sets of properties such that ΔΓ\Delta \vdash \Gamma.

Now I want to show you that this definition captures all the intuitive notions we have about abstractions.

First, it captures the notion that the abstraction is composed of two parts, the interface and the implementation, which are represented by Γ\Gamma and Δ\Delta respectively.

Second, it captures the notion of information hiding, as ΔΓ\Delta \vdash \Gamma, which tells us that Δ\Delta has Γ\Gamma has less information than Δ\Delta. Alternatively, recalling that if ΔΓ\Delta \vdash \Gamma then ΓΔ\Gamma^* \subseteq \Delta^*, the information that the interface has is quite literally a subset of the information the implementation has, where the information that is being hidden through the process of encapsulation is precisely the difference ΔΓ\Delta^* - \Gamma^*.

Third, it captures the notion that the implementation always implies the interface, which is laid out in the definition itself.

Lastly, it captures the notion that although the interface does not imply the implementation, it constrains, given that ΓΔ\Gamma^* \subseteq \Delta^*, which means that Δ\Delta^* can only "shrink" to the point that it still contains Γ\Gamma^*.

Now that we have a formal definition of abstraction, let's explore some of its properties and relations to other concepts.

The Three Personas

Three personas exist for every abstraction: the client, the designer and the implementer.

These personas are not necessarily different people, as many times they're actually the same person. The idea is that each persona personifies a different role or perspective for an abstraction.

The designer is the persona who designs the interface, by deciding which properties it will be composed of.

The implementer is the persona who implements the interface, it crafts the implementation according to the constraints imposed by the interface.

The client is the persona who uses the abstraction, where its use cases must be supported by the interface.

When we look at our formal definition of abstraction, we notice that the designer is related to Γ\Gamma and the implementer is related to Δ\Delta, but what about the client, what does it relate to?

To answer this question, we introduce the concept of use case.

A use case, within the scope of this post, is a set of properties Σ\Sigma that represents the requirements a client needs the abstraction (and more precisely, its interface) to fulfill in order to support that use case.

To understand use cases in full, first, we need to pay attention to the fact that most times, clients do not rely on all the properties that an abstraction exposes through its interface, but only a subset of them.

Let me show you a concrete example:

Suppose there's a function double that outputs two times the number it receives as its input.

Besides the fact that this function double multiplies numbers by two, it also has the property that it works for any number, and this property, of course, it part of its interface.

Also, let's say that in a given part of an application, we're calling that function three times: double(2), double(4), double(7).

At first look, it might seem that as a client, we're relying on the property that this function works for any number, but upon a close look, the property we need is weaker than that — what we actually need, is for the function to work for only three values, namely, 2, 4 and 7.

The property that guarantees that double works for the specific values 2, 4 and 7, is implied by the property that it works for any number, however, the converse is not true.

Thus, formally speaking, the properties we really need, represented by Σ\Sigma, are implied by the properties that the interface, Γ\Gamma, contains, that is, ΓΣ\Gamma \vdash \Sigma.

Also, from the theorem we saw before, ΣΓ\Sigma^* \subseteq \Gamma^*, that is, the properties we're using are a subset of the properties contained by the interface.

So, by taking the use case, the interface and the implementation all at once, we arrive at the following conclusions:

The implementation implies the interface, which in turn, implies the use case.

ΔΓΣ\Delta \vdash \Gamma \vdash \Sigma

The use case is a subset of the properties of the interface, which in turn, is a subset of the properties of the implementation.

ΣΓΔ\Sigma^* \subseteq \Gamma^* \subseteq \Delta^*

Having the concept of use case established, we can now explore the interactions between these personas.

Abstraction vs Flexibility

So far, we have thought about abstractions in a mostly static, where use cases, interfaces and implementations are "fixed".

Now, I want us to consider what happens when these things change, and how these changes affect each other.

Let's start by looking at changes to interfaces.

As we just saw, interfaces both support use cases and constrain implementations, a fact that is formalized by ΔΓΣ\Delta \vdash \Gamma \vdash \Sigma.

So, for a given interface Γ\Gamma, there is the set of all the use cases it supports and also the set of all suitable implementations.

To formalize these concepts, we'll introduce two families of sets:

A family S(Γ)\mathcal{S}(\Gamma) that represents the set of all use cases a given Γ\Gamma supports.

S(Γ)={ΣΓΣ}\mathcal{S}(\Gamma) = \{ \Sigma \mid \Gamma \vdash \Sigma \}

And another family D(Γ)\mathcal{D}(\Gamma) that represents the set of all suitable implementations for a given Γ\Gamma.

D(Γ)={ΔΔΓ}\mathcal{D}(\Gamma) = \{ \Delta \mid \Delta \vdash \Gamma \}

There are many different ways interfaces can be changed, and anytime an interface Γ1\Gamma_1 changes to a different interface Γ2\Gamma_2, the sets of supported use cases and suitable implementations will change as well.

However, I want us to focus in two specific kinds of changes, one where the interface grows, that is, Γ1Γ2\Gamma_1 \subseteq \Gamma_2 and another where the interface shrinks, that is, Γ1Γ2\Gamma_1 \supseteq \Gamma_2.

First, let's consider this situation intuitively.

For a given interface Γ\Gamma, any use case Σ\Sigma must always be a subset of the properties of the interface, that is, ΣΓ\Sigma^* \subseteq \Gamma^*, therefore, as the interface grows, the amount of possible subsets grows as well. So, the more guarantees an interface has, the more use cases it supports.

Conversely, as the interface shrinks, the amount of possible subsets of that interface shrinks as well. So, the less guarantees an interface has, the less use cases it supports.

For instance, let's say there's a function random(x) that returns a pseudo-random number between 0 and a given number x.

If this function, at first, supports only integers, any use cases that would require passing floats are not supported by this function.

If we augment the function's interface so that it starts supporting floats as well as integers (and, of course, change its implementation accordingly), it will still support all of the previous use cases, but now it will also support the additional use cases that require passing floats.

When looking at implementations, however, the situation is the inverse.

For a given interface Γ\Gamma, any suitable implementation Δ\Delta of that interface must be a superset of the properties that are guaranteed by Γ\Gamma, that is, ΓΔ\Gamma^* \subseteq \Delta^*.

As Γ\Gamma grows, it fits inside fewer implementations than it did before, and thus there will be fewer suitable implementations.

As Γ\Gamma shrinks, it fits inside more implementations than it did before, and thus there will be more suitable implementations.

For instance, suppose an Array object has a map method that creates a new array from the original one by iterating on each element and mapping it to a new element using a callback that's passed by the client.

If initially, the map method has no guarantees in terms of the iteration order that is going to be used, the implementer may choose whatever order it sees fit. Maybe it will iterate in direct order, or maybe in reverse, or maybe even iterate over all even indexes then move to the odd ones, it doesn't matter.

However, if map changes its interface so that now it follows a specific iteration order, all those other implementations that had a different iteration order won't be suitable anymore.

Thus, to sum it up:

As the interface grows, the set of all supported use cases grows and the set of all suitable implementations shrinks.

Γ1Γ2S(Γ1)S(Γ2) \Gamma_1^* \subseteq \Gamma_2^* \rightarrow \mathcal{S}(\Gamma_1) \subseteq \mathcal{S}(\Gamma_2)
Γ1Γ2D(Γ1)D(Γ2) \Gamma_1^* \subseteq \Gamma_2^* \rightarrow \mathcal{D}(\Gamma_1) \supseteq \mathcal{D}(\Gamma_2)

As the interface shrinks, the set of all supported use cases shrinks and the set of all suitable implementations grows.

Γ1Γ2S(Γ1)S(Γ2) \Gamma_1^* \supseteq \Gamma_2^* \rightarrow \mathcal{S}(\Gamma_1) \supseteq \mathcal{S}(\Gamma_2)
Γ1Γ2D(Γ1)D(Γ2) \Gamma_1^* \supseteq \Gamma_2^* \rightarrow \mathcal{D}(\Gamma_1) \subseteq \mathcal{D}(\Gamma_2)

Alternatively, we can say that the amount of supported use cases is directly proportional to the size of an interface and the amount of suitable implementations is inversely proportional to it.

What this tells us is that there is a permanent tension between abstraction and flexibility.

The more abstracted (or abstract) you make an abstraction, the fewer properties you guarantee, that is, the smaller the interface, which means that this abstraction becomes less flexible, as it supports fewer use cases.

On the other hand, the more flexible you make an abstraction so that it supports more use cases, the more properties you have to guarantee, that is, the bigger the interface, which means that this abstraction becomes less abstracted (or abstract), as it places more constraints on implementations.

At the same time, the more abstracted you make an abstraction, the less information its interface has, which means that clients need to know less stuff to understand and use the abstraction effectively.

Conversely, the more flexible you make an abstraction, the more information its interface has, which means that clients need to know more stuff to understand and use the abstraction effectively.

Balancing these two forces is something that we do all the time whether we're aware of it or not.

Framework and library maintainers have to choose carefully how abstract or flexible their APIs will be, for if abstractions are too abstract, they will cover far too few use cases of users.

If, however, they make their APIs too flexible and expose too much of its internals, it makes it hard to maintain and to evolve the APIs as changes to their implementations are more likely to break clients.

When choosing a library, or a service to use in our application, we'll be faced with this dilemma as well.

There are tools that cover a lot of use cases, and choosing such tools makes it easier for us to evolve our application as new use cases come by, however, these tools also require us to learn a lot to use them effectively.

Other tools are much simpler to use, as they abstract a lot of stuff for us, but at the same time, they are much more limited than other tools that are less abstracted, and thus as our application evolve, newer user cases might not be covered by these tools and we'll be forced to migrate to a more flexible abstraction.

Lately there was a lot of discussion on high code vs low code vs no code, and this idea of abstraction vs flexibility plays a key role in these discussions.

A no-code solution is usually easier to use than a low-code solution, which in turn is easier to use than a high-code solution.

However, this ease of use does not come for free, as often, the more a tool abstracts for you, the faster you hit its customizability ceiling, which is a common complaint regarding both low-code and no-code tools.

So now, it should be clear to us that the very thing that makes an abstraction easier to use and easier to maintain is also the thing that makes the abstraction less flexible, and vice versa, making an abstraction more flexible makes it harder to use and harder to maintain.

The Tao of Abstraction

There's a phenomenon that occurs in some situations, where a client is simultaneously a client and an implementer, and an implementer is simultaneously an implementer and a client.

Consider an abstraction that's a function map, and that function takes two arguments, an array and a callback that will be used to map the elements of the original array to a new, transformed array.

This is abstraction's interface is composed both by the fact that this function takes a callback as an argument, as well as the interface that this callback must comply with..

However, because this callback is provided by the client, and not the implementer, there's the possibility that this callback ends up being implemented by the client itself (think of passing an anonymous function).

Analogously, the implementer on the other side of the abstraction will receive this callback, but it doesn't have access to this callback's implementation, the only thing it can rely on is its interface, and thus it is a client of that callback.

Stacking Abstractions

Almost every abstraction is built on top of other abstractions.

This layering of abstractions is a way to keep complexity in check, by ensuring that each abstraction has the "right" amount of information, both in its interface and in its implementation.

The amount of information contained in an interface is dictated by the designer of an abstraction.

Analogously, the amount of information that an implementation contains, is a consequence of how the abstraction's implementer chose to implement it.

Let's say there's an abstraction A=ΓA,ΔAA =\langle \Gamma_A, \Delta_A \rangle that is implemented in terms of (that is, its implementation makes use of) the abstractions B=ΓB,ΔBB = \langle \Gamma_B, \Delta_B \rangle, C=ΓC,ΔCC =\langle \Gamma_C, \Delta_C \rangle and D=ΓD,ΔDD = \langle \Gamma_D, \Delta_D \rangle.

What this means is that AA's implementation is a client of BB, CC and DD, and thus, it needs to know about their interfaces, as it relies on its properties. So, formally speaking, ΓB\Gamma_B, ΓC\Gamma_C, ΓD\Gamma_D are all contained within ΔA\Delta_A.

By relying on BB, CC and DD, instead of having to know about ΔB\Delta_B, ΔC\Delta_C and ΔD\Delta_D, AA's implementation only needs to know about the information that's contained in their interfaces.

Responsibilities

In the context of software development, a responsibility is something that a construct must do or know.

From this definition, we can say that there are two kinds of responsibilities: the responsibility of doing something, and the responsibility of knowing something.

The responsibility of doing something, is a responsibility towards the clients of a construct, whereas the responsibility of knowing something is a responsibility towards the construct itself.

The reason we talk about responsibilities in software development is to keep complexity in check by limiting the amount of information we'll have to deal with at a time, which means that responsibilities are tightly linked with abstractions.

By carefully assigning responsibilities to constructs in software development, we can avoid constructs that either do too much or know too much.

From what we've learned so far, it's clear that responsibilities correspond to a certain kind of property, where responsibilities about doing something are part of an abstraction's interface, and responsibilities about knowing something are part of an abstraction's implementation.

Thus, assigning responsibilities, designing abstractions and layering them go hand in hand.

Finale

At last, we have reached the end of our journey.

We started with questioning ourselves about what abstractions really are, and then little by little we progressed from intuition to a precise definition of abstraction.

Hopefully, I was able to show you the value of precision, and how sharp definitions leads to sharp thinking.

To finish this post, I want to make a rundown of the key takeaways we encountered throughout the post:

  • The essence of abstraction is information hiding, where through a process of encapsulation, abstractions preserve information that is relevant in a given context, while forgetting information that is irrelevant in that context.

  • Abstractions are composed of two parts: an interface and an implementation, where the implementation always implies the interface, but the converse is not true.

  • An abstraction is a construct that offers a simplified representation of itself, where this representation, the interface, exposes only a selected subset of the construct's properties, the implementation.

  • A property is any characteristic that can be attributed to something.

  • Information, in the context of abstraction, is essentially a set of properties.

  • Sets of properties may imply other properties, which is represented by the consequence relation \vdash, which is reflexive and transitive.

  • The deductive closure of a set of properties, is the set of all properties that are implied by the initial set. E.g. Γ={ϕΓϕ}\Gamma^* = \{\phi \mid \Gamma \vdash \phi \}.

  • ΔΓΓΔ\Delta \vdash \Gamma \leftrightarrow \Gamma^* \subseteq \Delta^*.

  • We can use \vdash to produce an ordering for deductive closures that lets us compare sets of properties in terms of how much information they have, where ΔΓ\Delta^* \vdash \Gamma^* means that Δ\Delta^* has more information than Γ\Gamma^*.

  • An abstraction can be formally defined as an ordered pair Γ,Δ\langle \Gamma, \Delta \rangle such that Γ\Gamma, called "the interface", and Δ\Delta, called "the implementation", are sets of properties such that ΔΓ\Delta \vdash \Gamma, or, alternatively, ΓΔ\Gamma^* \subseteq \Delta^*.

  • There are three personas associated with every abstraction: a client, who uses the abstraction, a designer that designs the interface and an implementer, that comes up with the implementation.

  • A use case, is a set Σ\Sigma of properties. For a use case to be compatible with an abstraction, its interface must imply that use case, that is, ΓΣ\Gamma \vdash \Sigma.

  • ΔΓΣ\Delta \vdash \Gamma \vdash \Sigma and ΣΓΔ\Sigma^* \subseteq \Gamma^* \subseteq \Delta^*.

  • For any interface Γ\Gamma, there is a set S(Γ)={ΣΓΣ}\mathcal{S}(\Gamma) = \{\Sigma \mid \Gamma \vdash \Sigma \} of all the use cases that are supported by Γ\Gamma and a set D={ΔΔΓ}\mathcal{D} = \{\Delta \mid \Delta \vdash \Gamma \} of all the implementations that are suitable for Γ\Gamma.

  • There is a permanent tension between abstraction and flexiblity, where the more abstracted you make an abstraction the less flexible it becomes, and vice versa.

  • As the interface grows, the more use cases it supports but the amount of suitable implementations decreases and the amount of information we need to know in order to use it increases. Γ1Γ2S(Γ1)S(Γ2)D(Γ1)D(Γ2)\Gamma_1^* \subseteq \Gamma_2^* \rightarrow \mathcal{S}(\Gamma_1) \subseteq \mathcal{S}(\Gamma_2) \wedge \mathcal{D}(\Gamma_1) \supseteq \mathcal{D}(\Gamma_2).

  • As the interface shrinks, the amount of suitable implementations increases, and the amount of information we need to know in order to use it decreases, but the fewer use cases it supports. Γ1Γ2S(Γ1)S(Γ2)D(Γ1)D(Γ2)\Gamma_1^* \supseteq \Gamma_2^* \rightarrow \mathcal{S}(\Gamma_1) \supseteq \mathcal{S}(\Gamma_2) \wedge \mathcal{D}(\Gamma_1) \subseteq \mathcal{D}(\Gamma_2).

  • A responsibility is something that a construct must do or know.

  • Responsibilities that are about doing something are part of an abstraction's interface and responsibilities that are about knowing something are part of an abstraction's implementation.

  • Stacking abstractions is a way to make sure that abstractions do not accumulate more responsibilities than they should.

The End.