BrandGhost
Object-Oriented Programming in C#: State, Contracts, and Collaboration

Object-Oriented Programming in C#: State, Contracts, and Collaboration

Object-oriented programming in C# becomes easier to understand when you stop treating its vocabulary as a checklist. An object has information, offers operations, and can work with other objects. The useful questions are what information belongs together, what callers are allowed to change, and what another object must promise before you can collaborate with it.

This guide connects those questions using a small reservation example. It introduces encapsulation, abstraction, inheritance, polymorphism, and composition without turning every class into an interface or every shared operation into a base class. The goal is understanding the relationships, not collecting language features.

Example scope: Each code block is a separate, complete console-program example intended for .NET 10. The examples have not been compiled or executed. Descriptions of their behavior are source-based reasoning, not recorded output.

Begin object-oriented programming in C# with an object

The concepts answer different questions; this is a map for the examples rather than a required design checklist.

Concept Question to ask
Encapsulation Which operations preserve the object's valid state?
Abstraction What promise does a caller need?
Composition Which collaborator supplies this responsibility?
Polymorphism Which implementation does this call select?
Inheritance Is this base relationship appropriate for the contract?

Microsoft describes a class definition as a blueprint and an object as an instance. The definition describes available members. A particular instance holds the state used by those members. Two reservations can use the same definition while representing different remaining capacity.

Imagine a workshop with a fixed number of seats. Its reservation model needs to know the capacity and how many seats are reserved. The important behavior is not merely storing two numbers. It is deciding whether another reservation is allowed.

An invariant is a condition the model intends to preserve. Here, reserved seats must stay between zero and capacity. That is a rule chosen for this example, not a rule imposed by C#. A compiler can recognize a valid integer assignment without knowing whether the assignment makes sense for a workshop.

This distinction matters because syntax and meaning solve different problems. A public integer property might be convenient to access, but convenience does not establish what negative capacity means. The application has to make that decision and represent it consistently.

Start with the smallest meaningful responsibility. A reservation object need not send email, manage a database, or choose a payment provider. Those activities can be collaborators. Keeping the example small helps you see which state and operations belong together before discussing architectural boundaries.

Encapsulation protects meaningful operations

Encapsulation limits access to members. It provides a way to hide implementation details from callers. In a useful model, that boundary also lets public operations preserve the chosen rules.

The following example exposes remaining capacity as a query and reservation as an operation. Construction rejects a capacity that the example considers invalid. Reservation checks the request before updating state.

using System;

namespace ReservationExample;

internal static class Program
{
    private static void Main()
    {
        var workshop = new Workshop(12);
        workshop.Reserve(3);
        Console.WriteLine(workshop.RemainingSeats);
    }
}

public sealed class Workshop
{
    private readonly int _capacity;
    private int _reservedSeats;

    public Workshop(int capacity)
    {
        if (capacity <= 0)
        {
            throw new ArgumentOutOfRangeException(nameof(capacity));
        }

        _capacity = capacity;
    }

    public int RemainingSeats => _capacity - _reservedSeats;

    public void Reserve(int seats)
    {
        if (seats <= 0)
        {
            throw new ArgumentOutOfRangeException(nameof(seats));
        }

        if (seats > RemainingSeats)
        {
            throw new InvalidOperationException("Not enough seats remain.");
        }

        _reservedSeats += seats;
    }
}

The constructor establishes the starting rule. The method preserves it for this single-threaded example. A rejected request does not reach the assignment. Callers cannot directly assign a new reserved-seat count through the public API.

Microsoft's constructor guidance explains that constructors enable valid instances at creation. That is an opportunity, not automatic validation: the checks still need to be written. Similarly, a private field does not automatically make every public method correct.

Notice that this object is intentionally mutable. Reserving seats changes its state. Encapsulation and immutability are not synonyms. One controls the available access paths; the other concerns whether state can change. A model can allow carefully controlled changes without exposing arbitrary assignments.

The example is not a concurrency or distributed-reservation solution. It has no synchronization, persistence, or transaction boundary. Treating it as one would stretch its promise beyond the code. Its educational purpose is to connect a rule to construction, observation, and a valid transition.

Abstraction is the promise a caller uses

Abstraction is the idea of working with relevant behavior while leaving unnecessary detail behind a boundary. In the first example, the caller uses Reserve without handling the private counter directly. A class can provide that boundary without also requiring an interface.

An interface is another way to describe a contract. Microsoft's interface reference documents contracts, default implementations, and static members. Therefore, “interfaces never contain implementation” is not an accurate description of current C#.

A small interface is still useful when a caller needs a role rather than an entire concrete type. For a confirmation message, that role might be formatting a reservation summary. The caller needs the formatting operation; it does not need to know how the formatter stores configuration.

The existing explanation of using OOP interfaces effectively gives more context for contract boundaries. The important distinction here is between the operations a consumer requires and the implementation that supplies them.

Interfaces do not remove the need to explain behavior. A method signature returning a string does not say whether the string includes a heading, whether invalid input is rejected, or whether calling the method changes anything. Those expectations belong in the contract's documentation and examples.

There is also a cost to adding another type. A focused role can clarify collaboration, while an interface that repeats an entire class without a meaningful consumer boundary can add indirection. The discussion of drawbacks to using interfaces is useful when learning why abstractions should have a purpose.

Composition connects separate responsibilities

Composition describes an object working with another object as part of its implementation. Rather than inheriting a formatter, a confirmation service can receive one and ask it to format a message.

The next program separates the consumer from two formatting implementations. It uses ordinary constructor wiring, not a dependency injection container. All the types needed for the example appear in the block.

using System;

namespace ConfirmationExample;

internal static class Program
{
    private static void Main()
    {
        var plain = new ConfirmationService(new PlainFormatter());
        var detailed = new ConfirmationService(new DetailedFormatter());

        Console.WriteLine(plain.CreateMessage(3));
        Console.WriteLine(detailed.CreateMessage(3));
    }
}

public interface IReservationFormatter
{
    string Format(int seats);
}

public sealed class PlainFormatter : IReservationFormatter
{
    public string Format(int seats)
    {
        return $"Reserved seats: {seats}";
    }
}

public sealed class DetailedFormatter : IReservationFormatter
{
    public string Format(int seats)
    {
        return $"Workshop confirmation. Reserved seats: {seats}.";
    }
}

public sealed class ConfirmationService
{
    private readonly IReservationFormatter _formatter;

    public ConfirmationService(IReservationFormatter formatter)
    {
        if (formatter is null)
        {
            throw new ArgumentNullException(nameof(formatter));
        }

        _formatter = formatter;
    }

    public string CreateMessage(int seats)
    {
        if (seats <= 0)
        {
            throw new ArgumentOutOfRangeException(nameof(seats));
        }

        return _formatter.Format(seats);
    }
}

The service contains a formatter reference and delegates formatting to it. The two formatters implement the same role but provide different wording. The service validates the positive-seat assumption before calling either implementation.

This is not proof that any formatter can be substituted safely. An implementation that rejects a positive count or returns an unrelated message might violate the intended behavior. Matching the interface is the language-level requirement; honoring the meaning of the role is an additional design responsibility.

The introductory composition examples explore the relationship itself. Here, composition explains why formatting can be separate from reservation state without introducing another reservation subclass.

Polymorphism explains which implementation runs

In the confirmation example, the service calls through an interface reference. The concrete formatter supplies the implementation for that role. That is a useful way to understand polymorphic collaboration: a consumer performs one kind of operation while different implementations supply the behavior.

Class inheritance provides another route. Microsoft's polymorphism documentation describes virtual members and runtime override selection. A call through a base-class reference can invoke the derived implementation of the selected virtual member.

That rule does not apply indiscriminately to every same-named method. Hiding a member with new differs from overriding it. With hiding, the compile-time type of the reference matters. With overriding, the runtime object determines the implementation of the selected virtual member.

Keep two questions separate: which member is available at the call site, and which implementation supplies its behavior? This mental model is more useful than saying that C# always chooses the “most specific” method. The latter phrase hides the distinction between member selection and virtual dispatch.

Explicit interface implementations add another boundary. Microsoft documents that these members are callable through the specified interface, not directly as ordinary class members. A concrete object can provide different implementations for different roles without exposing both through its concrete API.

Understanding dispatch is about reading a call correctly. Understanding whether different implementations fulfill the same behavioral promise is another topic. The existing Liskov substitution discussion covers that deeper responsibility without needing to repeat its examples here.

Inheritance is a relationship, not a default reuse button

Inheritance lets a derived class participate in a base-class relationship. Virtual and abstract members can establish extension points within that relationship. The existing inheritance examples in C# provide a focused syntax introduction.

For learning purposes, distinguish inheriting a relationship from sharing a task. A specialized reservation might genuinely need the public behavior of a reservation base class. A formatter collaborating with a reservation does not become a reservation simply because both participate in confirmation.

Inheritance can centralize shared behavior and define a consistent extension point. Its tradeoff is coupling the derived implementation to the base relationship and its expectations. Composition can keep responsibilities separate, but introduces wiring and requires you to describe the collaborator's role.

Neither choice eliminates contracts. A composed formatter still has expectations. A base class still needs meaningful behavior. The decision is not whether you prefer fewer lines of code; it is whether the relationship represents specialization or collaboration.

An interface and an abstract class also answer different needs. A focused interface can describe a role across unrelated types. A base class can provide shared implementation within a hierarchy. These are explanatory distinctions, not a rule requiring one technique everywhere.

State boundaries include references

One subtle point connects encapsulation and composition: storing a reference does not give you exclusive control of the referenced object. Microsoft explains that assigning a class reference to another variable does not copy the object. Both variables can refer to the same instance.

Likewise, the readonly reference rule prevents replacing a field's reference after construction; it does not freeze the referenced object's data. The confirmation service's field cannot be reassigned by its ordinary instance methods, but readonly does not promise that every possible formatter is immutable.

A collection boundary needs the same care. Microsoft's ReadOnlyCollection documentation demonstrates a wrapper reflecting changes made to its underlying list. A read-only view is not automatically a snapshot.

Ownership is separate again. The dispose guidance distinguishes owning a disposable collaborator from merely holding its reference. A service using a dependency does not necessarily have responsibility for disposing it.

These distinctions prevent a surprisingly common misunderstanding: a clean-looking API can still expose shared, changing state or rely on a lifetime managed elsewhere. Naming and documentation should explain those boundaries rather than letting readers infer guarantees from one keyword.

Frequently asked questions

What is object-oriented programming in C#?

It is a way of organizing a program around objects, their state, their operations, and their collaboration. Classes describe available members; instances participate in the program. Encapsulation and contracts help explain what callers can depend on. The workshop example illustrates these ideas without requiring a large hierarchy.

Does every class need an interface?

No requirement in these examples demands that. The workshop exposes meaningful operations directly. The formatter interface describes a separate role used by a consumer. An interface is useful when the boundary explains collaboration, not merely because a class exists. Extra indirection needs a reason just as a new base class does.

Are encapsulated objects always immutable?

No. The workshop changes its reserved-seat count through an operation that preserves the example's rule. Encapsulation limits access; immutability concerns change. Current record and readonly documentation also require attention to shallow references rather than assuming a keyword freezes an entire object graph.

How do inheritance and composition differ?

Inheritance places a type in a base-class relationship. Composition lets an object collaborate with another object. The confirmation service contains a formatter rather than becoming one. Both techniques can organize behavior, but they express different relationships and carry different coupling and wiring responsibilities.

Does polymorphism always use a virtual class method?

No. The formatter example uses an interface contract. Virtual class members are another mechanism with documented runtime override selection. Hidden members and static interface contracts have different rules. Understanding the particular call is more precise than treating all same-named operations as equivalent dispatch.

Who owns an injected dependency?

Receiving it does not answer that question by itself. The creator, container, or documented transfer contract may manage the lifetime. Microsoft's dependency injection guidelines assign cleanup of container-created disposable services to the container. A borrower should not infer permission to dispose a collaborator just because it can call its methods.

Connect the concepts through responsibilities

Object-oriented programming in C# is not a contest to use the most abstractions. State belongs with the operations that give it meaning. Public boundaries describe what callers may request. Collaborators supply separate responsibilities, and dispatch explains how a selected operation reaches its implementation.

The workshop and formatter examples are deliberately small. Their value is the connection: construction establishes assumptions, encapsulation controls transitions, abstraction describes relevant behavior, and composition brings separate roles together. Inheritance and polymorphism become easier to understand once those responsibilities are visible.

Keep the guarantees precise. A private field is not automatic validation, a readonly reference is not deep immutability, and a dependency reference is not an ownership token. Clear object models begin by stating what is promised and what remains outside the example's scope.

Examples of Composition in C# - A Simple Guide for Beginners

Check these examples of composition in C#! Learn about composition in object-oriented programming with these simple code examples in C# - perfect for beginners!

Examples of Inheritance in C# - A Simplified Introduction to OOP

See examples of inheritance in C# in this introductory guide to object oriented programming. Learn about when to use inheritance in C# and... when not to!

State Design Pattern in C#: Complete Guide with Examples

Master the state design pattern in C# with practical examples showing state transitions, behavior changes, and finite state machine implementations.

An error has occurred. This application may no longer respond until reloaded. Reload