The free-shipping threshold moves from 200.00 to 250.00. In Stage 05 of this series' example that is a one-line edit, and it lands in the class that also adds up the discounts, applies the 20% cap and computes the tax, so all of that is retested for a change to none of it. Whether that is a problem depends on how often the threshold moves and on who asks for it to move.
Coupling and cohesion are the criteria by which a design is judged; the five SOLID principles are later heuristics for reaching them, with separate origins and separate costs, and each is worth following only where a change you can name is expected.
This part takes from Part 3 the discount policies of Stage 05, and from Part 4 the observation that most of the 23 patterns are one indirection through an interface, and asks what justifies that indirection and what it costs.
Coupling and cohesion came first
Part 2 gave the two words their sources: Larry Constantine developed the concepts in the mid-1960s, and the article "Structured design", by W. P. Stevens, G. J. Myers and L. L. Constantine, carried them to a wide readership in the IBM Systems Journal in 1974. That is more than twenty years before SOLID.
The book followed the article. Structured Design: Fundamentals of a Discipline of Computer Program and Systems Design, by Edward Yourdon and Larry L. Constantine, is in the Computer History Museum's catalogue in its Prentice-Hall edition of 1979. The IEEE Computer Society's biography of Constantine says the books were "published successively by Yourdon Press and Prentice-Hall in 1975 and 1979". The 1975 edition rests on that sentence alone.
In the usual reading, coupling is a property of the connection between two modules: how likely a change to one is to force a change to the other. Cohesion is a property of one module: how closely the things inside it belong together. Both are measured against change.
That is why, in our judgement, the two words are the criteria and the principles are not. A criterion is a question put to finished code: if the shipping fee changes, which files change, and what else is in them? A principle is advice on how to write code that answers well, and advice can be followed to the letter and still fail the question.
The author of the principles draws the same line. In a post of 8 May 2014, Robert C. Martin traces the single responsibility principle to Parnas's 1972 paper, to Dijkstra's separation of concerns and to Constantine, writing that "the notions of Coupling and Cohesion were introduced by Larry Constantine, and amplified by Tom DeMarco, Meilir Page-Jones and many others".
He summarises it in two sentences: "Gather together the things that change for the same reasons. Separate those things that change for different reasons." In our reading the first is cohesion and the second is coupling.
Where the five principles came from
Martin's columns of 1996
On his page The Principles of OOD, Martin gives his own starting point: "In March of 1995, in comp.object, I wrote an article that was the first glimmer of a set of principles for OOD". In 1996 he wrote the "Engineering Notebook" columns for The C++ Report. Four of the five principles are stated there, in this order: open-closed, Liskov substitution, dependency inversion and interface segregation.
The year is firm and the months are not. The third column refers to the open-closed column as "last January", which places the first in January 1996; the reprints print no month, and those usually cited for the other three are not established here.
The paper most often cited as the place where the principles were first gathered is Martin's "Design Principles and Design Patterns", dated 2000. Its date, its list of principles and the frequent claim that it lacks the single responsibility principle are not established here, and nothing in this article rests on it.
Who coined the word
Martin's page lists the five as "principles of class design" and does not contain the word SOLID. The acronym is Michael Feathers's, by Martin's account in an InfoQ podcast of 24 August 2018: "I think it was 2004 and Michael wrote me an e-mail, and said that if you rearranged the principles they spell SOLID." The year is a recollection, hedged by the person recalling it. An account by Feathers is not established here.
Open-closed: Meyer's principle and Martin's reading of it
Bertrand Meyer's Object-Oriented Software Construction was first published by Prentice Hall in 1988; Meyer's own page dates the second edition to 1997. The principle is a requirement on how a system is divided, quoted here as Martin reproduces it in a post of 12 May 2014: "A satisfactory modular decomposition technique must satisfy one more requirement: It should yield modules that are both open and closed."
For Meyer a module is open if it is available for extension, and closed if it is available for use by other modules, with a stable description. His mechanism for having both was inheritance.
Martin's 1996 column introduces its statement with the words "To paraphrase him:", and the paraphrase is the sentence most people know: "SOFTWARE ENTITIES (CLASSES, MODULES, FUNCTIONS, ETC.) SHOULD BE OPEN FOR EXTENSION, BUT CLOSED FOR MODIFICATION."
The column also moves the mechanism, from inheriting an implementation to depending on an abstraction: abstract base classes and polymorphism. The two are not the same advice. Meyer's keeps a module stable for the modules that already use it. Martin's asks that new behaviour arrive as a new implementation of an abstraction, as in Stage 05.
Substitution: a property, a requirement, then a principle
Part 3 quoted Barbara Liskov's keynote at OOPSLA '87, delivered in October 1987 and reprinted in May 1988. She describes a "substitution property" as something wanted of subtypes. She does not name a principle.
The formal treatment is Liskov and Jeannette Wing's "A Behavioral Notion of Subtyping", in ACM Transactions on Programming Languages and Systems, November 1994. Its "Subtype Requirement" is given here from the authors' technical report of July 1999, which says it is based on that paper: "Let φ(x) be a property provable about objects x of type T. Then φ(y) should be true for objects y of type S where S is a subtype of T."
The name and the wording for classes are Martin's. His second column of 1996, "The Liskov Substitution Principle", presents its statement as a paraphrase: "FUNCTIONS THAT USE POINTERS OR REFERENCES TO BASE CLASSES MUST BE ABLE TO USE OBJECTS OF DERIVED CLASSES WITHOUT KNOWING IT." Whether the name was in use before that column is not established.
Single responsibility: restated by its own author
The single responsibility principle is Martin's. His Principles of OOD page, in existence by 2005, words it as "A class should have one, and only one, reason to change." The year it first appeared in print is not established here; it is usually given as his book Agile Software Development: Principles, Patterns, and Practices, dated 2002 or 2003.
In the 2014 post he restated it for modules and said what a reason to change is: a person or group the module answers to. He adds that he has "a vague feeling" he took the name from Meyer and could not confirm it. He restated the principle again on 18 October 2020, in Solid Relevance.
| Year | Who | What, in the original terms |
|---|---|---|
| mid-1960s, 1968 | Constantine | "coupling" and "cohesion" developed; first published in 1968, in a publication the biography does not identify |
| 1974 | Stevens, Myers and Constantine | "Structured design", IBM Systems Journal |
| 1975 and 1979 | Yourdon and Constantine | Structured Design; the 1979 edition is in a museum catalogue, the 1975 edition rests on one source |
| 1987 | Liskov | "substitution property"; keynote of October 1987, reprinted in May 1988 |
| 1988 | Meyer | modules "both open and closed" |
| 1988 | Johnson and Foote | "inversion of control" |
| 1994 | Liskov and Wing | "Subtype Requirement" |
| 1995 | Martin | an article of March 1995 in comp.object, "the first glimmer of a set of principles for OOD" |
| 1996 | Martin | four columns in The C++ Report: open-closed, Liskov substitution, dependency inversion, interface segregation |
| 2000 | Martin | "Design Principles and Design Patterns"; commonly cited, contents not established here |
| 2004 | Fowler and "various IoC advocates" | the name "Dependency Injection", 23 January 2004 |
| about 2004 | Feathers | "SOLID", by Martin's recollection in 2018 |
| 2014 | Martin | single responsibility restated: a reason to change is a person or group |
| 2020 | Martin | "Solid Relevance" |
Each row is sourced in the section that discusses it.
Five principles against one calculator
Stage 05 has the three policies that Part 3 showed and one class that uses them:
// The calculator knows that discounts exist, add up and are capped. It does not know
// which discounts there are: a new one is a new class, not a change to this one.
public sealed class PriceCalculator(IEnumerable<IDiscountPolicy> policies)
{
private const decimal DiscountCapRate = 0.20m;
private static readonly Money FreeShippingFrom = new(200m);
private static readonly Money ShippingFee = new(7.50m);
private readonly IReadOnlyList<IDiscountPolicy> _policies = [.. policies];
public PriceBreakdown Price(Order order, decimal taxRate = 0.23m)
{
if (taxRate is < 0m or > 1m)
{
throw new ArgumentOutOfRangeException(nameof(taxRate), "The tax rate lies between 0 and 1.");
}
Money subtotal = order.Subtotal;
List<DiscountLine> discounts =
[.. _policies.Select(policy => new DiscountLine(policy.Name, policy.DiscountFor(order).Amount))];
Money requested = new(discounts.Sum(discount => discount.Amount));
Money discount = Money.Min(requested, (subtotal * DiscountCapRate).Rounded());
Money afterDiscount = subtotal - discount;
Money shipping = afterDiscount >= FreeShippingFrom ? Money.Zero : ShippingFee;
Money tax = ((afterDiscount + shipping) * taxRate).Rounded();
Money total = (afterDiscount + shipping + tax).Rounded();
return new PriceBreakdown(
subtotal.Amount, discounts, discount.Amount, shipping.Amount, tax.Amount, total.Amount);
}
}Single responsibility
Whom does the calculator answer to? It holds the cap rate, the shipping fee, the free-shipping threshold and the valid range of a tax rate. If marketing decides the cap, logistics the shipping and finance the tax, three groups can each ask for an edit to this one class. If one person decides all four, it has one reason to change and is fine as it stands.
Stage 07 takes one of the four out. Shipping becomes a question put to an interface:
public interface IShippingPolicy
{
Money ShippingFor(Money subtotalAfterDiscount);
}
public sealed class FlatRateShipping(Money fee, Money freeFrom) : IShippingPolicy
{
public Money ShippingFor(Money subtotalAfterDiscount) =>
subtotalAfterDiscount >= freeFrom ? Money.Zero : fee;
}// Constructor injection: the class states what it needs and never builds it. Everything
// it collaborates with arrives from outside, so a test or a different deployment can
// hand it something else without touching this code.
public sealed class PricingService(IEnumerable<IDiscountPolicy> policies, IShippingPolicy shipping)
{
private const decimal DiscountCapRate = 0.20m;
private readonly IReadOnlyList<IDiscountPolicy> _policies = [.. policies];
public PriceBreakdown Price(Order order, decimal taxRate = 0.23m)
{
if (taxRate is < 0m or > 1m)
{
throw new ArgumentOutOfRangeException(nameof(taxRate), "The tax rate lies between 0 and 1.");
}
Money subtotal = order.Subtotal;
List<DiscountLine> discounts =
[.. _policies.Select(policy => new DiscountLine(policy.Name, policy.DiscountFor(order).Amount))];
Money requested = new(discounts.Sum(discount => discount.Amount));
Money discount = Money.Min(requested, (subtotal * DiscountCapRate).Rounded());
Money afterDiscount = subtotal - discount;
Money shippingCost = shipping.ShippingFor(afterDiscount);
Money tax = ((afterDiscount + shippingCost) * taxRate).Rounded();
Money total = (afterDiscount + shippingCost + tax).Rounded();
return new PriceBreakdown(
subtotal.Amount, discounts, discount.Amount, shippingCost.Amount, tax.Amount, total.Amount);
}
}What changed between the two classes: the constants FreeShippingFrom and ShippingFee are gone, the constructor takes a second parameter, the comparison with the threshold is now a call to shipping.ShippingFor, and the local variable is renamed shippingCost. What did not change: the check on the tax rate, the discount lines, the cap, the tax, the total, and the rounding of each. The host prints the same figures for both:
Stage 05 Interfaces | capped | subtotal 6000.00 | discount 1200.00 | shipping 0.00 | tax 1104.00 | total 5904.00
Stage 07 Dependency injection | capped | subtotal 6000.00 | discount 1200.00 | shipping 0.00 | tax 1104.00 | total 5904.00The proof is Every_stage_prices_every_order_in_the_table_identically in tests/Pricing.Tests/StageEquivalenceTests.cs, which runs 22 hand-computed orders against both.
The cost is in the same code. Ten lines, an interface and a class were added to move one line, and the service still holds the cap rate and validates the tax rate. The fee and the threshold did not disappear: they are written wherever a FlatRateShipping is built, which in this solution is two places.
The known difficulty with this principle is its unit. A reason to change is not visible in code: it is a fact about an organisation, and two reviewers who picture different organisations split the same class differently. The common misreading is that a class should do one thing. LoyaltyDiscount contains a switch over three tiers and has one reason to change.
Open-closed
The problem this answers is in Stage 04, where a fourth discount is an edit in the middle of the one method of OrderPricer (region stage04-price). In Stage 05 it is a new class, and the calculator is not opened. The test Replacing_a_policy_changes_the_result_as_expected in StrategyTests.cs does exactly that: it declares a FlatDiscount the production code has never heard of and checks the result against arithmetic done by hand.
The calculator is closed against one kind of change: a new rule that looks at an order and returns an amount. It is not closed against a discount that depends on another discount, or a cap that varies by customer. Closure is relative to a change somebody predicted.
Something is modified in every case. Stage 06 selects policies by name, and the names live in a table:
// Policies picked by name at run time, for example from configuration.
public static class PolicySelection
{
private static readonly Dictionary<string, Func<IDiscountPolicy>> Known = new()
{
["volume"] = () => new VolumeDiscount(),
["loyalty"] = () => new LoyaltyDiscount(),
["coupon"] = () => new CouponDiscount(),
};
public static IReadOnlyList<IDiscountPolicy> Select(params string[] names) =>
[.. names.Select(name => Known.TryGetValue(name, out Func<IDiscountPolicy>? create)
? create()
: throw new ArgumentException($"Unknown discount policy '{name}'.", nameof(names)))];
}A fourth policy is a new class and a new line in Known. The edit has not been removed; it has moved to a place where it is one line long and cannot break the arithmetic.
The cost is paid by the reader. In Stage 04 the method says which discounts apply. In Stage 05 you find whoever builds the list, and the order of the discount lines, which the rules fix as volume, loyalty, coupon, is the order of that list.
The misreading is that existing code must never be edited, which is not Meyer's meaning. In our judgement a branch is the right form until the same kind of edit arrives a second or third time. Adding a tier to LoyaltyDiscount is an edit to an enumeration and a switch, and that is the right size for it.
Liskov substitution
Every class in Stages 04 to 07 is sealed, so Martin's wording about base and derived classes has nothing to apply to. Liskov's property and the Liskov and Wing requirement are about types and behaviour, and they cover the implementations of an interface: any IDiscountPolicy must be usable where the calculator expects one.
The interface does not say what the calculator expects. Its signature promises a Money. The calculator relies on more: that the amount is not negative, and that it is already rounded, because the calculator sums the amounts as they arrive. A policy that returned a negative amount would compile and would satisfy the interface. The sum would fall, Money.Min would pick it over the cap, and the order would be priced above its subtotal before shipping and tax. Nothing in Stage 05 refuses that.
This is the gap the 1994 formulation names: clients rely on properties of behaviour, and a type checker verifies signatures. A fourth policy is protected by nothing but its author's reading of the other three.
Closing the gap costs either tests that every implementation must pass or a narrower type. Stage 09, which Part 7 covers, takes the second route: a discount there is a Percentage, valid only from 0 to 100.
The misreading is that the principle is a rule about inheritance, checked by asking whether one noun is a kind of another. The property is about what programs can observe. Read strictly, every provable property must hold, so in practice somebody has to decide which properties callers depend on.
Interface segregation
Martin's fourth column states it as "CLIENTS SHOULD NOT BE FORCED TO DEPEND UPON INTERFACES THAT THEY DO NOT USE." The example has nothing to fix here: it never merged discounts and shipping into one interface. IDiscountPolicy has two members, a name and a method, and each of its three clients, the calculator of Stage 05, the pricer of Stage 06 and the service of Stage 07, uses both. IShippingPolicy has one method.
At its limit the principle produces interfaces of one method, and an interface with one method and no state is a function. Stage 06 shows this in src/Pricing/Stage06Strategy/Strategy.cs: the same three rules exist there as Func<Order, Money> values, and the test The_interface_form_and_the_delegate_form_give_the_same_result runs all 22 orders through both forms. We would keep the interface when a policy has several operations or is found by a container, and use the delegate when the rule is one expression.
Dependency inversion
The third column of 1996 states the principle in two parts. The first: "A. HIGH LEVEL MODULES SHOULD NOT DEPEND UPON LOW LEVEL MODULES. BOTH SHOULD DEPEND UPON ABSTRACTIONS."
The second: "B. ABSTRACTIONS SHOULD NOT DEPEND UPON DETAILS. DETAILS SHOULD DEPEND UPON ABSTRACTIONS."
In Stage 04 the routine that knows the order of the steps contains the detail of every discount. In Stage 05 the calculator names IDiscountPolicy and so does VolumeDiscount. Neither names the other. That is the inversion: the detail now points at an abstraction, where before the calculator pointed at the detail.
Stage 04 OrderPricer (rules and rates in one method)
Stage 07 CompositionRoot ..builds..> every concrete class
|
v
PricingService
| |
v v
IDiscountPolicy IShippingPolicy
^ ^ ^ ^
| | | | implements
Volume Loyalty Coupon FlatRateShippingThe example goes this far and no further. The calculator, the interface and the policies share one assembly and one namespace, and nothing but convention stops the calculator from constructing a VolumeDiscount itself. The direction becomes a checked rule only in Stage 10, which is Part 7's subject. And not everything is behind an abstraction: the calculator and every policy depend directly on the concrete Order and Money of Stage 04. In our judgement that is correct: those types are stable, and an interface over them would add a name and remove nothing.
The cost is in the diagram: more boxes, and a reader who must follow an arrow to learn what runs.
Dependency inversion, dependency injection and a container are three things
The commonest misreading treats these terms as synonyms. They have different authors and different dates.
Inversion of control is the oldest in print. Ralph Johnson and Brian Foote's "Designing Reusable Classes", in the Journal of Object-Oriented Programming of June/July 1988, describes a framework that calls the user's code: "This inversion of control gives frameworks the power to serve as extensible skeletons." Martin Fowler, in a note of 26 June 2005, reports that the authors believe they took the term from somewhere else.
The dependency inversion principle is Martin's, 1996. Dependency injection is a name from 2004. Fowler's article of 23 January 2004, Inversion of Control Containers and the Dependency Injection pattern, argues that inversion of control was too generic a name for what the lightweight containers of the time did, and records how the new one was chosen: "As a result with a lot of discussion with various IoC advocates we settled on the name Dependency Injection." Fowler did not coin it alone. The article describes constructor, setter and interface injection, and contrasts them with a service locator.
| Term | First recorded | What it is about | Where it is in the example |
|---|---|---|---|
| Inversion of control | Johnson and Foote, 1988 | a framework calls application code | the test runner calls the tests; the container calls the constructors |
| Dependency inversion principle | Martin, 1996 | the direction of source dependencies | PriceCalculator and VolumeDiscount both name IDiscountPolicy |
| Dependency injection | Fowler and others, 2004 | a class receives its collaborators from outside | the constructors of PriceCalculator and PricingService |
| Container | a library, not a principle | builds the object graph and manages lifetimes | ContainerRoot in src/Pricing.Host |
Stage 05 already injects its dependencies: the calculator receives its policies through its constructor. What Stage 07 adds is a second dependency and a named place where the graph is built.
The composition by hand
// The composition root: the one place that knows every concrete class and wires the
// object graph by hand. No container is needed to inject dependencies; a container only
// automates this function.
public static class CompositionRoot
{
public static PricingService CreatePricingService() =>
new(
policies: [new VolumeDiscount(), new LoyaltyDiscount(), new CouponDiscount()],
shipping: new FlatRateShipping(fee: new Money(7.50m), freeFrom: new Money(200m)));
}This function is the only code in Stage 07 that names a concrete policy, and the compiler checks it: leave out the shipping argument and the solution does not build. A rule now lives here too: one mutation of the counter-proof script changes the free-shipping threshold in this function from 200 to 250 and requires the tests to fail.
The same graph in a container
Microsoft's documentation describes what the platform provides: ".NET supports the dependency injection (DI) software design pattern, which is a technique for achieving Inversion of Control (IoC) between classes and their dependencies." The host project registers Stage 07's graph with that container:
// Stage 07's object graph, registration for registration. Registering three
// implementations of one interface injects all three, in this order, wherever an
// IEnumerable<IDiscountPolicy> is asked for.
private static void AddStage07(ServiceCollection services)
{
services.AddSingleton<IDiscountPolicy, VolumeDiscount>();
services.AddSingleton<IDiscountPolicy, LoyaltyDiscount>();
services.AddSingleton<IDiscountPolicy, CouponDiscount>();
services.AddSingleton<IShippingPolicy>(
new FlatRateShipping(fee: new Money(7.50m), freeFrom: new Money(200m)));
services.AddTransient<PricingService>();
}The host asks the container for a PricingService and prints what it computes. The_hand_written_root_and_the_container_root_give_the_same_result, in CompositionTests.cs, compares the two roots for every order in the table.
Stage 07 Dependency injection | mixed | subtotal 2000.00 | discount 360.00 | shipping 0.00 | tax 377.20 | total 2017.20
Stage 07 through the container | mixed | subtotal 2000.00 | discount 360.00 | shipping 0.00 | tax 377.20 | total 2017.20The container reads the constructor of PricingService and finds the arguments itself. It also manages lifetimes, which the documentation gives as three: "Services can be registered with a transient, scoped, or singleton lifetime." The host shows them with the services of Stage 10:
Lifetime singleton | one instance across two scopes: True
Lifetime scoped | one instance within a scope: True; across two scopes: False
Lifetime transient | one instance for two requests: FalseThe container adds none of the design. It inverts no dependency: the interfaces existed in Stage 05. It also changes when a mistake is found. A missing argument in the hand-written root is a compiler error. A missing registration is an exception while the program runs, which is why the host switches on the container's two validation options. And one rule became implicit: the policies reach the service in the order of the registration lines, which the test The_container_injects_the_three_policies_in_registration_order pins.
No class in the Pricing project knows the container exists. That is the difference from a service locator: the classes receive what they need and never ask a registry for it.
In our judgement the hand-written root is the better choice for a program of this size. A container earns its indirection when the graph is large, when lifetimes differ, or when the framework already supplies one.
What the criticism gets right
Little published criticism of SOLID under that name is established here. The record of how the principles were made supports the usual objections.
They are heuristics, and their sources say so. Meyer wrote of a requirement on a decomposition technique. Liskov wrote of a property that is wanted. The imperative sentences in capitals are Martin's paraphrases of 1996, and the one principle that is wholly his has been restated by him twice. Treating the five as laws, enforced in review by name, is a use their history does not support.
Interfaces multiply. Stage 04 answers every question about pricing in one static class. Stage 07 answers them with eight types in two files: two interfaces, four implementations, a service and a composition root. The results are identical to the cent, so the eight types are justified only by the changes they make cheap.
An interface with one implementation is a claim about the future. IShippingPolicy has a single implementation, and no test in the solution substitutes another. The variation that exists, a fee and a threshold, is carried by two constructor arguments. We would have given the service those two values and added the interface when a second shipping rule appeared. IDiscountPolicy is the contrast: three implementations in production, a fourth in the tests, three clients. It earns its place.
None of the five says which change is coming. That judgement comes from the coupling question, asked about a change that has already happened once, and older designs can pass it: Stage 03's modules, with no interface at all, answer it for a change of shipping fee with one module of nine lines.
Part 6 starts from code that has none of these shapes and shows how to move it towards them in small steps that leave its behaviour unchanged.
What to take from this
- Before applying a principle, name a change and ask: which files change, and what else is in them? Cite a principle in review only with the change it protects against.
- Extract an interface when a second implementation exists or a test needs to substitute one. Until then, pass values, or a delegate when the rule is one expression.
- Treat the signature of an interface as half its contract. Write the other half as tests that every implementation has to pass.
- Keep one composition root and write it by hand first. Add a container when lifetimes or the size of the graph justify it, and keep it out of every class it builds.
- When somebody says dependency injection, ask which they mean: a direction of dependency, the passing of a collaborator, or a library.
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 testruns the tests.dotnet run --project src/Pricing.Hostprints the output shown above../counterproof.shchanges one rule at a time in a copy of the solution and requires the tests to fail each time. One mutation is the free-shipping threshold in the Stage 07 composition root.
What this article showed is covered by StageEquivalenceTests.cs (Stages 05, 06 and 07 give the same result for every order in the table), StrategyTests.cs (a policy can be replaced, and the interface and delegate forms agree) and CompositionTests.cs (the two roots agree, the policies arrive in registration order, and the lifetimes are as printed).
References
- IEEE Computer Society, "Larry L. Constantine", Computer Pioneers.
- Computer History Museum, catalogue record 102742907: Edward Yourdon and Larry L. Constantine, Structured Design: Fundamentals of a Discipline of Computer Program and Systems Design, Prentice-Hall, 1979.
- Robert C. Martin, "The Single Responsibility Principle", The Clean Code Blog, 8 May 2014.
- Robert C. Martin, "The Principles of OOD", author's site.
- Robert C. Martin, "The Open-Closed Principle", Engineering Notebook, The C++ Report, 1996.
- Robert C. Martin, "The Liskov Substitution Principle", Engineering Notebook, The C++ Report, 1996.
- Robert C. Martin, "The Dependency Inversion Principle", Engineering Notebook, The C++ Report, 1996.
- Robert C. Martin, "The Interface Segregation Principle", Engineering Notebook, The C++ Report, 1996.
- Wes Reisz with Robert C. Martin, "Uncle Bob Martin on Clean Software, Craftsperson, Origins of SOLID, DDD, & Software Ethics", InfoQ podcast, 24 August 2018.
- Bertrand Meyer, Object-Oriented Software Construction, second edition, author's page; first edition Prentice Hall, 1988.
- Robert C. Martin, "The Open Closed Principle", The Clean Code Blog, 12 May 2014.
- Barbara Liskov, "Data Abstraction and Hierarchy", OOPSLA '87 keynote; ACM SIGPLAN Notices 23(5), May 1988.
- Barbara H. Liskov and Jeannette M. Wing, "Behavioral Subtyping Using Invariants and Constraints", CMU-CS-99-156, Carnegie Mellon University, July 1999.
- Robert C. Martin, "Solid Relevance", The Clean Code Blog, 18 October 2020.
- Ralph E. Johnson and Brian Foote, "Designing Reusable Classes", Journal of Object-Oriented Programming 1(2), June/July 1988.
- Martin Fowler, "InversionOfControl", 26 June 2005.
- Martin Fowler, "Inversion of Control Containers and the Dependency Injection pattern", 23 January 2004.
- Microsoft Learn, ".NET dependency injection".
- Microsoft Learn, "Service lifetimes".
The Foundations of Software Engineering
From the origins of programming to modern architecture. 8 parts, read in order.
- 01Before 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.
- 02Structured 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.
- 03Object-Oriented Programming: Origins, Promises and Trade-offs
Object orientation is several mechanisms with separate origins: Simula's subclasses, abstract data types, Smalltalk's messaging. One pricing example shows what encapsulation and an interface buy, what they cost, and when functions over plain data stay the better design.
- 04The 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.
- 05SOLID, Coupling, Cohesion and Dependency Inversion
You are here.
- 06Refactoring: 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.
- 07From 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.
- 08Do 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