Abstract Classes: Powerful Tools for Extensible OOP Design

abstract classes banner overcoded

Abstract classes are types of programming architectures that allow developers to define broad behaviors that can be applied to more specific constructs. In object-oriented programming, Abstract classes are used to support polymorphism and inheritance among superclass and subclass objects.

Depending on the language, abstract classes may be called protocols, interfaces, or a myriad of other terminologies used for differentiating object types. Abstract classes can be fully or partially abstract and allow developers a range of flexibility and options for supporting quality object-oriented design principles.

What Are Abstract Classes

abstract class UML diagram overcoded
UML Diagram showing an partially abstract Shape class and subclassed Square classs

In practical terms, abstract classes are like molds for creating a very basic structure of an object. The details, such as methods, fields, and related polymorphic relationships, can be a range across many subclassed objects.

For example, an abstract Shape class might broadly define a geometric model as requiring a method to return the number of sides. All subclassed objects, such as Square, Triangle, or Dodecahedron would implement this method in a way that other developers could depend on it to return the number of sides for that particular object (i.e. Square.getSideCount() == 4; // true).

When to Use Abstract Classes

Abstract classes are better suited for use among similar subclass objects. For example, an application that models several geometric shapes such as Square, Rectangle, Rhomboid, and Dodecahedron would be better served through the subclassing of a Shape abstract class rather than an interface. Interfaces are better suited for wide implementation by unrelated classes. For example, Serializable, Clonable, Printable, etc.

Abstract classes are better suited for use among similar subclasses that need to implement methods and fields using more restrictive access modifiers such as private, package-private, or protected. By contrast, all abstract, static, and default methods in Interfaces implicitly have public access.

Abstract classes are better suited when a developer wishes to declare non-static or non-final fields. This allows subclasses to modify such values via specific implementation whereas Interfaces fields are public static final—the equivalent of a constant.

Partially abstract classes offer a high degree of dynamism and flexibility. These implementations can become burdensome to maintain once attributes and methods start mixing accessor restriction levels or inheriting from multiple sources. Many modern IDEs, like IntelliJ by Jetbrains, provide some degree of helpful prompting.

intellij abstract subclass nag overcoded
Modern IDEs like IntelliJ by Jetbrains can help spot implementation issues when utilizing abstract classes

Example Code of Abstract Class

Below is example code of how one might implement a Shape abstract superclass from which a Square concretion can be subclassed. Note the mixed-use of abstract and non-abstract methods here. This is an approach best suited for abstract classes, rather than interfaces, since Shape and Square share so much in common.

package AbstractClasses;

/**
 * Abstract class implantation that requires certain attributes to be
 * present in all subclass objects, specific method implementations, and
 * some abstract methods to be implemented specifically by subclassing objects.
 */
public abstract class Shape {

    // Define required fields for all Shape type objects
    public int sides;
    public int width;
    public int length;

    /**
     * Define the constructor class requiring a number of sides to be defined.
     * @param sides - int number of sides for a Geometry.
     */
    Shape(int sides, int length, int width){
        this.sides = sides;
        this.length = length;
        this.width = width;
    };

    /**
     * Abstract method to return the number of sides an
     * object is defined as having.
     * @return int - number of sides.
     */
    public int getSides(){
        return this.sides;
    }

    /**
     * Return the calculated area of a shape.
     * @return float - area of the shape.
     */
    public abstract float getArea();
}

/**
 * Subclass of Shape defining the behavior of a Square geometry.
 */
class Square extends Shape{
    
    /**
     * Define the constructor class requiring a number of sides to be defined.
     * @param sides  - int number of sides for a Geometry.
     * @param width - float value of shape width and length (same for squares)
     */
    Square(int sides, int width) {
        super(sides, width, width);
    }

    /**
     * Class-specific implementation to calculate the area
     * of a square using length x width formula.
     * @return - float value of Square area.
     */
    @Override
    public float getArea() {
        return this.width * this.width;
    }
}

This code illustrates the mixed-use of abstract and concrete methods along with publicly accessible fields. The Square subclass extends the Shape superclass, being required to implement a custom getArea() method specific to the formula used to calculate the area of a Square (width ^ 2).

Note the custom use of the Square constructor here as well — while still making use of the signature required by the Shape superclass. This implements a somewhat contrived requirement set forth in the Superclass that all Shape-type objects require a length and width measurement.

Abstract Classes vs. Interfaces

In some popular programming languages like Python, there are mechanisms in place to support multiple inheritance of partially abstract classes. This allows a subclass to inherit from multiple superclasses with both abstract and implemented methods.

In other languages like Java, multiple inheritance is not strictly allowed. I say “strictly” because one can implement multiple interfaces in subclassed objects. Interfaces are very much like fully abstract classes in that none of their methods are implemented (1).

Programming languages like Java also differentiate abstract classes from Interfaces in several other notable, yet subtle ways. For example, Java allows fields to have access restrictions other than public static final which are required for interfaces. This allows more flexibility at the cost of complexity.

TL;DR—Essentially, the developers of Java decided that if one chooses multiple inheritances it must be from fully abstract superclasses.

Final Thoughts

Abstract classes are foundational to the polymorphic and inheritance aspects of modern object-oriented design. Proper implementation of components such as interfaces and abstract base classes work to allow robust use of design patterns, such as the Observer.

I remember thinking how simple abstract classes, inheritance, polymorphic behavior, and related object-oriented concepts were when first taught. That feeling faded quickly once I started becoming aware of the seemingly endless gotchas, edge-cases, and variety of contexts in which these concepts could apply.

Abstract classes are powerful tools to leverage in Object-oriented design but must be studied with care, applied with forethought, and perpetually reconsidered/refactored to ensure implementations that will avoid embarrassment during one’s next code review. Check out our ever-evolving list of the most popular computer science and programming books for some great references!

References

  1. Oracle. “The Java Tutorials – Abstract Methods and Classes.” Oracle, 18 Mar. 2014, docs.oracle.com/javase/tutorial/java/IandI/abstract.html.
Zαck West
Full-Stack Software Engineer with 10+ years of experience. Expertise in developing distributed systems, implementing object-oriented models with a focus on semantic clarity, driving development with TDD, enhancing interfaces through thoughtful visual design, and developing deep learning agents.