An abstract class is a class that cannot be instantiated directly. That is, you cannot create objects directly from it.
To use an abstract class, you must inherit from it and provide a complete implementation for all the abstract methods defined in the abstract class.
Why would you want a class that cannot be instantiated? An abstract class is used to define a common structure that I can reuse through inheritance but is not ready to be used directly in code.
Ultimately, abstract classes are used to model abstract or generic concepts that do not have a concrete implementation but are the common foundation for other objects.
As you might be a bit 🤔 confused by the definition, let’s clarify it with a real-life example.
Purpose of Abstract Classes
Imagine you walk into a car dealership. You approach one of the salespeople, and you have the following conversation:
- Hello, I would like to buy a car.
+ Of course, what would you like to buy? A Sedan, a Coupe, or a Hatchback?
- No, I would like to buy a car “as a concept.”
Depending on how the salesperson is feeling that day, they will explain more or less kindly that you cannot buy a car “as a concept.” You can buy a particular subtype of car.
But a car “as a concept” is an abstraction. It is an idea that gathers the common characteristics of all types of cars. And ideas cannot be bought 🤷 (and then they would throw you out of the dealership).
In this case, Car
is an abstract class. It is an idea that represents everything cars have in common. And it cannot be instantiated.
Then it specializes into subclasses, such as Sedan
, Coupe
, Hatchback
, which are specializations of Car
, and you can instantiate them.
Methods and Abstract Classes
Abstract Methods
Abstract methods are declared methods, but they do not have an implementation in the class where they are declared (that is, they are basically methods that have no “body”, they are “empty”).
Since they have no implementation, no class that has an abstract method can be instantiated. Therefore, it will immediately become an abstract class.
If you try to instantiate an abstract class, the compiler or the IDE will give you an error, stating that an abstract class cannot be instantiated.
Abstract Classes
Abstract classes are those that cannot be instantiated directly, but are designed to be subclassed.
An abstract class can contain both abstract methods (without implementation) and concrete methods (with implementation).
But, as we mentioned, if a class implements at least one abstract method (without implementation), the class must be abstract.
Practical Example
Let’s see it with our little example of Car
, which is an abstract class. Therefore, it cannot be instantiated directly.
// Abstract class that represents a car
abstract class Car
{
// Abstract method, without implementation (no body)
void Drive();
}
// Trying to create an instance of the abstract class (this is not possible)
Car abstractCar = new Car(); // This will give a compilation error
What we can do is inherit from other classes that will provide a concrete implementation of their abstract methods.
// Class that represents a Sedan, derived from Car
class Sedan extends Car
{
void Drive()
{
Console.Log("Driving a Sedan");
}
}
// Class that represents a Coupe, derived from Car
class Coupe extends Car
{
void Drive()
{
Console.Log("Driving a Coupe");
}
}
// Create instances of the derived classes
Car mySedan = new Sedan();
Car myCoupe = new Coupe();
Examples in Different Languages
Let’s see examples of declaring abstract classes in different programming languages
Abstract classes in C# are defined using the abstract
keyword. These classes can contain abstract methods, which must be implemented by derived classes.
public abstract class Shape
{
// Abstract method
public abstract double CalculateArea();
// Concrete method
public void Display()
{
Console.WriteLine("Displaying shape");
}
}
public class Circle : Shape
{
public double Radius { get; set; }
public override double CalculateArea()
{
return Math.PI * Radius * Radius;
}
}
// Usage
Circle circle = new Circle { Radius = 5 };
Console.WriteLine(circle.CalculateArea());
circle.Display();
In C++, abstract classes are defined using at least one pure virtual function. A pure virtual function is defined with = 0
at the end of the function declaration.
#include <iostream>
#include <cmath>
class Shape {
public:
// Abstract method
virtual double CalculateArea() const = 0;
// Concrete method
void Display() const {
std::cout << "Displaying shape" << std::endl;
}
};
class Circle : public Shape {
public:
double Radius;
double CalculateArea() const override {
return M_PI * Radius * Radius;
}
};
// Usage
int main() {
Circle circle;
circle.Radius = 5;
std::cout << circle.CalculateArea() << std::endl;
circle.Display();
return 0;
}
JavaScript does not have native support for abstract classes, but we can simulate them “more or less” with different techniques.
class Shape {
// Optional constructor to initialize common properties
constructor() {
if (this.constructor === Shape) {
throw new Error("Cannot instantiate an abstract class.");
}
}
// Abstract method
CalculateArea() {
throw new Error("You must implement the abstract method CalculateArea");
}
// Concrete method
Display() {
console.log("Displaying shape");
}
}
class Circle extends Shape {
constructor(radius) {
super(); // Calls the base class constructor
this.radius = radius;
}
CalculateArea() {
return Math.PI * this.radius * this.radius;
}
}
// Usage
try {
let shape = new Shape(); // This will throw an error
} catch (error) {
console.error(error.message);
}
let circle = new Circle(5);
console.log(circle.CalculateArea());
circle.Display();
In TypeScript, abstract classes are defined using the abstract
keyword.
abstract class Shape {
// Abstract method
abstract calculateArea(): number;
// Concrete method
display(): void {
console.log("Displaying shape");
}
}
class Circle extends Shape {
radius: number;
constructor(radius: number) {
super();
this.radius = radius;
}
calculateArea(): number {
return Math.PI * this.radius * this.radius;
}
}
// Usage
const circle = new Circle(5);
console.log(circle.calculateArea());
circle.display();
In Python, abstract classes are defined using the abc
module and the ABC
class. Abstract methods are defined using the @abstractmethod
decorator.
from abc import ABC, abstractmethod
import math
class Shape(ABC):
@abstractmethod
def calculate_area(self):
pass
def display(self):
print("Displaying shape")
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def calculate_area(self):
return math.pi * self.radius ** 2
# Usage
circle = Circle(5)
print(circle.calculate_area())
circle.display()
Good Practices Tips
Like almost everything in OOP, the question you will have is when and to what extent to use something. In the case of abstract classes, it is to provide implementation for common methods that can be reused by subclasses.
For example, you have a DbConnection
object that manages database connections. That object has a certain internal logic; it already “does things.” But by itself, it cannot fully define itself.
To define itself, it needs to know more details, in this example about the database it is going to use. Thus, we will have DbConnectionMySQL
, DbConnectionPostgresSQL
, DbConnectionMongo
, or things like that.
Here we have a possible example of detecting an abstract class
- We have a class that already has logic
- By itself, it will not work
- It needs to specialize into different subtypes
So possibly DbConnection
should be an abstract class.
So far, the “good” part. But abstract classes also have their problems and detractors. The biggest problem is that they can complicate your object model (and your life with them) and end up with a huge mess 🐔.
When you program “classic” OOP, you run the risk of going crazy using abstract classes. So you end up having DbConnectionBase
, which uses DbRepositoryBase
, which uses DbObjectableBase
, which uses… all with “base.”
In summary, if you have to use an abstract class, use it. That’s what they are for; they are useful. But use them wisely, and aim for the coupling between classes and the levels of inheritance you are going to use.
And if you have doubts, prefer other solutions like composition or interfaces, which may not seem “as elegant” from the perspective of classic OOP, but will give you fewer problems in the future.