Chapter 1: Inheritance - Creating a Base 'Model' Class
Theoretical Foundations
Inheritance is the mechanism that allows a new class to acquire the properties and behaviors of an existing class. In the context of building AI systems, we utilize inheritance to create a hierarchy where a generalized Model class sits at the top, defining the standard behaviors that all machine learning models must support, such as training (fit) and making predictions (predict). This establishes a "contract" that ensures consistency across different model implementations, whether they are simple linear regressions or complex neural networks.
To understand this practically, consider a real-world analogy of a vehicle manufacturing plant. The Model base class acts as the blueprint for a generic vehicle. This blueprint dictates that every vehicle must have an engine (analogous to the fit method for training) and steering capabilities (analogous to the predict method for inference). However, the blueprint does not specify whether the vehicle is a sedan, a truck, or a sports car. It only enforces that these core components exist. When a factory builds a specific car, like a Sedan, it inherits from the Vehicle blueprint. The Sedan class provides the specific implementation details—perhaps a 4-cylinder engine and standard suspension—while still adhering to the requirement that it has an engine and steering. This allows the assembly line to treat all vehicles uniformly; a mechanic knows exactly where to look to fix the engine, regardless of the specific car type.
In C#, inheritance is implemented using the : symbol. We define a base class (the parent) and a derived class (the child). The derived class automatically gains the public and protected members of the base class.
The Role of Abstract Base Classes (ABCs)
While standard inheritance is powerful, it has a limitation: it allows the creation of instances of the base class. In our vehicle analogy, we wouldn't want to manufacture a "Generic Vehicle" because it doesn't actually exist; we only manufacture specific types. Similarly, in AI, a "Generic Model" is not useful because it doesn't know how to perform specific calculations.
This is where Abstract Base Classes come in. An abstract class is a special type of class that cannot be instantiated. Its purpose is to define a common interface and shared functionality for a group of related classes. It acts as a template or a blueprint that derived classes must follow.
In C#, we declare an abstract class using the abstract keyword. Within this class, we can define abstract methods—methods that have no implementation in the base class. Any class that inherits from the abstract class is forced to provide an implementation for these abstract methods. This enforces a strict structure, ensuring that no matter what type of model we create later, it will always have the necessary methods.
using System;
namespace AI.Modeling
{
// Abstract base class defining the contract for all AI models
public abstract class Model
{
// Abstract methods: No implementation here.
// Derived classes MUST implement these.
public abstract void Fit(double[] trainingData, double[] labels);
public abstract double Predict(double[] input);
// Concrete method: Shared behavior for all models.
// This utilizes inheritance to avoid code duplication.
public void Save(string filePath)
{
Console.WriteLine($"Model saved to {filePath}");
// Actual serialization logic would go here
}
}
}
Polymorphism: The Key to Modularity
Polymorphism (meaning "many forms") is the ability of a single interface to represent different underlying forms (data types). In the context of our Model base class, polymorphism allows us to write code that interacts with the Model type without knowing the specific derived type (e.g., LinearRegressionModel or NeuralNetworkModel).
This is critical for building modular AI data pipelines. Imagine you are building an application that needs to switch between using a cloud-based model (like OpenAI) and a local model (like Llama). Without polymorphism, you would need to write if/else statements checking the type of the model every time you wanted to make a prediction.
With polymorphism, you treat both models simply as Model. The specific implementation is handled at runtime. This concept was introduced in the previous chapter regarding interfaces, but inheritance provides a way to share common code (like the Save method) alongside the shared interface.
Here is how polymorphism works in practice:
public class LinearRegressionModel : Model
{
// Implementation specific to Linear Regression
public override void Fit(double[] trainingData, double[] labels)
{
Console.WriteLine("Fitting Linear Regression model...");
// Math logic for calculating slope and intercept
}
public override double Predict(double[] input)
{
Console.WriteLine("Predicting with Linear Regression...");
return 0.0; // Placeholder for calculation
}
}
public class NeuralNetworkModel : Model
{
// Implementation specific to Neural Networks
public override void Fit(double[] trainingData, double[] labels)
{
Console.WriteLine("Training Neural Network (Backpropagation)...");
// Complex matrix operations
}
public override double Predict(double[] input)
{
Console.WriteLine("Predicting with Neural Network...");
return 0.0; // Placeholder for forward pass
}
}
// Usage in a generic pipeline
public class Pipeline
{
public void RunInference(Model model, double[] data)
{
// The 'model' parameter accepts any derived type of Model
// due to polymorphism.
double result = model.Predict(data);
// We can also call the shared method inherited from the base class
model.Save("checkpoint.bin");
}
}
Architectural Implications and Visualization
The architectural implication of using inheritance in this manner is the creation of a Plug-and-Play System. The Pipeline class does not need to know the internal workings of LinearRegressionModel or NeuralNetworkModel. It only cares that they adhere to the Model contract. This decouples the data processing logic from the model implementation logic.
If we need to add a new model type, say a DecisionTreeModel, we simply inherit from Model, implement the Fit and Predict methods, and pass it to the Pipeline. No changes to the Pipeline code are required.
The following diagram illustrates the hierarchy. Note that Model is abstract (indicated by the italics or specific notation in diagrams), meaning you cannot create an instance of it directly. You can only create instances of the concrete derived classes.
Edge Cases and Nuances
- The Fragile Base Class Problem: One risk of inheritance is that a change in the base class (e.g., adding a new abstract method to
Model) breaks all derived classes that haven't implemented it. In AI development, where models are often complex, modifying a base class requires rigorous testing. - Method Hiding vs. Overriding: While we focus on
override(polymorphism), C# also allowsnewto hide a base method. In AI modeling, hiding is generally discouraged because it breaks the expected behavior of the base class. Always useoverridewhen extending functionality of abstract methods. - Access Modifiers: When inheriting,
publicandprotectedmembers are accessible.privatemembers are not. In the context of theModelclass, we keep training logicprotectedif helper methods are needed, but the interface (Fit,Predict) remainspublicto ensure the pipeline can access them.
By strictly adhering to this inheritance structure, we ensure that our AI system is robust, extensible, and easy to maintain. The base class handles the "what" (the contract), while the derived classes handle the "how" (the specific mathematical implementation).
Basic Code Example
Objective: Frame the problem that the code example solves in a relatable, real-world context.
In the realm of AI development, we often encounter a scenario where multiple algorithms compete to solve the same problem. Consider a simple task: predicting the price of a house based on its square footage. We might have two distinct approaches:
- Linear Regression: A simple statistical model that assumes a straight-line relationship.
- Polynomial Regression: A more complex model that can capture curves (e.g., diminishing returns on size).
Without a common structure, the code for training and predicting with these models would look entirely different. One might use a Train() method, the other Fit(). One might return a double for prediction, the other a List<double>. This inconsistency makes it impossible to build a scalable system where we can easily swap models or compare them side-by-side.
The solution is Inheritance. By defining a base Model class, we enforce a strict contract. Every model that inherits from Model must provide implementations for Fit (training) and Predict (inference). This allows us to write a unified data pipeline that treats all models identically, regardless of their internal complexity.
using System;
namespace AI_Data_Structures
{
// ---------------------------------------------------------
// 1. The Abstract Base Class (The Contract)
// ---------------------------------------------------------
// We use 'abstract' because we never want to create a generic "Model".
// We only care about specific implementations (Linear, Polynomial, etc.).
// This class defines the shared interface that all AI components must adhere to.
public abstract class Model
{
// 'abstract' methods have no implementation here.
// They force any non-abstract child class to provide the logic.
/// <summary>
/// Trains the model on the provided dataset.
/// </summary>
/// <param name="x">The input features (e.g., square footage).</param>
/// <param name="y">The target values (e.g., house prices).</param>
public abstract void Fit(double[] x, double[] y);
/// <summary>
/// Predicts a value based on input features.
/// </summary>
/// <param name="input">The input features to predict on.</param>
/// <returns>The predicted value.</returns>
public abstract double Predict(double input);
/// <summary>
/// Saves the model to a file (common functionality).
/// </summary>
public virtual void Save()
{
// 'virtual' allows this method to be overridden, but provides a default implementation.
// Here, we simulate saving by printing to console.
Console.WriteLine("Model state saved to disk.");
}
}
// ---------------------------------------------------------
// 2. Concrete Implementation: Linear Regression
// ---------------------------------------------------------
// This class inherits from Model and provides the specific mathematical logic.
public class LinearRegressionModel : Model
{
// Fields to store the learned parameters (weights)
private double _slope;
private double _intercept;
/// <summary>
/// Implements the abstract Fit method using the Least Squares method.
/// </summary>
public override void Fit(double[] x, double[] y)
{
if (x.Length != y.Length)
{
throw new ArgumentException("Input and output arrays must be the same length.");
}
// Simple calculation of slope (m) and intercept (b) for y = mx + b
// Note: In a real system, this would be a complex optimization algorithm.
double sumX = 0, sumY = 0, sumXY = 0, sumXX = 0;
int n = x.Length;
for (int i = 0; i < n; i++)
{
sumX += x[i];
sumY += y[i];
sumXY += x[i] * y[i];
sumXX += x[i] * x[i];
}
_slope = (n * sumXY - sumX * sumY) / (n * sumXX - sumX * sumX);
_intercept = (sumY - _slope * sumX) / n;
Console.WriteLine($"Linear Model Trained: y = {_slope:F2}x + {_intercept:F2}");
}
/// <summary>
/// Implements the abstract Predict method.
/// </summary>
public override double Predict(double input)
{
return (_slope * input) + _intercept;
}
}
// ---------------------------------------------------------
// 3. Concrete Implementation: Polynomial Regression (2nd Degree)
// ---------------------------------------------------------
// This demonstrates polymorphism: a different implementation of the same interface.
public class PolynomialRegressionModel : Model
{
private double _a; // x^2 coefficient
private double _b; // x coefficient
private double _c; // constant
/// <summary>
/// Fits a polynomial curve y = ax^2 + bx + c.
/// </summary>
public override void Fit(double[] x, double[] y)
{
// Simplified logic for demonstration purposes.
// Real polynomial fitting requires matrix operations (e.g., Normal Equation).
// We will hardcode values to simulate a trained state for this example.
_a = 0.1;
_b = 2.0;
_c = 10.0;
Console.WriteLine($"Polynomial Model Trained: y = {_a:F2}x^2 + {_b:F2}x + {_c:F2}");
}
/// <summary>
/// Predicts using the polynomial formula.
/// </summary>
public override double Predict(double input)
{
return (_a * input * input) + (_b * input) + _c;
}
}
// ---------------------------------------------------------
// 4. The System Context (Usage)
// ---------------------------------------------------------
public class HousingPriceSystem
{
public static void RunDemo()
{
// Real-world data: Square footage (x) and Price in thousands (y)
double[] squareFootage = { 1000, 1500, 2000, 2500 };
double[] prices = { 150, 220, 280, 350 };
Console.WriteLine("--- Scenario: Predicting House Prices ---\n");
// -----------------------------------------------------
// Using the Base Class Type (Polymorphism in Action)
// -----------------------------------------------------
// We can declare variables of the base type 'Model'.
// This allows us to swap algorithms without changing the calling code.
Model currentModel = new LinearRegressionModel();
Console.WriteLine("1. Training Linear Model...");
currentModel.Fit(squareFootage, prices);
double predictionLinear = currentModel.Predict(1800);
Console.WriteLine($"Prediction for 1800 sqft: ${predictionLinear:F2}k\n");
// -----------------------------------------------------
// Swapping the Implementation
// -----------------------------------------------------
// Notice the code below is identical to the block above,
// yet it executes completely different logic.
Console.WriteLine("2. Switching to Polynomial Model...");
currentModel = new PolynomialRegressionModel();
currentModel.Fit(squareFootage, prices);
double predictionPoly = currentModel.Predict(1800);
Console.WriteLine($"Prediction for 1800 sqft: ${predictionPoly:F2}k\n");
// -----------------------------------------------------
// Using Common Functionality
// -----------------------------------------------------
// The 'Save' method is inherited from the base class.
// We can call it on any Model, regardless of its specific type.
Console.WriteLine("3. Saving Model State...");
currentModel.Save();
}
}
}
Visualizing the Architecture
The relationship between the base Model class and its concrete implementations is best visualized as a hierarchy. The Model class acts as the anchor, defining the structure, while the specific algorithms fill in the details.
Step-by-Step Explanation
-
Defining the Abstract Base Class (
Model):- We declared
Modelasabstract. This prevents instantiation of a generic model (which would have no logic) and signals that this class is a blueprint. FitandPredict: These are marked asabstract. This is a strict enforcement mechanism. If a developer creates a new classNeuralNetworkModel : Modelbut forgets to implementPredict, the code will not compile. This guarantees that all models in our system adhere to the same interface.Save: We marked this asvirtual. This demonstrates a different aspect of inheritance: shared implementation. While every model needs to predict, they might all save to a database in the same way. By providing a default implementation here, we reduce code duplication.
- We declared
-
Implementing
LinearRegressionModel:- This class inherits from
Modelusing the:syntax. - It adds private fields (
_slope,_intercept) to store its specific state. These fields are invisible to the outside world; they are encapsulated within the class. - The
overridekeyword is crucial. It tells the compiler, "I am fulfilling the contract defined by the abstractModelclass." - The logic inside
Fitcalculates the regression line. The logic insidePredictapplies that line to new data.
- This class inherits from
-
Implementing
PolynomialRegressionModel:- This class also inherits from
Model, but it brings different mathematical capabilities. - It stores different state variables (
_a,_b,_c) appropriate for a quadratic equation. - Even though the internal math is different, the signature of the methods (the return types and parameters) matches the base class exactly. This is essential for polymorphism.
- This class also inherits from
-
Polymorphism in the
HousingPriceSystem:- We declare the variable
currentModelas typeModel, notLinearRegressionModelorPolynomialRegressionModel. - Swapping: In the first block,
currentModelpoints to aLinearRegressionModel. In the second block, we reassign it to aPolynomialRegressionModel. The variablecurrentModeldoesn't change type, but the object it points to changes behavior. - Unified Interface: Notice the method calls:
currentModel.Fit(...)andcurrentModel.Predict(...). The calling code does not need to know which specific algorithm is running. It relies entirely on the interface provided by the base class. This is the core benefit of inheritance in complex systems: it decouples the usage of an object from its implementation.
- We declare the variable
Common Pitfalls
Mistake: Forgetting the override keyword.
A frequent error when working with inheritance is modifying a method in the child class without explicitly marking it as override.
// INCORRECT CODE
public class BadLinearModel : Model
{
// Missing 'override' keyword
public void Fit(double[] x, double[] y)
{
// Logic here...
}
}
Why this is dangerous:
- Compilation Error: If
Fitwas defined asabstractin the base class, the code will fail to compile because the child class is effectively hiding the base method rather than implementing it. - Runtime Behavior: If
Fitwas defined asvirtualin the base class (with a default implementation), omittingoverridecreates a new method that is unrelated to the base method. If you callFitvia a variable of typeModel, the base class implementation will run, not the child's. This leads to silent logic errors where your custom training code is simply ignored.
The Fix: Always use override when implementing methods from an abstract base class or changing the behavior of a virtual method.
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.