An order line with a quantity of zero is a mistake, and in a program made of functions over plain data nothing stops one from existing. It can be built, passed along and stored, and it is refused only when it reaches the one function that checks. Whether that is a defect or a sound design depends on how many places build an order and how many have to trust it.

Object orientation is not one idea but several mechanisms with separate origins, and each earns its cost only where it protects an invariant or lets a decision vary on its own; where neither holds, functions over plain data remain the better design.

This part takes from Part 2 the modules of Stage 03, each hiding one decision that is likely to change, and asks what classes and interfaces add to them.

Where the mechanisms came from

Simula: classes and subclasses in a simulation language

Ole-Johan Dahl and Kristen Nygaard worked at the Norwegian Computing Center in Oslo, and the title of their 1966 paper states what their first language was for: "SIMULA: an ALGOL-based simulation language". By their own history, "The SIMULA I compiler was finished at the end of 1964".

Its successor contains the mechanism now called inheritance, under other names. The idea of "prefixing" came in December 1966: a class formed by prefixing another is a "subclass" of it. The authors wrote: "The idea of class prefixing and concatenation was the most important step towards our goal." The language was presented at Lysebu, near Oslo, on 22 to 26 May 1967, and its Common Base Definition is dated June 1967. They credit Hoare's record classes as the starting point for subclasses. The word inheritance does not occur in their history.

Abstract data types and CLU

A second line of work was about data abstraction, not simulation. In 1974 Barbara Liskov and Stephen Zilles published "Programming with abstract data types" in the proceedings of an ACM SIGPLAN symposium; Liskov had spoken on the subject in April 1973. The language that carried the idea was named in the autumn of 1973, from "cluster". In A history of CLU, Liskov describes it so: "CLU was the first implemented programming language to provide direct linguistic support for data abstraction."

Smalltalk and what Kay meant by object-oriented

Smalltalk was developed at Xerox PARC by Alan Kay's Learning Research Group. In The early history of Smalltalk, Kay dates Smalltalk-72 to 1972 to 1976, Smalltalk-76, designed by Dan Ingalls, to 1976 to 1980, and Smalltalk-80 to 1980 to 1983, with the official release in 1983.

The term is Kay's by his own account. In an e-mail of 23 July 2003 he recalls answering, probably in 1967, that what he was doing was object-oriented programming. That date is a recollection written 36 years later and hedged by its author. The same e-mail says what he meant: "OOP to me means only messaging, local retention and protection and hiding of state-process, and extreme late-binding of all things." Classes and subclasses are not in that sentence. In a message of 10 October 1998 he had already written that the term objects draws attention to the lesser idea, and that the big idea is messaging.

The textbook triad of encapsulation, inheritance and polymorphism is therefore not Kay's definition. It describes the Simula line of mechanisms, and the two meanings have coexisted since the 1970s.

C++, Java and C#

Bjarne Stroustrup's FAQ gives the dates for C++. He started the work in 1979 and the first version was called "C with Classes". The name C++, suggested by Rick Mascitti, has two dates, both Stroustrup's: coined in the summer of 1983, first used in December 1983. The first commercial implementation was released in October 1985 and the first ISO standard was ratified in 1998.

For Java, one date is verified here: Sun's JavaSoft announced the final release of Java 1.0 on 23 January 1996.

Microsoft announced C# on 26 June 2000; the language specification dates the first widely distributed implementation to July 2000. Microsoft's page The history of C# dates version 1.0 to January 2002, with Visual Studio .NET 2002, and gives its goal as a "simple, modern, general-purpose object-oriented language". The steps after it are not all object-oriented ones: generics in 2.0 (November 2005), lambda expressions in 3.0 (November 2007), async and await in 5.0 (August 2012), pattern matching in 7.0 (March 2017), records in 9 (November 2020).

The page's own summary of one of those steps: "C# 3.0 began to lay the groundwork for turning C# into a hybrid Object-Oriented / Functional language."

As of 13 November 2026 the latest version is C# 15, released in November 2026 and, by the language versioning page, "supported only on .NET 11 and newer versions". C# 14 came in November 2025 with .NET 10.

YearWhoWhat, in the original terms
1964Dahl and NygaardSIMULA I compiler finished
1966 to 1967Dahl and Nygaard"prefixing" (December 1966); Simula 67 with "class" and "subclass"
1967Strachey"parametric polymorphism" and "ad hoc polymorphism"; published in 2000
about 1967Kay"object-oriented", by his recollection in 2003
1972Parnas"information hiding" as the criterion for modules
1973 to 1974Liskov and Zilles"Programming with abstract data types"; CLU named
1979 to 1985Stroustrup"C with Classes"; the name C++ (1983); first commercial release
1985Cardelli and Wegner"inclusion polymorphism"
1987Liskovthe "substitution property"; printed in May 1988
1994Gamma, Helm, Johnson, Vlissides"Design Patterns", published 31 October 1994, copyright 1995
1996Sun (JavaSoft)Java 1.0
2002MicrosoftC# 1.0; announced on 26 June 2000

Each row is sourced in the section that discusses it.

Four ideas grouped under one name

Encapsulation is not information hiding

David Parnas's December 1972 paper proposed "information hiding" as the criterion for dividing a system into modules: "Every module in the second decomposition is characterized by its knowledge of a design decision which it hides from all others." The word encapsulation does not occur in that paper, and who first used it for objects is not established.

The two are different claims. Information hiding is a criterion for deciding what a module keeps to itself, and it needs no objects: Stage 03 applies it with static classes. Encapsulation, as the word is used for objects, is a mechanism: data and its operations are declared together and the language refuses access from outside. A class can use the mechanism and hide nothing that matters, by giving every field a public setter. In our judgement the criterion is the valuable half.

Inheritance, and the substitution property

The mechanism is Simula 67's. The word came later: Kay's history has a section titled "Inheritance" under Smalltalk-76. Who first applied it is not established.

What a subtype owes its parent was stated by Liskov in her keynote at OOPSLA '87, delivered in October 1987 and printed in May 1988. She presents it as something wanted, a "substitution property", about an object o1 of type S and an object o2 of type T: "for all programs P defined in terms of T, the behavior of P is unchanged when o1 is substituted for o2, then S is a subtype of T". She does not call it a principle. The name in use today is the title of a 1996 column by Robert C. Martin, "The Liskov Substitution Principle", which rewords it for base and derived classes; whether the name was used before that column is not established. Part 5 follows it into SOLID.

Polymorphism

Christopher Strachey distinguished "parametric polymorphism" from "ad hoc polymorphism" in lectures given in Copenhagen in August 1967, formally published only in 2000. In December 1985 Luca Cardelli and Peter Wegner refined the classification, "introducing a new form of polymorphism called inclusion polymorphism to model subtypes and inheritance". In our reading, a C# generic is parametric, an overloaded operator is ad hoc, and a call through an interface is the inclusion kind. People who say polymorphism about objects usually mean only the last.

Composition

No origin is established for the term. What is established is the maxim. "Design Patterns: Elements of Reusable Object-Oriented Software" was published on 31 October 1994 with a 1995 copyright date. Its introduction states two principles, which an interview with Erich Gamma gives as "Program to an interface, not an implementation." and "favor object composition over class inheritance". The verb is favour, not replace.

What was promised and what happened

Two promises travelled with objects: that classes would be reusable parts, and that a program would model the world it serves. The first is in the record: the 1994 book's subtitle is about reusable object-oriented software. The second has an origin that explains it: the first language with classes was a simulation language, where modelling things outside the program is the task.

What the record shows afterwards is a correction. In 1994 the best-known book on reusable object-oriented design advised favouring composition over the mechanism most associated with reuse. C#, designed as an object-oriented language, went on to add generics, lambda expressions, pattern matching and records.

The rest is our judgement, not history. In our experience reuse came from libraries with small, stable interfaces far more than from extending other people's classes, and a hierarchy copied from the nouns of a business ages badly because the business reclassifies its nouns. The objects that last are the ones that guard a rule.

Encapsulation in the example: Stage 03 against Stage 04

Stage 03 keeps the order as plain data and hides the definition of a valid line in a module:

C#
// Hides: what a valid line is and how lines add up.
public static class Subtotals
{
    public static decimal Of(OrderData order)
    {
        ArgumentNullException.ThrowIfNull(order);
        if (order.Lines.Count == 0)
        {
            throw new ArgumentException("An order needs at least one line.", nameof(order));
        }

        decimal subtotal = 0m;
        foreach (OrderLineData line in order.Lines)
        {
            if (line.Quantity < 1) throw new ArgumentException("A quantity is at least 1.", nameof(order));
            if (line.UnitPrice < 0m) throw new ArgumentException("A unit price cannot be negative.", nameof(order));
            subtotal += line.UnitPrice * line.Quantity;
        }
        return subtotal;
    }
}

Stage 04 moves the same checks into constructors. The line checks go to OrderLine (region stage04-order-line in src/Pricing/Stage04Encapsulation/Order.cs); the rest are here:

C#
public sealed class Order
{
    public const string WelcomeCoupon = "WELCOME10";

    private readonly List<OrderLine> _lines;

    public Order(IEnumerable<OrderLine> lines, CustomerTier tier, bool isFirstOrder, string? couponCode = null)
    {
        _lines = [.. lines];
        if (_lines.Count == 0)
        {
            throw new ArgumentException("An order needs at least one line.", nameof(lines));
        }
        if (!Enum.IsDefined(tier))
        {
            throw new ArgumentOutOfRangeException(nameof(tier), "Unknown customer tier.");
        }
        if (!string.IsNullOrEmpty(couponCode) && couponCode != WelcomeCoupon)
        {
            throw new ArgumentException("Unknown coupon.", nameof(couponCode));
        }

        Tier = tier;
        IsFirstOrder = isFirstOrder;
        CouponCode = string.IsNullOrEmpty(couponCode) ? null : couponCode;
    }

    public IReadOnlyList<OrderLine> Lines => _lines.AsReadOnly();
    public CustomerTier Tier { get; }
    public bool IsFirstOrder { get; }
    public string? CouponCode { get; }

    public Money Subtotal => _lines.Aggregate(Money.Zero, (sum, line) => sum + line.Total);
}

What changed: the checks run when the object is built, not when it is priced; the list is copied, private and exposed read-only; there is no setter. An Order that exists is valid, so the pricer in OrderPricer.cs validates nothing but the tax rate. What did not change: the rules, the messages and the results.

Rounding makes the same move. In Stage 03 it is a function over decimal:

C#
// Hides: how a money amount is rounded.
public static class Rounding
{
    public static decimal Money(decimal amount) =>
        Math.Round(amount, 2, MidpointRounding.AwayFromZero);
}

In Stage 04 it is an operation of a type:

C#
// A value type for amounts of money. Arithmetic keeps full precision; rounding is one
// named operation, so the rounding rule exists in exactly one place.
public readonly record struct Money(decimal Amount) : IComparable<Money>
{
    public static readonly Money Zero = new(0m);

    public static Money operator +(Money left, Money right) => new(left.Amount + right.Amount);
    public static Money operator -(Money left, Money right) => new(left.Amount - right.Amount);
    public static Money operator *(Money amount, decimal factor) => new(amount.Amount * factor);

    public static bool operator <(Money left, Money right) => left.Amount < right.Amount;
    public static bool operator >(Money left, Money right) => left.Amount > right.Amount;
    public static bool operator <=(Money left, Money right) => left.Amount <= right.Amount;
    public static bool operator >=(Money left, Money right) => left.Amount >= right.Amount;

    public int CompareTo(Money other) => Amount.CompareTo(other.Amount);

    public static Money Min(Money left, Money right) => left <= right ? left : right;

    public Money Rounded() => new(Math.Round(Amount, 2, MidpointRounding.AwayFromZero));
}

In Stage 03 the compiler accepts a tax rate added to a subtotal, or a quantity passed to Rounding.Money. In Stage 04 Money times a decimal is defined and Money plus a decimal is not.

The costs are in the same code. Money takes 21 lines where Rounding took 6, and every amount is unwrapped with .Amount when the result is built. The type gives rounding one home but does not apply it: each caller still calls Rounded(). Raw input must be mapped into the new types (region catalogue-mappings in src/Pricing/StageCatalogue.cs). And one decision moved to a worse place: to reject an unknown coupon, Order has to know the code WELCOME10, which in Stage 03 only the discount module knew. The object protects its invariant at the price of knowing a discount rule.

Polymorphism in the example: Stage 05

In Stage 04 the three discounts are fixed branches inside one method (region stage04-price). Stage 05 puts the question in an interface and gives it three answers:

C#
// The abstraction the calculator depends on. A policy answers one question: what is this
// order's discount under my rule?
public interface IDiscountPolicy
{
    string Name { get; }
    Money DiscountFor(Order order);
}
C#
public sealed class VolumeDiscount : IDiscountPolicy
{
    private static readonly Money SmallFrom = new(1000m);
    private static readonly Money LargeFrom = new(5000m);

    public string Name => "volume";

    public Money DiscountFor(Order order) =>
        order.Subtotal >= LargeFrom ? (order.Subtotal * 0.10m).Rounded()
        : order.Subtotal >= SmallFrom ? (order.Subtotal * 0.05m).Rounded()
        : Money.Zero;
}

public sealed class LoyaltyDiscount : IDiscountPolicy
{
    public string Name => "loyalty";

    public Money DiscountFor(Order order) => order.Tier switch
    {
        CustomerTier.Silver => (order.Subtotal * 0.03m).Rounded(),
        CustomerTier.Gold => (order.Subtotal * 0.07m).Rounded(),
        _ => Money.Zero,
    };
}

public sealed class CouponDiscount : IDiscountPolicy
{
    public string Name => "coupon";

    public Money DiscountFor(Order order) =>
        order.CouponCode == Order.WelcomeCoupon && order.IsFirstOrder
            ? (order.Subtotal * 0.10m).Rounded()
            : Money.Zero;
}

The calculator (region stage05-calculator in PriceCalculator.cs) receives a sequence of IDiscountPolicy, asks each for its name and its discount, and applies the cap, shipping and tax as before. The call policy.DiscountFor(order) is the polymorphic one: the object decides which method runs.

Text
PriceCalculator ----uses----> IDiscountPolicy
      |                        ^     ^     ^
      |                        |     |     |   implements
      |                   Volume  Loyalty  Coupon
      |                  Discount Discount Discount
      v                        |     |     |
Order, OrderLine, Money <------+-----+-----+   uses

What changed: the calculator names no discount, so a fourth is a new class and not an edit. What did not change: the rates, the thresholds, the cap. The host prints the same figures for all three stages:

Text
Stage 03 Modules               | mixed  | subtotal  2000.00 | discount   360.00 | shipping 0.00 | tax   377.20 | total  2017.20
Stage 04 Encapsulation         | mixed  | subtotal  2000.00 | discount   360.00 | shipping 0.00 | tax   377.20 | total  2017.20
Stage 05 Interfaces            | mixed  | subtotal  2000.00 | discount   360.00 | shipping 0.00 | tax   377.20 | total  2017.20

The proof is Every_stage_prices_every_order_in_the_table_identically in tests/Pricing.Tests/StageEquivalenceTests.cs, which runs 22 hand-computed orders against every stage, and Every_stage_rejects_invalid_input in InvalidInputTests.cs. What the interface adds is proved in StrategyTests.cs by An_order_priced_with_no_policies_has_no_discount and Replacing_a_policy_changes_the_result_as_expected.

The cost: one 40-line module became an interface, three classes and a calculator, and the order of the discount lines now belongs to whoever builds the list.

Why Stages 04 and 05 inherit from nothing

Every class in Stages 04 and 05 is sealed and none derives from another. That is deliberate.

A base class was tempting in two places. Each policy ends in a percentage of the subtotal, rounded, so a PercentageDiscount base with an abstract rate would remove the repetition. It would also bind every future discount to that shape: the flat discount written in StrategyTests.cs would not fit, and a change to the base would change all three at once. The second place is a GoldOrder deriving from Order, which fixes the tier in the object's type and needs a class for each combination of tier and first order. In both cases the base class couples things that only look alike today. The interface couples the policies to one method.

Inheritance is the right tool where the substitution property is the point. The example relies on one such case, from the framework: Stage 04 rejects an order with no lines with ArgumentException and a negative price with ArgumentOutOfRangeException, and Every_stage_rejects_invalid_input accepts both with one assertion because the second type derives from the first. A handler written for the base type works unchanged for the derived one. Outside these stages the solution has two more honest uses: the Template Method pattern, and an abstract test class in Stage08CharacterisationTests.cs that runs the same tests against two implementations.

Where plain data and functions remain the better design

Stage 03 is not a draft of Stage 05. In our judgement it stays the better design when data passes through the program and is not kept, when one place builds it and one place reads it, and when the rules change together. The example makes the point itself: PriceBreakdown, the result, is a plain record in every stage, because a result has no invariant to protect. The switch over the customer tier survives in the loyalty policy for the same reason: three tiers in a closed enumeration are a table, not a design failure.

Move to Stage 04 when invalid values start to reach code that cannot handle them. Move to Stage 05 when discounts need to be added, removed or tested one at a time.

Part 4 takes these mechanisms and examines the named, recurring solutions built from them: the 23 patterns.

What to take from this

  • Give a type a constructor that refuses bad input when several places build the value or many trust it. Otherwise leave it a plain record.
  • Before adding an interface, name the decision that will vary. If you cannot, keep the branch.
  • Seal classes by default. Derive only when every use of the base type must keep working with the derived one.
  • When two classes share code, try a shared function or a collaborator before a base class.
  • When you say object-oriented, say which mechanism you mean. They have different costs.

Run the code

The example is one download, software-foundations-example.zip: unpack it and run the commands in its foundations folder. It was built and its tests were run with the .NET 10 SDK on 13 November 2026, and it needs that SDK. .NET 10 is a long-term support release; .NET 11 was released on 10 November 2026 and the example does not need it.

  • dotnet test runs the tests.
  • dotnet run --project src/Pricing.Host prints the output shown above.

What this article showed is covered by StageEquivalenceTests.cs (Stages 03, 04 and 05 give the same result for every order in the table), InvalidInputTests.cs (each rejects every invalid input, wherever the check now lives) and StrategyTests.cs (a policy can be removed or replaced through the interface).

References

Tags#software-foundations#software-design#engineering#dotnet#development#havecode

The Foundations of Software Engineering

From the origins of programming to modern architecture. 8 parts, read in order.

  1. 01
    Before Design Patterns: How Software Engineering Learned to Manage Complexity

    From Lovelace's Notes of 1843 to Brooks's essay of 1986, each advance in programming removed a difficulty of the tools and left the difficulty of the problem. One pricing routine, written three ways with identical output, shows what structure and names buy and what they do not.

  2. 02
    Structured Programming, Modularity and Information Hiding

    Structured control flow, procedures, modules, information hiding, coupling, cohesion and separation of concerns are separate ideas from separate authors. The same pricing rules, cut as functions and then as modules, show what Parnas's criterion localises and what it does not.

  3. 03
    Object-Oriented Programming: Origins, Promises and Trade-offs

    You are here.

  4. 04
    The Gang of Four: 23 Design Patterns and the Problems They Solve

    The 23 patterns of the 1994 book, each as a problem, a shape, a cost and its modern C# form, in one pricing example. Most are a single move, an interface placed where something varies, and the example needed only one of them.

  5. 05
    SOLID, Coupling, Cohesion and Dependency Inversion

    Coupling and cohesion are the criteria; the five SOLID principles are later heuristics with separate origins in Meyer, Liskov and Martin. One pricing calculator shows what each buys and costs, and why dependency inversion, dependency injection and a container are three different things.

  6. 06
    Refactoring: Improving Design Without Changing Behaviour

    Refactoring changes structure against a recorded definition of behaviour, in steps small enough to check. One legacy pricing method is pinned by characterisation tests, reshaped in seven steps, and the tests themselves are proved able to fail.

  7. 07
    From Design Patterns to Domain-Driven Design and Modern Architecture

    Domain-driven design, ports and adapters, the onion and the clean architecture share one rule: the business rules refer to nothing else. The pricing example gains a domain model and a tested boundary, and needs 22 types where one class did.

  8. 08
    Do We Still Need Gang of Four Patterns in Modern C# and .NET?

    All 23 patterns set against C# 14 and .NET 10: eleven have a shorter form in the example, built from a language or platform feature, and twelve do not. The problems remain in every case, and each modern form has a failure of its own.

Support independent engineering knowledge

Knowledge, Analysis, the Field Guide and the tools are free to read and use, and they stay that way. If this article saved you time, you can support the research and writing behind it. Support is voluntary and unlocks nothing.

Buy me a coffee (opens in a new tab)João Lívio's page on buymeacoffee.com

Comments