Module 3 ended with a promise left unfulfilled: in the Abstraction lesson you saw that abstract class and interface are "two different tools for expressing abstraction," and that interfaces would be studied in detail in this module. That moment has arrived. An interface is a contract: a list of members (methods, properties, events) that a class commits to implementing, without the interface itself contributing any code. This lesson opens Module 4, Advanced Concepts, by defining two interfaces for BiblioTech — ILendable and ISearchable — and applying them to the domain model you already know, as a first step toward the rest of the modern C# tools you'll see in this module.

Contents

  1. What an interface is and what problem it solves
  2. Interfaces versus abstract classes: revisiting the distinction from Module 3
  3. Defining and implementing an interface: ILendable
  4. Implementing multiple interfaces
  5. A second interface: ISearchable
  6. Members with a default implementation (C# 8 onward)
  7. BiblioTech's model with interfaces applied

  1. What an interface is and what problem it solves

An interface declares what a type must be able to do, without saying how it does it. It's defined with the interface keyword, by convention with the prefix I (ILendable, ISearchable, or the already familiar IEnumerable, IComparable from the .NET library itself), and it lists members with no implementation body at all:

interface ILendable
{
    bool Available { get; }
    void Lend();
    void Return();
}

Any class that declares : ILendable commits, to the compiler, to offering an Available property (at least readable), and the methods Lend() and Return() with exactly those signatures. If any of them is missing, the code doesn't compile. The interface itself can't be instantiated (new ILendable() makes no sense and doesn't compile), nor does it contain any field with its own state: it's pure form, no content.

The problem it solves is decoupling the contract from the implementation: code that only needs to know that something "can be lent" can work with the ILendable interface without caring whether, underneath, there's a Book, a Magazine, or any future type that doesn't even exist yet, as long as it fulfills the contract.

  1. Interfaces versus abstract classes: revisiting the distinction from Module 3

In the Abstraction lesson you saw that an abstract class models an "is a" relationship with shared code inheritance, while an interface models a "can do" contract with no shared implementation at all. Now that interfaces are formally defined, this table captures the full comparison:

Abstract class (abstract class) Interface (interface)
Relationship it models "Is a" (Book is a LibraryItem) "Can do" (Book can be lent)
Shared implementation Yes, in non-abstract members No, except default members (section 6)
Its own constructor Yes No
Fields with state Yes No
How many a class can combine A single base class Several interfaces at once
Example in BiblioTech LibraryItem ILendable, ISearchable

The practical rule for choosing between the two: if you need to share real code (already implemented properties, methods with a body) among classes related by inheritance, use an abstract class; if you only need to guarantee that several types — related to each other or not — offer certain capabilities, without sharing any implementation, use an interface. In fact, the two tools aren't mutually exclusive: in the next section you'll see that LibraryItem remains an abstract class and, in addition, implements interfaces.

  1. Defining and implementing an interface: ILendable

Implementing an interface consists of declaring it after the class name (after the base class, if there is one, separated by commas) and making sure all its members are covered by public members with the same signature:

abstract class LibraryItem : ILendable
{
    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 lending.");
        }
    }

    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();
}

Notice an important detail: LibraryItem already had Available, Lend(), and Return() since Module 3, with exactly the shape ILendable requires. There was no need to write a single line of new code to "satisfy" the interface: it was enough to add : ILendable, and the compiler checks that the already-existing members match the contract. This is common: interfaces often formalize capabilities a class already had, giving them an explicit, reusable name.

Book book1 = new Book("Hopscotch", "Julio Cortazar", "978-84-376-0495-4");

ILendable lendable = book1; // valid: Book is-a LibraryItem, which is-an ILendable
lendable.Lend();
Console.WriteLine(lendable.Available); // False

A variable of an interface type (ILendable lendable) can only use the members declared in the interface (Available, Lend(), Return()), even though the underlying object is a Book with many more members of its own (Isbn, Describe()...); to access those other members you'd need to convert back to Book, with is/as, as you already saw in the Polymorphism lesson.

  1. Implementing multiple interfaces

Unlike class inheritance (a class can only have one direct base class), a class can implement several interfaces at once, separated by commas:

class Example : BaseClass, ILendable, ISearchable, IComparable<Example>
{
    // must fulfill the contract of all three interfaces, in addition to inheriting from BaseClass
}

This is one of the practical advantages most often cited for interfaces over abstract classes: they let you combine capabilities that are independent of one another (something can be, at the same time, "lendable," "searchable," and "comparable") without forcing a single, rigid inheritance hierarchy. In the next section, LibraryItem will go on to implement two different interfaces at the same time.

  1. A second interface: ISearchable

To complete the example, a second interface is defined, intended for searches across BiblioTech's catalog (a capability you'll use more fully in the LINQ lesson, later in this module):

interface ISearchable
{
    bool Matches(string text);
}

LibraryItem implements both interfaces at once, and declares Matches() as virtual (it's not part of the interface's contract that a method be virtual, but nothing prevents it: it's a design decision so that Book and Magazine can enrich the search with their own data):

abstract class LibraryItem : ILendable, ISearchable
{
    // ... Title, Author, Available, constructor, Lend(), Return(), ShowDetails() ...

    public abstract string Describe();

    public virtual bool Matches(string text)
    {
        return Title.Contains(text, StringComparison.OrdinalIgnoreCase)
            || Author.Contains(text, StringComparison.OrdinalIgnoreCase);
    }
}

Book overrides Matches() so the search also takes the ISBN into account, reusing the base class's implementation with base.Matches(text):

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})";
    }

    public override bool Matches(string text)
    {
        return base.Matches(text) || Isbn.Contains(text, StringComparison.OrdinalIgnoreCase);
    }
}
Book book1 = new Book("Hopscotch", "Julio Cortazar", "978-84-376-0495-4");

Console.WriteLine(book1.Matches("cortazar"));       // True: matches on Author
Console.WriteLine(book1.Matches("978-84-376-0495")); // True: matches on Isbn
Console.WriteLine(book1.Matches("harry potter"));    // False

  1. Members with a default implementation (C# 8 onward)

Since C# 8, an interface can optionally include a default implementation for one of its members, using the same { } (or =>) body as in a class. Any class implementing the interface inherits that behavior automatically, without having to write it, although it can still override it if needed:

interface ILendable
{
    bool Available { get; }
    void Lend();
    void Return();

    string StatusText() => Available ? "Available" : "Lent out"; // member with a default body
}
Book book1 = new Book("Hopscotch", "Julio Cortazar", "978-84-376-0495-4");
ILendable lendable = book1;

Console.WriteLine(lendable.StatusText()); // "Available", without LibraryItem implementing it

This feature is used mainly when an already-published library needs to add a new member to an existing interface without breaking the code of those who already implemented it: thanks to the default body, old classes keep compiling with no changes. In everyday application code (like BiblioTech's) it's a tool you'll see infrequently; it's enough to know it exists and recognize it if you see it in someone else's code.

  1. BiblioTech's model with interfaces applied

With the changes in this section, here's how LibraryItem looks now, implementing both interfaces, with nothing changing in how Book and Magazine are used from the rest of the program:

classDiagram
    class ILendable {
        <<interface>>
        +bool Available
        +Lend()
        +Return()
    }
    class ISearchable {
        <<interface>>
        +Matches(string) bool
    }
    class LibraryItem {
        <<abstract>>
        +string Title
        +string Author
        +Describe()* string
    }
    class Book {
        +string Isbn
    }
    class Magazine {
        +int IssueNumber
    }
    ILendable <|.. LibraryItem
    ISearchable <|.. LibraryItem
    LibraryItem <|-- Book
    LibraryItem <|-- Magazine

The dashed arrow (<|..) is the usual notation for "implements an interface," distinct from the solid arrow (<|--) for class inheritance you already know from Module 3.

Common Mistakes and Tips

  • Trying to give a body to an interface member without using default-member syntax: void Lend(); ends in ;, just like an abstract method; if you write { } directly in the interface without meaning to, you're actually using (from C# 8 onward) a default member, with the implications from section 6.
  • Forgetting to implement one of the interface's members: if a class declares : ILendable but is missing, say, Return(), the compiler raises an error pointing to exactly which member is missing.
  • Confusing the variable's type with the object's actual type: ILendable lendable = book1; only allows access to ILendable's members; to use Isbn or Describe() you need to convert back to Book with is/as.
  • Overusing interfaces with a single method: if only one class implements the interface and there's no reason to decouple the contract from the implementation, a regular method is sometimes enough; interfaces provide more value the more distinct implementations can share the same contract.
  • Tip: always name interfaces with the I prefix (ILendable, ISearchable, IComparable); it's a very well-established convention in C# and throughout the .NET library, and it makes it easy to recognize at a glance that a type is an interface.

Exercises

  1. Define the interface ILendable with bool Available { get; }, void Lend();, and void Return();. Make LibraryItem implement it (remember its existing members should cover the contract with no changes). Create a Book, assign it to an ILendable variable, and call Lend() through that variable.

  2. Define the interface ISearchable with bool Matches(string text);. Implement Matches() in LibraryItem (comparing Title and Author), and override it in Magazine so it also compares against IssueNumber.ToString(). Check the result with several sample searches.

  3. Add to ILendable a member with a default implementation, string StatusText() => Available ? "Available" : "Lent out";. Create a Book, lend it out, and show the result of StatusText() before and after lending it, always accessing it through an ILendable variable.

Solutions

interface ILendable
{
    bool Available { get; }
    void Lend();
    void Return();
}

abstract class LibraryItem : ILendable
{
    // Title, Author, Available, constructor, Lend(), Return() unchanged
}

Book book1 = new Book("Hopscotch", "Julio Cortazar", "978-84-376-0495-4");
ILendable lendable = book1;
lendable.Lend(); // "'Hopscotch' has been lent out."
interface ISearchable
{
    bool Matches(string text);
}

abstract class LibraryItem : ILendable, ISearchable
{
    // ...
    public virtual bool Matches(string text)
    {
        return Title.Contains(text, StringComparison.OrdinalIgnoreCase)
            || Author.Contains(text, StringComparison.OrdinalIgnoreCase);
    }
}

class Magazine : LibraryItem
{
    // ...
    public override bool Matches(string text)
    {
        return base.Matches(text) || IssueNumber.ToString().Contains(text);
    }
}

Magazine magazine1 = new Magazine("National Geographic", "Various authors", 302);
Console.WriteLine(magazine1.Matches("302"));      // True
Console.WriteLine(magazine1.Matches("national")); // True
interface ILendable
{
    bool Available { get; }
    void Lend();
    void Return();

    string StatusText() => Available ? "Available" : "Lent out";
}

Book book1 = new Book("Hopscotch", "Julio Cortazar", "978-84-376-0495-4");
ILendable lendable = book1;

Console.WriteLine(lendable.StatusText()); // "Available"
lendable.Lend();
Console.WriteLine(lendable.StatusText()); // "Lent out"

Conclusion

In this lesson you've learned to define interfaces with interface, to implement them on existing classes (checking that LibraryItem already satisfied most of ILendable's contract), to combine several interfaces in the same class, and to recognize C# 8's default implementation members. LibraryItem now stands as abstract class LibraryItem : ILendable, ISearchable, with Book and Magazine inheriting and enriching both contracts.

Interfaces solve "what a type can do," but there's another open question in BiblioTech: how to react when something happens — for example, when a loan is registered — without directly coupling whoever triggers it to whoever needs to know about it. In the next lesson you'll learn about delegates and events, the C# tool designed exactly for that, and you'll see the Library class come to life, which in the coming lessons of this module will go on to organize the entire catalog and its members.

© Copyright 2026. All rights reserved