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
- The problem: instantiating a class that shouldn't exist on its own
- Abstract classes: the
abstractkeyword - Abstract methods versus virtual methods
- Turning
LibraryIteminto an abstract class ShowDetails()remainsvirtual: the difference in practice- When to use an abstract class
- Abstraction versus interfaces: a conceptual distinction
- 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.
- Abstract classes: the
abstract keyword
abstract keywordA 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 abstractNotice 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.
- 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.
- Turning
LibraryItem into an abstract class
LibraryItem into an abstract classWith 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."
ShowDetails() remains virtual: the difference in practice
ShowDetails() remains virtual: the difference in practiceIt'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 thoughBookandMagazineenrich it by adding their own particular data. That's why it'svirtual: it offers a useful default behavior that inheritors can extend if they want.Describe(), on the other hand, has no reasonable implementation inLibraryItem: 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'sabstract: 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.
- 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, versusBookorMagazine). - 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 |
- 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
abstractclass 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
abstractmethod in an inheriting class: ifBookdoesn't defineoverride string Describe(), the compiler gives an error, unlessBookis also declaredabstract(which would pass the obligation on to its own inheritor, instead of resolving it). - Giving a body to an
abstractmethod: anabstractmethod always ends with;, with no braces{ }; if you need a default implementation (even a simple one), the right choice isvirtual, notabstract. - Marking a class
abstractwith noabstractmembers 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 beabstract, 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
-
Declare
LibraryItemas anabstract class, keeping its properties (Title,Author,Availablewithprivate set) and its constructor. Check thatnew LibraryItem("...", "...")causes a compilation error if you try it. -
Add to
LibraryItema methodpublic abstract string Describe();, and provide its implementation withoverridein bothBook(including the ISBN) andMagazine(including the issue number). Create an object of each class and show the result ofDescribe(). -
Add a third inheriting class,
Dvd : LibraryItem, with its own propertyDurationMinutes(int) and its own constructor withbase(...). ImplementDescribe()so it returns, for example,"Dvd: <Title> (<DurationMinutes> min)". Add it to aLibraryItem[]array along with aBookand aMagazine, and traverse the array showing the result ofDescribe()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.
C# Programming Course
Module 1: Introduction to C#
- Introduction to C#
- Setting Up the Development Environment
- Hello World Program
- Basic Syntax and Structure
- Variables and Data Types
- Arrays and Strings
Module 2: Control Structures
Module 3: Object-Oriented Programming
- Classes and Objects
- Methods
- Constructors and Destructors
- Inheritance
- Polymorphism
- Encapsulation
- Abstraction
- Structs and Records: Value Types and Reference Types
Module 4: Advanced C# Concepts
- Interfaces
- Delegates and Events
- Pattern Matching and Modern C# Features
- Generics
- Collections
- LINQ (Language Integrated Query)
- Asynchronous Programming
Module 5: Working with Data
- File I/O
- Serialization
- Database Connectivity
- Entity Framework
- Working with JSON and Consuming REST APIs
Module 6: Advanced Topics
- Reflection
- Attributes
- Dynamic Programming
- Memory Management and Garbage Collection
- Multithreading and Parallel Programming
Module 7: Building Applications
Module 8: Best Practices and Design Patterns
- Coding Standards and Best Practices
- Design Patterns
- Dependency Injection and Inversion of Control
- Unit Testing
- Code Review and Refactoring
