The Virtual Zoo: A Masterclass in C# Object-Oriented Design
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.
