Vlad's Roam Garden

Powered by 🌱Roam Garden

Respecting level of abstraction

Respecting levels of abstraction means that all the code in a given block (a given function, an interface, an object, or an implementation) is at the same abstraction level. Said differently, abstraction levels are respected if, at a given level, there isn’t any code coming from another level of abstraction.

It's one of the core insights for writing clean code it underlies a lot of other principles

[[functions should do one thing]]

[[all code in the function should be at the same level of abstraction]]

[[Choose Names at the Appropriate level of abstraction]]

extracting variables works a bit less well then extracting functions - the details are still close by

[[cohesion is having only one abstraction at a given place]]

[[good code/names should be consistent with the level of abstraction they are used in]]

It's a pretty good introduction to the general idea.

Therefore, breaking encapsulation means providing information that goes beyond the abstraction level of the interface.

polymorphism consists of segregating levels of abstraction.

Indeed, for a given interface (or abstract class) and concrete implementation, the base class is abstract and the implementation is less abstract, because the base class describes what it does as an interface, while the implementation code describes how it does it.

Note that the derived class is still somewhat abstract since it is not expressed in terms of 0 and 1, but it is at an inferior level of abstraction compared to the base class. The base class represents what the interface offers, and the derived class represents how it is implemented:

abstraction

It is important to create abstractions that separate higher level general concepts from lower level detailed concepts. Sometimes we do this by creating abstract classes to hold the higher level concepts and derivatives to hold the lower level concepts. When we do this, we need to make sure that the separation is complete. We want all the lower level concepts to be in the derivatives and all the higher level concepts to be in the base class.
For example, constants, variables, or utility functions that pertain only to the detailed implementation should not be present in the base class. The base class should know nothing about them.
This rule also pertains to source files, components, and modules. Good software design requires that we separate concepts at different levels and place them in different containers. Sometimes these containers are base classes or derivatives and sometimes they are source files, modules, or components. Whatever the case may be, the separation needs to be complete. We don’t want lower and higher level concepts mixed together.
Consider the following code:
   public interface Stack {
     Object pop() throws EmptyException;
     void push(Object o) throws FullException;
     double percentFull();

     class EmptyException extends Exception {}
     class FullException extends Exception {}
   }

The percentFull function is at the wrong level of abstraction. Although there are many implementations of Stack where the concept of fullness is reasonable, there are other implementations that simply could not know how full they are. So the function would be better placed in a derivative interface such as BoundedStack.
Perhaps you are thinking that the implementation could just return zero if the stack were boundless. The problem with that is that no stack is truly boundless. You cannot really prevent an OutOfMemoryException by checking for
stack.percentFull() < 50.0
Implementing the function to return 0 would be telling a lie.
The point is that you cannot lie or fake your way out of a misplaced abstraction. Isolating abstractions is one of the hardest things that software developers do, and there is no quick fix when you get it wrong.

Hiding implementation is not just a matter of putting a layer of functions between the variables. hiding implementation is about abstractions! A class does not simply push its variables out through getters and setters. Rather it exposes abstract interfaces that allow its users to manipulate the essence of the data, without having to know its implementation.