Abstractions

Abstraction is a fundamental concept in computer programming that refers to the process of simplifying complex systems or ideas by focusing on the essential details and ignoring unnecessary details. In other words, you use abstraction when creating a model or representation of a system that exposes what you need to know and abstracts away implementation detail that is not important to every consumer of that specific piece of code.
Abstraction is achieved by defining abstract classes or interfaces that specify a set of methods and properties that a concrete class must implement. This allows the concrete class to implement its own specific behavior while still adhering to the contract specified by the abstract class or interface. In other words, you have abstractions that describes something and imposes rules but doesn’t really implements anything. And you have the concrete implementation, that might stand on it’s own or be bound by what what an abstraction has defined.
Abstraction is used to simplify programming tasks and reduce the complexity of code. It makes it easier to reason about a system, as developers can focus on the high-level design rather than the low-level implementation details. Abstraction is a key concept in Object-Oriented Programming (OOP), and is often used in conjunction with encapsulation and inheritance to create robust and maintainable code.

Interfaces

Here are a few examples where interfaces can be used to create an abstraction:
UI components: UI libraries often define interfaces for components, such as buttons or menus, that are used to define a common set of properties and methods that are shared by all instances of that component.
Database access: Interfaces can be used to define a common API for accessing different types of databases, such as relational or NoSQL databases, making it easier to switch between them.
Networking: Interfaces can be used to define a common set of methods and properties for interacting with different network protocols, such as HTTP or WebSockets.
Let’s take the example of UI components and expand it to show some example code:
interface UIComponent {
  id: string;
  isVisible: boolean;
  render(): void;
  destroy(): void;
}
In this example, UIComponent is an interface that defines a common set of properties and methods for UI components. It has an id property, an isVisible property, and two methods, render and destroy.
Here’s an example of a Button class that implements the UIComponent interface:
class Button implements UIComponent {
  id: string;
  isVisible: boolean;
  label: string;

  constructor(id: string, isVisible: boolean, label: string) {
    this.id = id;
    this.isVisible = isVisible;
    this.label = label;
  }

  render() {
    // Render the button HTML
  }

  destroy() {
    // Remove the button from the UI
  }
}
By defining the UIComponent interface, we can ensure that all UI components in our application have a common set of properties and methods, making it easier to work with and maintain our UI code. The Button class can be used alongside other UI components that also implement the UIComponent interface.

Abstract classes

Sometimes you need something more than what interfaces can offer, this is when the abstract classes that we talked about earlier come in handy.
Interfaces cannot provide any implementation details. They simply define the structure of the contract that a class must adhere to. Interfaces are useful when you want to define a contract that multiple classes can implement, regardless of their inheritance hierarchy. Abstract classes can provide implementation details in addition to the contract they define. This means that you can provide some common concrete implementation for all the classes that inherit from the abstract class.
One example of how abstract classes can be used is how the framework NestJS provides the abstraction for HTTP adapters. NestJS is a framework that can, among other things, expose HTTP APIs using different HTTP server implementations, such as express or fastify. To accomplish that an AbstractHttpAdapter is provided and extended by the express and fastify HTTP adapters.
Here is a simplified version of AbstractHttpAdapter:
export abstract class AbstractHttpAdapter {
  protected httpServer: TServer;

  public getHttpServer(): TServer {
    return this.httpServer as TServer;
  }

  public setHttpServer(httpServer: TServer) {
    this.httpServer = httpServer;
  }

  abstract initHttpServer(options: NestApplicationOptions);
}
In this example you can see that it provides a getHttpServer and a setHttpServer method, and both are concrete methods. Meaning they have an implementation that should be the same for all sub-classes. But it also has an abstract initHttpServer that needs to be overriden by the sub-classes making it specific for that particular adapter.
I tried to bring a more practical example this time as we will start using the theory we have learned so far to implement things more useful than a boring generic Person class that has a name and an age. Hopefully a more practical approach will also help fixate any concepts that might not be very clear for you so far and make you more confident in applying these to your projects.