SOLID, Explained With Fighter-Jet Code
The five SOLID principles — each a one-line rule, a jet analogy, and bad-vs-good code, all themed around a PvP dogfight game.

Five rules for arranging classes so change stays cheap. Every example is pulled from the same world — a PvP fighter-jet dogfight game. Rule, analogy, then the code.
S — Single Responsibility
One class, one reason to change.
A jet keeps its systems apart: flight controls fly it, the weapons system fires, the radar scans, the flight recorder logs the mission. Swap the missile and only the weapons system is opened up.
// Bad — one Jet, four reasons to change
public class Jet
{
public void Fly(Vector3 dir) { }
public void FireMissile(Jet target) { }
public void UpdateRadar() { }
public void SaveToDisk() { }
}
// Good — one job each
public class FlightController { public void Fly(Vector3 dir) { } }
public class WeaponSystem { public void FireMissile(Jet target) { } }
public class RadarSystem { public void Scan() { } }
public class JetSaveService { public void Save(Jet jet) { } }
O — Open/Closed
Open to extend, closed to modify.
Wing hardpoints: bolt on a new missile, don’t cut open the wing.
// Bad — every new weapon edits the same switch
public void Fire(string weapon, Jet target)
{
switch (weapon)
{
case "cannon": break;
case "missile": break;
// railgun? crack this open again
}
}
// Good — fixed contract, new weapon = new class
public interface IWeapon { void Fire(Jet target); }
public class Cannon : IWeapon { public void Fire(Jet t) { } }
public class Missile : IWeapon { public void Fire(Jet t) { } }
public class Railgun : IWeapon { public void Fire(Jet t) { } } // nothing else touched
public class WeaponSystem
{
public void Fire(IWeapon weapon, Jet target) => weapon.Fire(target);
}
L — Liskov Substitution
A subtype must stand in for its base without surprises.
A flare can’t damage a target — so it isn’t a weapon.
// Bad — Flare lies about being a weapon
public class Flare : IWeapon
{
public void Fire(Jet target) => throw new NotSupportedException();
}
// Good — model it as what it is
public interface IWeapon { void Fire(Jet target); }
public interface ICountermeasure { void Deploy(); }
public class Flare : ICountermeasure { public void Deploy() { } }
I — Interface Segregation
Don’t force a class to implement methods it doesn’t use.
Give each cockpit only the controls its airframe has. No dead switches.
// Bad — one fat interface, dead buttons
public interface ICombatant
{
void Fire(Jet target);
void DeployFlare();
void Cloak(); // only stealth jets have this
}
public class TrainerJet : ICombatant
{
public void Cloak() => throw new NotImplementedException();
}
// Good — small role interfaces
public interface IAttacker { void Fire(Jet target); }
public interface IDefender { void DeployFlare(); }
public interface IStealth { void Cloak(); }
public class TrainerJet : IAttacker, IDefender { }
public class StealthJet : IAttacker, IDefender, IStealth { }
D — Dependency Inversion
Depend on the abstraction, not the concrete detail.
The trigger fires whatever missile is loaded on the rail — it’s not welded to one.
// Bad — welded to one missile
public class WeaponSystem
{
private readonly Aim9Missile missile = new Aim9Missile();
public void Fire(Jet target) => missile.Launch(target);
}
// Good — any missile that fits the hardpoint
public interface IMissile { void Launch(Jet target); }
public class Aim9Missile : IMissile { public void Launch(Jet t) { } }
public class RadarMissile : IMissile { public void Launch(Jet t) { } }
public class WeaponSystem
{
private readonly IMissile missile;
public WeaponSystem(IMissile missile) => this.missile = missile;
public void Fire(Jet target) => missile.Launch(target);
}
Game Programmer & Co-Founder of PixelPunch LLP. I ship games, build tools, and make the web work harder.
Know more →