Oop Patterns

By the end of this lesson you'll be able to apply the everyday design patterns — Factory, Strategy, Observer, and Singleton — and the Dependency Inversion principle that ties them together, so your C# code stays flexible, testable, and easy to extend.

Learn Python, JavaScript, Java and more with free interactive lessons, real projects and AI-powered help. Beginner-friendly.

Part of the free C# course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.

Design patterns are like the standard joints a carpenter knows — a dovetail, a mortise and tenon, a dowel. Nobody invents a new joint for every shelf; they reach for the one that fits the job. A power drill is a great analogy for the patterns here: the drill body (your code) doesn't care which bit is fitted — it just spins whatever clicks into the chuck. A Strategy is swapping the bit (the algorithm) without changing the drill. A Factory is the bit holder that hands you the right one by name. The chuck itself — the standard socket every bit must fit — is an interface , and "always fit to the chuck, never weld a bit on" is Dependency Inversion . Learn the joints once and you stop reinventing them.

A design pattern is a named, reusable solution to a problem that keeps coming up — not a library you install, but a shape your classes take. The four below (from the classic "Gang of Four" catalogue) are the ones you'll actually reach for week to week, and they all rest on one idea: program to an interface, not a concrete class .

1. Strategy — Swap Algorithms at Runtime

The Strategy pattern turns a sprawling if / switch over "which way of doing this" into a set of small classes that all implement one interface. A context class holds a reference to the interface and delegates the work, so you can change the behaviour by swapping in a different strategy object — even while the program runs — without editing the context at all. Read this worked example, run it, then you'll wire one up yourself.

Your turn. The program below has a HalfPrice strategy and a Checkout context — they just need two blanks filled. Implement the discount and pick the strategy at runtime, then run it.

2. Factory — Hide Object Creation

A Factory gathers all the new SomeClass(...) calls into one place so the rest of your code asks for an object by name or type and gets back an interface. This matters because scattering new Circle() everywhere couples every caller to a concrete class — change the class and you hunt down every site. With a factory, callers depend only on the IShape interface, and adding a new shape means editing one method. Read the worked example first, then finish your own.

Now you try. Finish the factory so it returns a Square for "square" and a Circle otherwise, then ask the factory (not new ) for a square. Fill in the two blanks:

3. Observer — One Change, Many Reactions

The Observer pattern sets up a one-to-many link: a subject keeps a list of observers and, when its state changes, calls each one. The subject doesn't know or care what the observers do — it just notifies them — so you can add an email alert, an analytics tracker, or a logger without ever touching the subject. C# bakes this in through event and delegates, but building it by hand makes the moving parts obvious.

4. Singleton — Exactly One Instance

A Singleton guarantees a class has just one instance and gives the whole app a single way to reach it — handy for shared configuration or a logger. You make the constructor private so no one else can call new , and expose the one instance through a static property. The trap is thread safety : if two threads race to build it lazily you can get two instances. The simplest correct version uses a static readonly field, which the C# runtime initialises exactly once for you.

5. Dependency Inversion — Program to Interfaces

Notice what every pattern above shares: classes talk to each other through interfaces , never concrete types. That's the Dependency Inversion principle — the "D" in SOLID. High-level code (a service) shouldn't be welded to low-level details (a specific email library); both should depend on an abstraction. In practice this means asking for an interface in your constructor and letting the caller supply the concrete object. It's what makes code testable (pass a fake), swappable (change the backend), and is the foundation of the Dependency Injection you'll meet next.

You rarely need to hand-roll the Observer list in real C# — the language gives you event and delegates that do the bookkeeping for you. The hand-written version is worth understanding, but reach for events in production.

The ?.Invoke is null-safe: if nobody has subscribed, PriceChanged is null and a plain PriceChanged(symbol) would throw.

Here's a small but real program that combines this lesson's ideas: a Factory picks a Strategy by name, and the calling code only ever sees the IShippingStrategy interface (Dependency Inversion). To add a new carrier you edit one dictionary entry — nothing else changes.

Notice the factory stores Func IShippingStrategy creators in a dictionary. That's the "open for extension, closed for modification" idea — you register new behaviour without rewriting the lookup.

Q: How is the Factory pattern different from just calling a constructor?

A constructor builds one specific class; you have to name it. A factory chooses which class to build based on input and hands back an interface, so callers stay decoupled from concrete types and you can add new ones in one place.

Q: Strategy and Dependency Inversion look almost identical — what's the difference?

They share the mechanism (depend on an interface), but the intent differs. Strategy is about swapping interchangeable algorithms , often at runtime. Dependency Inversion is the broader principle that any dependency should be an abstraction. Strategy is one way of applying it.

It has a bad reputation because it's global state that hides dependencies and complicates testing. It's not forbidden, but in modern C# you usually get the same "one shared instance" by registering the type as a singleton in a Dependency Injection container instead.

Q: How do I know when I'm over-engineering with patterns?

If a pattern adds interfaces and classes but the behaviour never actually varies, it's over-engineering. Start with the simplest code that works; introduce a pattern only when a real, repeated change becomes painful without it.

Q: Why does removing observers matter so much?

An observer that subscribes but never unsubscribes is kept alive by the subject's reference, so it can never be garbage-collected — a classic memory leak. Pair every += / Subscribe with a matching -= / Unsubscribe .

No blanks this time — just a brief and an outline. Build an IOperation interface with Add and Multiply strategies, a Calculator that delegates to whichever operation it holds, then swap the strategy at runtime. Run it and check your output against the expected lines in the comments.

Practice quiz

What problem does the Strategy pattern solve?

  • Creating objects without naming the class
  • Sharing one instance app-wide
  • Swapping interchangeable algorithms behind one interface, even at runtime
  • Notifying many subscribers

Answer: Swapping interchangeable algorithms behind one interface, even at runtime. Strategy puts a family of interchangeable algorithms behind one interface so the caller can swap them at runtime.

In Strategy, what does the context class do?

  • Holds a reference to the interface and delegates the work to it
  • Implements every algorithm itself
  • Creates new objects
  • Stores global state

Answer: Holds a reference to the interface and delegates the work to it. The context holds a strategy reference and delegates to it, without knowing which concrete algorithm it uses.

What does a Factory method give back to callers?

  • A concrete class they must name
  • A static field
  • Nothing
  • An interface, while hiding which concrete class was created

Answer: An interface, while hiding which concrete class was created. A factory centralises creation and returns an interface, so callers stay decoupled from concrete types.

What guarantees a Singleton has exactly one instance accessible app-wide?

  • A public constructor
  • A private constructor plus a static access point
  • An interface
  • An event

Answer: A private constructor plus a static access point. Making the constructor private stops outside 'new' calls, and a static property exposes the single instance.

Why is a 'static readonly' field a thread-safe way to build a Singleton?

  • The runtime guarantees a type's static fields are initialised exactly once
  • It uses a lock on every read
  • It is never null
  • It runs on every thread separately

Answer: The runtime guarantees a type's static fields are initialised exactly once. The C# runtime initialises a type's static fields exactly once, so static readonly avoids the lazy-init race.

In the Observer pattern, what does the subject do when its state changes?

  • Nothing
  • Deletes its observers
  • Calls each subscribed observer to notify them
  • Creates new observers

Answer: Calls each subscribed observer to notify them. The subject keeps a list of observers and notifies each one on a change, without knowing what they do.

Why must you unsubscribe observers (e.g. -= on events)?

  • For speed
  • Otherwise the subject's reference keeps them alive — a memory leak
  • It's required by the compiler
  • To change their behaviour

Answer: Otherwise the subject's reference keeps them alive — a memory leak. An observer that subscribes but never unsubscribes is kept alive by the subject's reference and can never be garbage-collected.

What is the Dependency Inversion principle?

  • Depend on concrete classes for speed
  • Invert all loops
  • Avoid interfaces
  • Depend on an interface (abstraction), not a concrete class

Answer: Depend on an interface (abstraction), not a concrete class. Dependency Inversion (the D in SOLID) says high-level code should depend on abstractions, not concrete details.

How does a class typically receive its dependency under Dependency Inversion?

  • By calling 'new' internally
  • By asking for an interface in its constructor (injection)
  • Via a global static
  • It hard-codes it

Answer: By asking for an interface in its constructor (injection). The class asks for an interface in its constructor and the caller supplies the concrete object, keeping it swappable and testable.

What is C#'s built-in equivalent of the Observer pattern?

  • record
  • lock
  • event and delegates
  • interface

Answer: event and delegates. C# events and delegates do the Observer bookkeeping for you; prefer them over hand-rolling the observer list in production.