9

I was reading this wiki on the Stable Abstractions Principle (SAP).

The SAP states that the more stable a package the more abstract it should be. This implies that if a package is less stable (more likely to change) then it should be more concrete. What I don't really understand is why this should be the case. Surely in all cases regardless of stability we should be depending upon abstractions and hiding the concrete implementation?

maple_shaft
  • 26,570

5 Answers5

7

Think of your packages as an API, to take the example from the paper, take definitions for Reader with string Reader.Read() and Writer with void Writer.Write(string) as your abstract API.

You can then create a class Copy with a method Copier.Copy(Reader, Writer) and the implementation Writer.Write(Reader.Read()) and maybe some sanity checks.

Now, you make concrete implementations, e.g. FileReader, FileWriter, KeyboardReader and DownloadThingsFromTheInternetReader.

What if you want to change your implementation of FileReader? No problem, just change the class and recompile.

What if you want to change the definition of your abstraction Reader? Oops, you cannot just change that, but you will also have to change Copier, FileReader, KeyboardReader and DownloadThingsFromTheInternetReader.

This is the rationale behind the Stable Abstraction Principle: Make your concretisations less stable than the abstractions.

Residuum
  • 3,332
  • 31
  • 31
7

I think you're perhaps confused by the word stable chosen by Robert Martin. Here's where I think the confusion starts:

This implies that if a package is less stable (more likely to change) then it should be more concrete.

If you read through to the original article, you'll see (emphasis mine):

The classic definition of the word stability is:”Not easily moved.” This is the definition that we will be using in this article. That is, stability is not a measure of the likelihood that a module will change; rather it is a measure of the difficulty in changing a module.

Clearly, modules that are more difficult to change, are going to be less volatile. The harder the module is to change, i.e. the more stable it is, the less volatile it will be.

I have always struggled with the author's choice of the word stable, as I (like you) tend to think of the "likelihood" aspect of stability, i.e., unlikely to change. Difficulty implies that changing that module will break a lot of other modules, and it's going to be a lot of work to fix the code.

Martin also uses the words independent and responsible, which to me convey much more meaning. In his training seminar, he used a metaphor about parents of children growing up, and how they should be "responsible," because their children depend on them. Divorce, unemployment, incarceration, etc. are great real-world examples of the negative impact that change in parents will have on children. Therefore, parents should be "stable" for the benefit of their kids. By the way, this metaphor of children/parents is not necessarily related to inheritance in OOP!

So, following the spirit of "responsible" I came up with alternative meanings for difficult to change (or should not change):

  • Obligated - meaning other classes depend on this class so it shouldn't change.
  • Beholden - ibid.
  • Constrained - the obligations of this class limit its facility in changing.

So, plugging these definitions into the statement

the more stable a package the more abstract it should be

  • the more obligated a package the more abstract it should be
  • the more beholden a package the more abstract it should be
  • the more constrained a package the more abstract it should be

Let's cite the Stable Abstractions Principle (SAP), emphasizing the confusing words stable/unstable:

Packages that are maximally stable should be maximally abstract. Unstable packages should be concrete. The abstractness of a package should be in proportion to its stability.

Clarifying it without these confusing words:

Packages that are maximally beholden to other parts of the system should be maximally abstract. Packages that can change without difficulty should be concrete. The abstractness of a package should be in proportion to how difficult it will be to modify.

TL;DR

The title of your question asks:

Are there any significant disadvantages to depending upon abstractions?

I think if you create the abstractions properly (e.g., they exist because a lot of code depends on them), then there aren't any significant disadvantages.

Fuhrmanator
  • 1,475
6

Because of YAGNI.

If you currently have only one implementation of one thing, why bothering with an extra and useless layer ? It will only lead to unnecessary complexity. Even worse, sometimes you provide an abstraction thinking to the day a second implementation will come... and this day never happens. What a waste of work !

I also think the real question to ask itself is not "Do I need to depend on abstractions ?" but rather "Do I need modularity ?". And modularity is not always needed, see below.

In the company I am working, some of the softwares I develop are strongly tied to some hardware device with which it must communicate. These devices are developed to fulfill very specific goals and are everything but modular. :-) Once the first produced device goes out of the factory and is installed somewhere, both its firmware and hardware can never change, ever.

So, I can be sure that some parts of the software will never evolve. These parts don't need to depend on abstractions since it exists only one implementation and this one will never change. Declaring abstractions on these parts of code will only confuse everyone and take more time (without producing any value).

Spotted
  • 1,690
0

This implies that if a package is less stable (more likely to change) then it should be more concrete. What i don't really understand is why this should be the case.

Abstractions are things that are hard to change in the software because everything depend on them. If your package is going to change often and it provides abstractions, people who depend on it will be forced to rewrite a big bunch of their code when you change something. But if your unstable package provides some concrete implementations, much lesser code will have to be rewritten after changes.

So, if your package is going to change often, it should better provide concretes, not abstractions. Otherwise... who the hell will use it? ;)

0

Keep in mind Martin's stability metric and what he means by "stability":

Instability = Ce / (Ca+Ce)

Or:

Instability = Outgoing / (Incoming+Outgoing)

That is, a package is considered completely unstable if all of its dependencies are outgoing: it uses other things, but nothing uses it. In that case, it only makes sense for that thing to be concrete. It's also going to be the easiest kind of code to change since nothing else uses it, and therefore nothing else can break if that code is modified.

Meanwhile when you have the opposite scenario of complete "stability" with a package used by one or more things but it doesn't use anything on its own, like a central package used by the software, that is when Martin says this thing should be abstract. That is also reinforced by the DIP part of SOLI(D), the Dependency Inversion Principle, which basically states that dependencies should uniformly flow towards abstractions for both low and high-level code.

That is, dependencies should uniformly flow towards "stability", and more precisely, dependencies should flow towards packages with more incoming dependencies than outgoing dependencies and, furthermore, dependencies should flow towards abstractions. The gist of the rationale behind that is that abstractions provide breathing room to substitute one subtype for another, offering that degree of flexibility for the concrete parts implementing the interface to change without breaking the incoming dependencies to that abstract interface.

Are there any significant disadvantages to depending upon abstractions?

Well, I actually disagree with Martin here for my domain at least, and here I need to introduce a new definition of "stability" as in, "lacking reasons to change". In that case I would say dependencies should flow towards stability, but abstract interfaces do not help if abstract interfaces are unstable (by my definition of "unstable", as in prone to repeatedly being changed, not Martin's). If the developers cannot get the abstractions correct and clients repeatedly change their mind in ways that render abstract attempts to model the software incomplete or ineffective, then we no longer benefit from the enhanced flexibility of abstract interfaces to protect the system against cascading dependency-breaking changes. In my personal case I've found ECS engines, such as those found in AAA games, to be among the most stable engines I've ever worked on along with the most flexible against changing design needs and, in an ECS, the dependencies flow towards the most concrete: towards raw data, but such data is highly stable (as in, "unlikely to ever need to be changed"). I've often found the probability of something requiring future changes to be a more useful metric than the ratio of efferent to total couplings in guiding SE decisions.

So I would alter DIP a bit and just say, "dependencies should flow towards components that have the lowest probability of requiring further changes", regardless of whether those components are abstract interfaces or raw data. All that matters to me is the probability that they might require direct design-breaking changes. Abstractions are only useful in this context of stability if something, by being abstract, reduces that probability.

For many contexts that might be the case with decent engineers and clients who anticipate the needs of the software upfront and design stable (as in, unchanging) abstractions, while those abstractions offer them all the breathing room they need to swap out concrete implementations. But in some domains, the abstractions might be unstable and prone to be inadequate, while the data required of the engine might be much easier to anticipate and make stable in advance. So in those cases, it can actually be more beneficial from a maintainability standpoint (the ease of changing and extending the system) for dependencies to flow towards data rather than abstractions. In an ECS, the most unstable parts (as in parts most frequently changed) are typically the functionality residing in systems (PhysicsSystem, e.g.), while the most stable parts (as in least likely to be changed) are the components which just consist of raw data (MotionComponent, e.g.) which all the systems use.