The previous lesson ended by pointing out an awkward detail: nothing stops you from writing new LibraryItem("Any title", "Any author") anywhere in the program, even though "a library item" on its own, that's neither a book nor a magazine, doesn't represent any real object in BiblioTech's catalog. LibraryItem was always meant as a common base, never as something that should exist by itself. Abstraction is the principle that lets you express this directly in the code: declaring that a class is purely conceptual, a template for its inheritors, and that it must never be instantiated on its own. In this lesson you'll turn LibraryItem into an abstract class, and its Describe() method into an abstract method that every inheritor is required to implement.

Contents

  1. The problem: instantiating a class that shouldn't exist on its own
  2. Abstract classes: the abstract keyword
  3. Abstract methods versus virtual methods
  4. Turning LibraryItem into an abstract class
  5. ShowDetails() remains virtual: the difference in practice
  6. When to use an abstract class
  7. Abstraction versus interfaces: a conceptual distinction

  1. The problem: instantiating a class that shouldn't exist on its own

With the current design, this code compiles and runs without any error:

LibraryItem genericItem = new LibraryItem("No title", "No author");
Console.WriteLine(genericItem.Describe()); // "Item: No title, by No author"

Nothing in the code prevents this, even though it doesn't make sense conceptually: in BiblioTech's real domain, everything that's lent out is always, specifically, a book or a magazine (or, in the future, some other specific type), never "an item" without further specification. LibraryItem exists solely so that Book and Magazine can share code; allowing it to be instantiated directly is a possibility the design should explicitly close off, not leave open by oversight.

  1. Abstract classes: the abstract keyword

A class declared with abstract cannot be instantiated directly with new; it can only be used as the base class for other (non-abstract) classes that inherit from it:

abstract class LibraryItem
{
    public string Title { get; set; }
    public string Author { get; set; }
    public bool Available { get; private set; } = true;

    public LibraryItem(string title, string author)
    {
        Title = title;
        Author = author;
    }

    public void Lend()
    {
        if (Available)
        {
            Available = false;
            Console.WriteLine($"'{Title}' has been lent out.");
        }
        else
        {
            Console.WriteLine($"'{Title}' is not available for loan.");
        }
    }

    public void Return()
    {
        Available = true;
        Console.WriteLine($"'{Title}' has been returned.");
    }
}
// LibraryItem genericItem = new LibraryItem("...", "...");
// Compilation error: cannot create an instance of the abstract class 'LibraryItem'

Book book1 = new Book("Hopscotch", "Julio Cortazar", "978-84-376-0495-4"); // Ok: Book is not abstract

Notice an important detail: even though LibraryItem can no longer be instantiated directly, it still has a constructor, and that constructor still runs — via base(title, author) — every time a Book or a Magazine is created. An abstract class can have constructors, fields, properties and fully implemented methods, exactly like a normal class; the only thing that changes is that it itself cannot become an independent object.

  1. Abstract methods versus virtual methods

Besides marking the whole class as abstract, you can mark a method as abstract. An abstract method declares its signature (name, parameters, return type) but has no body: there's no implementation at all in the base class, and every inheriting class is required to provide its own with override.

abstract class LibraryItem
{
    // ... properties, constructor, Lend(), Return() as before ...

    public abstract string Describe(); // no body, ends with ";"
}

This is different from virtual, which does require (or offer) a default implementation in the base class, and leaves overriding as optional for inheritors.

virtual abstract
Implementation in the base class Required (has a body) Forbidden (only the signature, ends in ;)
Overriding in the inheriting class Optional Required
Can be used in a non-abstract class Yes No; a class with any abstract member must itself be abstract
Example in BiblioTech ShowDetails() Describe()

If Book or Magazine didn't provide their own implementation of Describe(), the compiler would give an error: being abstract, there's no "default" version to fall back on.

  1. Turning LibraryItem into an abstract class

With these two changes (the abstract class and Describe() as an abstract method), here's the complete model of BiblioTech's item hierarchy:

abstract class LibraryItem
{
    public string Title { get; set; }
    public string Author { get; set; }
    public bool Available { get; private set; } = true;

    public LibraryItem(string title, string author)
    {
        Title = title;
        Author = author;
    }

    public void Lend()
    {
        if (Available)
        {
            Available = false;
            Console.WriteLine($"'{Title}' has been lent out.");
        }
        else
        {
            Console.WriteLine($"'{Title}' is not available for loan.");
        }
    }

    public void Return()
    {
        Available = true;
        Console.WriteLine($"'{Title}' has been returned.");
    }

    public virtual void ShowDetails()
    {
        Console.WriteLine($"Title: {Title}");
        Console.WriteLine($"Author: {Author}");
        Console.WriteLine($"Available: {Available}");
    }

    public abstract string Describe();
}

class Book : LibraryItem
{
    public string Isbn { get; set; }

    public Book(string title, string author, string isbn) : base(title, author)
    {
        Isbn = isbn;
    }

    public override string Describe()
    {
        return $"Book: {Title}, by {Author} (ISBN {Isbn})";
    }
}

class Magazine : LibraryItem
{
    public int IssueNumber { get; set; }

    public Magazine(string title, string author, int issueNumber) : base(title, author)
    {
        IssueNumber = issueNumber;
    }

    public override string Describe()
    {
        return $"Magazine: {Title}, issue number {IssueNumber}";
    }
}

The rest of the code you already wrote in the Polymorphism lesson — the LibraryItem[] array traversed with foreach, is, as — keeps working exactly the same: abstraction doesn't change how Book and Magazine objects are used, it only prevents a LibraryItem from being created "on its own."

  1. ShowDetails() remains virtual: the difference in practice

It's intentional that Describe() is abstract while ShowDetails() remains only virtual: these are two different situations that illustrate when to use each one.

  • ShowDetails() has a reasonable generic implementation (showing title, author and availability) that works as-is for any item, even though Book and Magazine enrich it by adding their own particular data. That's why it's virtual: it offers a useful default behavior that inheritors can extend if they want.
  • Describe(), on the other hand, has no reasonable implementation in LibraryItem: there's no generic sentence that makes sense for "any library item" without knowing whether it's a book or a magazine. That's why it's abstract: it forces each specific type to decide how to describe itself, without offering (or allowing) a default version that wouldn't fit any real case well.

This distinction — is there a reasonable default implementation, or isn't there? — is the key question for deciding whether a member should be virtual or abstract.

  1. When to use an abstract class

It's worth declaring a class as abstract when these conditions are all met at once:

  • The class represents a general concept that only makes sense through its concrete specializations (like LibraryItem, versus Book or Magazine).
  • You want to share common code (properties, methods with implementation) among several related classes, avoiding duplication — the same motivation you saw in the Inheritance lesson.
  • You want to force, at the compiler level, certain methods to be implemented in every concrete inheritor, without risking that someone forgets to do so.
Signal Abstract class?
The class should never be instantiated on its own, only its inheritors Yes
There's common behavior (with implementation) that several inheritors must share Yes
Some methods have no reasonable implementation without knowing the specific type Yes, mark those methods as abstract
All possible instances of this concept are, in practice, of the exact same type (no variants) No; a normal class is enough

  1. Abstraction versus interfaces: a conceptual distinction

C# offers another, closely related tool for expressing abstraction: interfaces (interface). The essential conceptual difference is this: an abstract class models an "is a" relationship with shared code inheritance (Book is a LibraryItem, and inherits its already-implemented Lend()/Return()), while an interface models a "can do" contract with no shared implementation and no single-base-class inheritance relationship. In addition, a class in C# can only inherit from one abstract class (recall the single inheritance from Module 3), but it can implement several different interfaces at once.

This module won't go deeper into interfaces: they're studied in detail, with all their syntax and use cases, in Module 4: Advanced C# Concepts, right in the first lesson. For now, keep in mind that abstract class and interface are two different tools for expressing abstraction, and that BiblioTech will use both later on, each where it makes the most sense.

Common Mistakes and Tips

  • Trying to instantiate an abstract class directly: new LibraryItem(...) never compiles, no matter what constructor it has defined; you can only create instances of its non-abstract inheriting classes (Book, Magazine).
  • Forgetting to implement an abstract method in an inheriting class: if Book doesn't define override string Describe(), the compiler gives an error, unless Book is also declared abstract (which would pass the obligation on to its own inheritor, instead of resolving it).
  • Giving a body to an abstract method: an abstract method always ends with ;, with no braces { }; if you need a default implementation (even a simple one), the right choice is virtual, not abstract.
  • Marking a class abstract with no abstract members at all: it's valid (it simply prevents direct instantiation), but if there's no member forcing something different to be implemented in each inheritor, it's worth asking whether it really needs to be abstract, or whether a normal class that simply isn't instantiated in practice would do.
  • Confusing an abstract class with an interface: if you need to share real implementation among classes related by inheritance, use an abstract class; if you only need to guarantee that several unrelated types offer certain methods, without sharing code, an interface (Module 4) usually fits better.

Exercises

  1. Declare LibraryItem as an abstract class, keeping its properties (Title, Author, Available with private set) and its constructor. Check that new LibraryItem("...", "...") causes a compilation error if you try it.

  2. Add to LibraryItem a method public abstract string Describe();, and provide its implementation with override in both Book (including the ISBN) and Magazine (including the issue number). Create an object of each class and show the result of Describe().

  3. Add a third inheriting class, Dvd : LibraryItem, with its own property DurationMinutes (int) and its own constructor with base(...). Implement Describe() so it returns, for example, "Dvd: <Title> (<DurationMinutes> min)". Add it to a LibraryItem[] array along with a Book and a Magazine, and traverse the array showing the result of Describe() for each one.

Solutions

abstract class LibraryItem
{
    public string Title { get; set; }
    public string Author { get; set; }
    public bool Available { get; private set; } = true;

    public LibraryItem(string title, string author)
    {
        Title = title;
        Author = author;
    }
}

// LibraryItem m = new LibraryItem("X", "Y");
// Compilation error: cannot create an instance of the abstract class
public abstract string Describe();

// In Book:
public override string Describe()
{
    return $"Book: {Title}, by {Author} (ISBN {Isbn})";
}

// In Magazine:
public override string Describe()
{
    return $"Magazine: {Title}, issue number {IssueNumber}";
}

Book book1 = new Book("Hopscotch", "Julio Cortazar", "978-84-376-0495-4");
Magazine magazine1 = new Magazine("National Geographic", "Various authors", 302);

Console.WriteLine(book1.Describe());
Console.WriteLine(magazine1.Describe());
class Dvd : LibraryItem
{
    public int DurationMinutes { get; set; }

    public Dvd(string title, string author, int durationMinutes) : base(title, author)
    {
        DurationMinutes = durationMinutes;
    }

    public override string Describe()
    {
        return $"Dvd: {Title} ({DurationMinutes} min)";
    }
}

LibraryItem[] catalog = new LibraryItem[]
{
    new Book("Hopscotch", "Julio Cortazar", "978-84-376-0495-4"),
    new Magazine("National Geographic", "Various authors", 302),
    new Dvd("The Godfather", "Francis Ford Coppola", 175)
};

foreach (LibraryItem item in catalog)
{
    Console.WriteLine(item.Describe());
}

This exercise confirms, in practice, the benefit of combining polymorphism with abstraction: the foreach loop doesn't change at all when Dvd is added as a third material type, and each one describes itself correctly according to its own implementation.

Conclusion

In this lesson you've finished closing the design of BiblioTech's item hierarchy: LibraryItem is now an abstract class that cannot be instantiated directly, with Describe() as an abstract method that forces every concrete inheritor (Book, Magazine) to define its own description, while ShowDetails() remains virtual for having a reasonable default implementation. You've also seen the conceptual difference between an abstract class and an interface, which you'll study in detail in Module 4.

This completes the object-oriented design of BiblioTech's domain model: LibraryItem (abstract) with Book and Magazine as concrete inheritors, Member and Loan rounding out the rest. There's one last piece left to resolve in this module: so far, everything you've modeled is classes, that is, reference types. In the next lesson, the last in the module, you'll meet structs and records, lighter alternatives for small, immutable data (such as a loan date or a member's address), and you'll understand when to choose each one over a traditional class.

© Copyright 2026. All rights reserved