Chapter 3: Abstract Classes vs Interfaces - The 'ILanguageModel' Contract
Theoretical Foundations
In the previous chapter, we established the hierarchy of inheritance, where a base class defines common state and behavior for a family of objects. While powerful, classical inheritance is rigid; it binds a class to a single parent, which can create bottlenecks in complex AI systems where models may need to share behaviors across different lineages (e.g., a TransformerModel sharing utility methods with a VisionModel but not with a SpeechRecognitionModel). To address this, we introduce Interfaces, the cornerstone of modular architecture in advanced object-oriented programming.
An interface defines a contract without providing an implementation. It specifies what a class must do, but not how it does it. Unlike an abstract class, which can contain implementation details (fields, methods with logic), an interface is strictly a blueprint of capabilities. This distinction is vital in AI engineering: while an abstract class might provide shared mathematical operations (like matrix multiplication), an interface defines the behavioral expectations of a model, such as the ability to generate text or classify an image.
The Real-World Analogy: The Power Socket
Consider the electrical power socket in your wall. It defines a strict interface: two or three pins with specific voltages and frequencies. It does not care whether the electricity comes from a coal plant, a solar panel, or a nuclear reactor. It does not care about the internal wiring of the power grid. It simply defines the contract: "If you can provide 120V/60Hz, you can plug in and function."
- The Socket: The
ILanguageModelinterface. - The Device (Lamp, TV): The AI application consuming the model.
- The Power Source (Coal, Solar): The concrete implementations (Transformer, RNN, LSTM).
If the wall socket were a concrete class (like an abstract class with implementation), you would be forced to use only the specific wiring provided by the utility company. If you wanted to switch to solar power, you would have to rewire your house. With an interface, you simply unplug one source and plug in another, provided both adhere to the voltage contract.
Interfaces in C# Syntax
In C#, an interface is defined using the interface keyword. By convention, interfaces in C# are prefixed with the letter 'I' to distinguish them from classes. An interface cannot contain fields (instance variables) or constructors. It can contain method signatures, properties, events, and indexers, but none of these can have an implementation.
Here is the definition of our ILanguageModel contract:
using System.Collections.Generic;
namespace AI.DataStructures.Interfaces
{
/// <summary>
/// Defines the contract for any model capable of processing natural language.
/// </summary>
public interface ILanguageModel
{
// A property signature (no implementation)
string ModelName { get; }
// Method signatures (no implementation bodies)
List<int> Tokenize(string input);
string Generate(List<int> tokens);
// An event signature
event System.EventHandler<ModelGeneratedEventArgs> OnGenerationComplete;
}
public class ModelGeneratedEventArgs : System.EventArgs
{
public string Output { get; set; }
}
}
Implementing the Interface
When a class implements an interface, it guarantees that it provides concrete implementations for every member defined in the interface. This is a strict compile-time check. If a class claims to implement ILanguageModel but fails to provide a Tokenize method, the code will not compile.
Let us look at two distinct implementations: a traditional RNN and a modern Transformer. Notice how their internal logic differs, yet they satisfy the same contract.
using System;
using System.Collections.Generic;
namespace AI.DataStructures.Implementations
{
// Concrete Implementation 1: Recurrent Neural Network
public class RNNModel : ILanguageModel
{
public string ModelName => "Legacy RNN v1.0";
public List<int> Tokenize(string input)
{
// Simple character-based tokenization for RNNs
Console.WriteLine("Tokenizing via RNN character mapping...");
var tokens = new List<int>();
foreach (char c in input) tokens.Add((int)c);
return tokens;
}
public string Generate(List<int> tokens)
{
// Simulate sequential generation
Console.WriteLine("Generating text sequentially via RNN hidden states...");
string output = "";
foreach (int token in tokens) output += (char)token;
return output;
}
public event EventHandler<ModelGeneratedEventArgs> OnGenerationComplete;
}
// Concrete Implementation 2: Transformer
public class TransformerModel : ILanguageModel
{
public string ModelName => "GPT-Style Transformer";
public List<int> Tokenize(string input)
{
// Complex sub-word tokenization (e.g., BPE)
Console.WriteLine("Tokenizing via Transformer BPE vocabulary...");
// In a real scenario, this would map to a vocabulary file
return new List<int> { 101, 2054, 2003, 3007, 102 };
}
public string Generate(List<int> tokens)
{
// Simulate parallel attention-based generation
Console.WriteLine("Generating text via Self-Attention mechanisms...");
return "Transformer output: " + tokens.Count + " tokens processed.";
}
public event EventHandler<ModelGeneratedEventArgs> OnGenerationComplete;
}
}
The Role of Interfaces in AI Architecture
In AI applications, interfaces are crucial for decoupling. Consider a pipeline that processes user prompts. Without an interface, your code might look like this:
// BAD: Tight Coupling
public class PromptProcessor
{
private TransformerModel _model; // Hard-coded to Transformer
public string Process(string prompt)
{
var tokens = _model.Tokenize(prompt);
return _model.Generate(tokens);
}
}
This code is brittle. If we want to switch to an RNN for legacy compatibility, we must rewrite the PromptProcessor class. This violates the Open/Closed Principle (open for extension, closed for modification).
Using the ILanguageModel interface, we decouple the processor from the specific model:
// GOOD: Loose Coupling via Interface
public class PromptProcessor
{
private ILanguageModel _model; // Depends on abstraction, not concretion
public PromptProcessor(ILanguageModel model)
{
_model = model;
}
public string Process(string prompt)
{
// The processor doesn't care if it's a Transformer or RNN
var tokens = _model.Tokenize(prompt);
return _model.Generate(tokens);
}
}
This architecture allows us to swap models dynamically at runtime, a requirement for A/B testing different AI models in production or loading user-selected models.
The Liskov Substitution Principle (LSP)
While interfaces define the contract, the Liskov Substitution Principle (LSP) ensures that the implementations honor the contract's semantic meaning. LSP states that objects of a superclass shall be replaceable with objects of its subclasses without breaking the application.
In the context of our ILanguageModel:
- Substitutability: Anywhere your code expects an
ILanguageModel(e.g., thePromptProcessor), you should be able to pass aTransformerModel, anRNNModel, or a futureQuantumModelwithout the system behaving unexpectedly. - Behavioral Consistency: If the contract implies that
Generatereturns a string, an implementation must not throw an exception (unless the contract specifies it) or return an integer. It must fulfill the promise.
Violation of LSP Example:
Imagine we create a BrokenModel that implements ILanguageModel but throws an exception in Tokenize for empty strings, while TransformerModel returns an empty list. This inconsistency violates LSP because the consumer cannot predict the behavior.
public class BrokenModel : ILanguageModel
{
public string ModelName => "Broken Model";
public List<int> Tokenize(string input)
{
if (string.IsNullOrEmpty(input))
throw new InvalidOperationException("Cannot tokenize empty input");
return new List<int>();
}
// ... other implementations
}
To adhere to LSP, our implementations must honor the preconditions and postconditions implied by the interface. If the interface does not specify behavior for empty strings, both models should handle it gracefully (e.g., both return empty lists).
Visualizing the Architecture
The following diagram illustrates the relationship between the interface and the concrete implementations. Notice how the interface acts as a bridge, allowing the PromptProcessor to interact with any model without knowing its internal structure.
Summary of Differences: Abstract Class vs. Interface
To fully grasp the architectural choice, we must contrast interfaces with the abstract classes discussed in previous chapters.
| Feature | Abstract Class | Interface |
|---|---|---|
| Implementation | Can provide default implementation for some methods. | Cannot provide any implementation (until C# 8.0 default methods, which we avoid here for clarity). |
| Fields/State | Can contain instance fields (state). | Cannot contain instance fields (stateless). |
| Inheritance | A class can inherit only one abstract class. | A class can implement multiple interfaces. |
| Access Modifiers | Members can have any access modifier. | Members are implicitly public. |
| Purpose | Defines "Is-A" relationships and shared code. | Defines "Can-Do" capabilities and contracts. |
In AI development, we use Abstract Classes when we have a family of models sharing significant mathematical infrastructure (e.g., a base NeuralNetwork class handling backpropagation logic). We use Interfaces when we define capabilities that may be shared across unrelated families (e.g., ILoadable, ISerializable, ILanguageModel).
By mastering the ILanguageModel contract, you establish the foundation for a modular, testable, and scalable AI system capable of integrating diverse technologies under a unified API.
Basic Code Example
Problem Framing: The Swappable Language Model
Imagine you are building a sophisticated AI-powered chatbot. The core of your system is a language model that processes text and generates responses. Initially, you might choose a lightweight model (like an RNN) for quick prototyping. Later, you want to upgrade to a powerful Transformer model without rewriting the entire application logic. How do you ensure that your application code remains stable when you swap the underlying model implementation?
This is where the ILanguageModel interface comes in. It acts as a strict contract. It guarantees that any class claiming to be a language model—whether it's a TransformerModel, RNNModel, or even a mock RuleBasedModel—must provide specific behaviors like tokenize and generate. By coding against the interface rather than a concrete class, we decouple our application from the specific implementation details, allowing for modular, maintainable, and testable AI systems.
The ILanguageModel Contract
We define an interface that specifies the required behaviors. In C#, interfaces contain only method signatures (no implementation) and no fields (only properties in advanced scenarios, but we stick to methods here).
using System;
// The Contract: Defines the behaviors any language model must provide.
public interface ILanguageModel
{
// Tokenizes raw text into a list of tokens (simulated as strings for simplicity).
string[] Tokenize(string input);
// Generates a response based on the input tokens.
string Generate(string[] tokens);
}
Implementations: Concrete Models
Here we create two distinct implementations. Notice how they handle the same contract differently. One might be a heavy neural network simulation, the other a simple heuristic engine.
// Implementation 1: A simple rule-based model (e.g., for testing or legacy systems).
public class RuleBasedModel : ILanguageModel
{
// Converts input to uppercase to simulate "processing" and splits by space.
public string[] Tokenize(string input)
{
// Logic: Uppercase and split.
return input.ToUpper().Split(' ');
}
// Generates a response based on specific keywords.
public string Generate(string[] tokens)
{
// Check for specific keywords to return a canned response.
foreach (string token in tokens)
{
if (token == "HELLO")
{
return "Greetings, human!";
}
}
return "I don't understand.";
}
}
// Implementation 2: A simulated Transformer model (complex logic placeholder).
public class TransformerModel : ILanguageModel
{
// Simulates a tokenizer that adds special BOS/EOS tokens.
public string[] Tokenize(string input)
{
// Logic: Wrap input in special tokens to simulate a tokenizer like BPE.
return new string[] { "<BOS>", input, "<EOS>" };
}
// Simulates a generative process (e.g., next token prediction).
public string Generate(string[] tokens)
{
// Logic: If we have enough tokens, generate a "complex" response.
if (tokens.Length >= 3)
{
return "Processed by Transformer: " + tokens[1];
}
return "Insufficient context for generation.";
}
}
Usage: The Swappable System
This is the core architectural benefit. The ChatApplication class depends only on the ILanguageModel interface. It has no knowledge of whether the underlying engine is a RuleBasedModel or a TransformerModel.
public class ChatApplication
{
private readonly ILanguageModel _model;
// Constructor accepts the interface, not a concrete class.
public ChatApplication(ILanguageModel model)
{
this._model = model;
}
public void RunChat()
{
string userInput = "hello world";
// 1. Tokenize
string[] tokens = _model.Tokenize(userInput);
// 2. Generate
string response = _model.Generate(tokens);
Console.WriteLine($"User: {userInput}");
Console.WriteLine($"Bot: {response}");
}
}
// Main Program execution
public class Program
{
public static void Main()
{
// Scenario A: Using the lightweight RuleBasedModel
Console.WriteLine("--- Scenario A: RuleBasedModel ---");
ChatApplication appA = new ChatApplication(new RuleBasedModel());
appA.RunChat();
Console.WriteLine();
// Scenario B: Swapping to the complex TransformerModel
// Notice: No changes to ChatApplication code are required.
Console.WriteLine("--- Scenario B: TransformerModel ---");
ChatApplication appB = new ChatApplication(new TransformerModel());
appB.RunChat();
}
}
Visualizing the Architecture
The following diagram illustrates the relationship. The ChatApplication points to the interface, while the concrete implementations realize the interface.
Common Pitfalls
1. Adding State to Interfaces A common mistake when moving from abstract classes to interfaces is attempting to define fields (state) directly inside the interface. In C#, interfaces cannot contain instance fields. They define a contract for behavior (methods), not storage. If you need shared state, consider using an abstract base class or managing state within the implementing classes themselves.
2. Violating the Liskov Substitution Principle (LSP)
While our example is simple, in complex systems, developers often implement an interface but change the expected behavior drastically. For example, if TransformerModel.Tokenize expected a specific input format (like JSON) while RuleBasedModel expected plain text, the ChatApplication would break when swapping models. LSP dictates that derived classes must be substitutable for their base types without altering the correctness of the program. Always ensure your implementations adhere to the semantic contract of the interface, not just the syntactic signature.
Step-by-Step Explanation
-
Interface Definition (
ILanguageModel):- We define a pure abstraction using the
interfacekeyword. - It declares two methods:
Tokenize(returns an array of strings) andGenerate(returns a string). - This interface serves as a "blueprint" or contract. Any class that implements this interface is explicitly stating, "I know how to tokenize and generate text."
- We define a pure abstraction using the
-
Concrete Implementation (
RuleBasedModel):- This class uses the
:syntax to implementILanguageModel. - It provides the actual code for the methods. Here, the logic is simplistic: converting text to uppercase and splitting it.
- This represents a "legacy" or "lightweight" component in our system.
- This class uses the
-
Concrete Implementation (
TransformerModel):- This class also implements
ILanguageModel. - However, its internal logic is different. It simulates a modern Transformer architecture by adding special boundary tokens (
<BOS>,<EOS>). - Crucially, despite the internal complexity difference, it adheres to the exact same method signatures defined in the interface.
- This class also implements
-
Consumer Class (
ChatApplication):- This class represents the business logic of our application.
- Dependency Injection: Notice the constructor
public ChatApplication(ILanguageModel model). It does not ask for aRuleBasedModelorTransformerModel; it asks for theILanguageModelinterface. - This is the essence of loose coupling. The application logic is completely unaware of the specific algorithm used to process the text.
-
Execution (
Program.Main):- We instantiate the
ChatApplicationtwice. - First, we pass a new instance of
RuleBasedModel. - Second, we pass a new instance of
TransformerModel. - The
ChatApplicationruns exactly the same way in both cases, demonstrating polymorphism. The system is flexible and future-proof; adding a newQuantumModellater would require zero changes to theChatApplicationclass.
- We instantiate the
The chapter continues with advanced code, exercises and solutions with analysis, you can find them on the ebook on Leanpub.com or Amazon
Code License: All code examples are released under the MIT License. Github repo.
Content Copyright: Copyright © 2026 Edgar Milvus | Privacy & Cookie Policy. All rights reserved.
All textual explanations, original diagrams, and illustrations are the intellectual property of the author. To support the maintenance of this site via AdSense, please read this content exclusively online. Copying, redistribution, or reproduction is strictly prohibited.