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.