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.