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
- What an interface is and what problem it solves
- Interfaces versus abstract classes: revisiting the distinction from Module 3
- Defining and implementing an interface:
ILendable - Implementing multiple interfaces
- A second interface:
ISearchable - Members with a default implementation (C# 8 onward)
- BiblioTech's model with interfaces applied
- 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:
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.
- 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.
- Defining and implementing an interface:
ILendable
ILendableImplementing 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); // FalseA 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.
- 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.
- A second interface:
ISearchable
ISearchableTo 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):
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
- 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 itThis 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.
- 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 anabstractmethod; 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
: ILendablebut 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 toILendable's members; to useIsbnorDescribe()you need to convert back toBookwithis/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
Iprefix (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
-
Define the interface
ILendablewithbool Available { get; },void Lend();, andvoid Return();. MakeLibraryItemimplement it (remember its existing members should cover the contract with no changes). Create aBook, assign it to anILendablevariable, and callLend()through that variable. -
Define the interface
ISearchablewithbool Matches(string text);. ImplementMatches()inLibraryItem(comparingTitleandAuthor), and override it inMagazineso it also compares againstIssueNumber.ToString(). Check the result with several sample searches. -
Add to
ILendablea member with a default implementation,string StatusText() => Available ? "Available" : "Lent out";. Create aBook, lend it out, and show the result ofStatusText()before and after lending it, always accessing it through anILendablevariable.
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.
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
