The Virtual Zoo: A Masterclass in C# Object-Oriented Design

Junior/Mid Engineer Asked at: Microsoft, Amazon, any company using .NET.

The OOP Challenge: Building a Virtual Pet Simulator in C#

Why this matters: This isn't just a coding test; it's a design simulation. The interviewer wants to see if you can translate a set of requirements into a logical, resilient, and extensible system. Your answer demonstrates your grasp of the four pillars of OOP, which are the bedrock of modern software architecture.

The Mental Model: The Factory Blueprint

Before writing a line of code, I think about the system architecturally. A great mental model for OOP is a factory that produces different kinds of vehicles.

  • Abstraction (The Blueprint): An abstract `Vehicle` blueprint defines what every vehicle *must* have (an engine, wheels) but doesn't build it. `public abstract void StartEngine();` is a non-negotiable contract.
  • Inheritance (The Assembly Lines): The factory has separate assembly lines for `Car` and `Truck`. Both use the master `Vehicle` blueprint, inheriting the basic design, but then add their own specialized parts.
  • Encapsulation (The Engine Block): The engine is a sealed unit. The driver doesn't need to know about pistons and spark plugs; they just use a simple interface (the ignition key). The engine protects its complex internal state.
  • Polymorphism (The Highway): On the highway, you can treat every vehicle the same. You can tell a `Car` to `Move()` and a `Truck` to `Move()`. They both do the same high-level action, but the underlying implementation (how much fuel they use, how fast they go) is specific to each type.

With this model, let's build the Pet Simulator.

1. Abstraction & Encapsulation: The `Pet` Blueprint

We start with an `abstract` class `Pet`. It's `abstract` because you can't have a "generic" pet; you can only have a specific *type* of pet. This class will define the contract and protect the internal state.

// Pet.cs
public abstract class Pet
{
    // --- ENCAPSULATION: The internal state is protected. ---
    // The outside world interacts via properties with private setters.
    public string Name { get; }
    public int Hunger { get; private set; }
    public int Happiness { get; private set; }

    public Pet(string name)
    {
        Name = name;
        Hunger = 50;     // Start with a neutral state
        Happiness = 50;
    }

    // --- ABSTRACTION: Exposing simple actions, hiding complex logic. ---
    public void Play()
    {
        Console.WriteLine($"{Name} is playing.");
        Happiness = Math.Min(100, Happiness + 20); // Don't exceed 100
        Hunger = Math.Min(100, Hunger + 10);      // Playing makes them hungry
    }

    public string GetStatus()
    {
        return $"{Name}: Hunger={Hunger}, Happiness={Happiness}";
    }

    // --- POLYMORPHISM: A behavior that can be changed by subclasses. ---
    public virtual string Speak()
    {
        return "...";
    }

    // --- ABSTRACTION: A contract that MUST be fulfilled by subclasses. ---
    public abstract void Eat();

    // Helper method for children to call without directly setting Hunger.
    protected void DecreaseHunger(int amount)
    {
        Hunger = Math.Max(0, Hunger - amount); // Don't go below 0
    }
}

2. Inheritance & Polymorphism: The Specialized Pets

Now we create our specific assembly lines. Each class inherits from `Pet`, gets all its functionality for free, and then provides its own specialized implementations for `Speak` and `Eat`.

// Dog.cs
public class Dog : Pet
{
    public string FavoriteToy { get; } // Bonus: Dog-specific property
    
    public Dog(string name, string favoriteToy) : base(name) 
    {
        FavoriteToy = favoriteToy;
    }

    public override string Speak() => "Woof!";

    public override void Eat()
    {
        Console.WriteLine($"{Name} is eating kibble.");
        DecreaseHunger(30); // Use the protected helper from the base class
    }
}

// Cat.cs
public class Cat : Pet
{
    public Cat(string name) : base(name) { }

    public override string Speak() => "Meow!";

    public override void Eat()
    {
        Console.WriteLine($"{Name} is eating fish.");
        DecreaseHunger(25);
    }
}

// Bird.cs
public class Bird : Pet
{
    public Bird(string name) : base(name) { }

    public override string Speak() => "Chirp!";

    public override void Eat()
    {
        Console.WriteLine($"{Name} is eating seeds.");
        DecreaseHunger(15);
    }
}

3. The Main Program: Bringing it All Together

This is the "highway" where we treat all our pets as `Pet` objects. Thanks to polymorphism, the .NET runtime ensures the correct `Speak()` and `Eat()` methods are called for each specific animal.

// Program.cs
class Program
{
    static void Main(string[] args)
    {
        var pets = new List<Pet>
        {
            new Dog("Fido", "Squeaky Ball"),
            new Cat("Whiskers"),
            new Bird("Polly")
        };

        foreach (var pet in pets)
        {
            Console.WriteLine($"--- Interacting with {pet.Name} ---");
            Console.WriteLine($"Initial Status: {pet.GetStatus()}");
            
            Console.WriteLine($"{pet.Name} says: {pet.Speak()}");
            
            pet.Eat();
            pet.Play();
            
            // Bonus Challenge: Check for a specific type to access unique properties
            if (pet is Dog dog)
            {
                Console.WriteLine($"Special Dog Info: Favorite toy is {dog.FavoriteToy}.");
            }

            Console.WriteLine($"Final Status: {pet.GetStatus()}");
            Console.WriteLine();
        }
    }
}

Bonus Challenge Analysis

1. Dog's Favorite Toy

The main loop only knows about the `Pet` contract. It has no idea what a `FavoriteToy` is. To access it, we must perform a safe type-check and cast. The modern C# `if (pet is Dog dog)` syntax is perfect for this. It simultaneously checks the type and, if it matches, assigns the cast object to a new variable `dog` that we can use inside the `if` block. This prevents us from trying to ask a cat about its favorite toy.

2. The Fish Problem

A fish can't play fetch. This is a classic modeling problem. Inheritance implies an "is-a" relationship, but sometimes the parent's behavior doesn't perfectly fit the child. The solution is to override the behavior.

public class Fish : Pet
{
    public Fish(string name) : base(name) { }

    public override void Eat()
    {
        Console.WriteLine($"{Name} is eating fish flakes.");
        DecreaseHunger(5);
    }
    
    public override string Speak() => "Blub...";
    
    // We override Play() to provide a more fitting behavior.
    public override void Play()
    {
        Console.WriteLine($"{Name} swims happily in circles.");
        Happiness = Math.Min(100, Happiness + 5); // Not as much fun as a real game
    }
}

By overriding `Play()`, we fulfill the `Pet` contract while providing a specialized implementation that makes sense for a `Fish`. The main loop doesn't need to change at all; it can still call `pet.Play()` on a fish, and polymorphism ensures the correct, specialized behavior is executed.

Written by Benito J D