{"id":17,"date":"2024-08-24T09:08:43","date_gmt":"2024-08-24T09:08:43","guid":{"rendered":"https:\/\/contentlabstudy.com\/soft\/?p=17"},"modified":"2026-09-24T21:15:24","modified_gmt":"2026-09-24T20:15:24","slug":"the-solid-principles","status":"publish","type":"post","link":"https:\/\/contentlabstudy.com\/soft\/the-solid-principles\/","title":{"rendered":"The SOLID Principles"},"content":{"rendered":"<p><strong>SOLID<\/strong> is five object-design heuristics from Robert C. Martin\u2019s school of thought. They are about where change lands, not about \u201cmore interfaces.\u201d Use them when a class has started to rot under two reasons to change. Ignore them as a scorecard for every DTO.<\/p>\n<h2>S \u2014 Single Responsibility<\/h2>\n<p>A module should have one <em>reason to change<\/em> (one actor \/ one axis of change), not \u201cone method.\u201d A <code>User<\/code> entity that also sends email and writes audit rows will change when HR policy, SMTP, or schema changes. Split along those axes: entity, <code>EmailGateway<\/code>, <code>AuditLog<\/code>.<\/p>\n<p><code>UserManager<\/code> changes when the email rule, SMTP, or the audit table changes.<\/p>\n<pre><code>sealed class UserManager\n{\n    public void ChangeEmail(User user, string email)\n    {\n        user.ChangeEmail(email);\n        SendSmtp(email, \"changed\");\n        InsertAuditRow(\"email changed\");\n    }\n\n    static void SendSmtp(string to, string body) { }\n    static void InsertAuditRow(string message) { }\n}<\/code><\/pre>\n<p>The entity changes the email. The other two types change when their own side changes.<\/p>\n<pre><code>sealed class User\n{\n    public string Email { get; private set; } = \"\";\n    public void ChangeEmail(string email) =&gt; Email = email;\n}\n\ninterface IEmailGateway { void Send(string to, string body); }\ninterface IAuditLog { void Write(string message); }\n\nsealed class ChangeEmail\n{\n    private readonly IEmailGateway email;\n    private readonly IAuditLog audit;\n    public ChangeEmail(IEmailGateway email, IAuditLog audit)\n    {\n        this.email = email;\n        this.audit = audit;\n    }\n\n    public void Handle(User user, string next)\n    {\n        user.ChangeEmail(next);\n        email.Send(next, \"changed\");\n        audit.Write(\"email changed\");\n    }\n}<\/code><\/pre>\n<p><strong>Trap:<\/strong> a thousand one-line classes is not SRP. Cohesion still matters.<\/p>\n<h2>O \u2014 Open\/Closed<\/h2>\n<p>Open for extension, closed for modification: add a new variant without editing the switch you already tested. Mechanisms: new implementations of an interface, composition, data-driven rules. Not \u201cnever touch a file.\u201d<\/p>\n<pre><code>\/\/ A switch you edit twice a year can stay. This one gained a carrier every week.\nstatic decimal WeeklySwitch(string carrier, decimal weight) =&gt; carrier switch\n{\n    \"ups\" =&gt; weight * 2m,\n    \"fedex\" =&gt; weight * 3m,\n    \"dhl\" =&gt; weight * 4m,\n    _ =&gt; throw new ArgumentOutOfRangeException(nameof(carrier))\n};\n\ninterface IShipper { decimal Price(decimal weight); }\n\nstatic decimal Price(IShipper shipper, decimal weight) =&gt; shipper.Price(weight);<\/code><\/pre>\n<p>The new carrier is a new <code>IShipper<\/code>. The twice-a-year switch in the trap below does not need this.<\/p>\n<p><strong>Trap:<\/strong> premature strategy hierarchies for two cases. A <code>switch<\/code> you edit twice a year is fine.<\/p>\n<h2>L \u2014 Liskov Substitution<\/h2>\n<p>If client code works with type <code>T<\/code>, it must still work with any subtype of <code>T<\/code>. Subtypes must honor the parent\u2019s contract: preconditions no stronger, postconditions no weaker, invariants preserved. No surprising exceptions, no no-op methods that the parent promised would do work.<\/p>\n<p>The penguin example is only an LSP bug if <code>Bird<\/code> <em>guarantees<\/em> <code>Fly()<\/code>. Then <code>Penguin : Bird<\/code> is the wrong model. Fix the contract: <code>Bird<\/code> does not fly; <code>FlyingBird<\/code> does. Square\/rectangle is the same bug: a <code>Rectangle.SetWidth<\/code> that a <code>Square<\/code> silently couples to height breaks callers who set width then height.<\/p>\n<p>This is a bug only because <code>Rectangle<\/code> lets a caller set the sides independently. <code>Stretch<\/code> expects 20. A <code>Square<\/code> returns 25. The repair is two types that do not inherit.<\/p>\n<pre><code>class Rectangle\n{\n    public virtual int Width { get; set; }\n    public virtual int Height { get; set; }\n    public int Area =&gt; Width * Height;\n}\n\nclass Square : Rectangle\n{\n    public override int Width\n    {\n        get =&gt; base.Width;\n        set { base.Width = value; base.Height = value; }\n    }\n\n    public override int Height\n    {\n        get =&gt; base.Height;\n        set { base.Height = value; base.Width = value; }\n    }\n}\n\nstatic int Stretch(Rectangle shape)\n{\n    shape.Width = 4;\n    shape.Height = 5;\n    return shape.Area; \/\/ 20 for a Rectangle, 25 for a Square\n}<\/code><\/pre>\n<pre><code>sealed record Box(int Width, int Height)\n{\n    public int Area =&gt; Width * Height;\n}\n\nsealed record SquareShape(int Side)\n{\n    public int Area =&gt; Side * Side;\n}<\/code><\/pre>\n<h2>I \u2014 Interface Segregation<\/h2>\n<p>Do not force clients to depend on methods they never call. Fat <code>IMachine<\/code> with Print\/Scan\/Fax makes a simple printer fake a scanner. Split roles the clients actually use. This is about <em>call sites<\/em>, not about one interface per method.<\/p>\n<pre><code>interface IMachine\n{\n    void Print();\n    void Scan();\n    void Fax();\n}\n\nsealed class SimplePrinter : IMachine\n{\n    public void Print() { }\n    public void Scan() { } \/\/ this printer has nothing to scan\n    public void Fax() { }\n}\n\ninterface IPrinter { void Print(); }\n\nsealed class Printer : IPrinter\n{\n    public void Print() { }\n}<\/code><\/pre>\n<p><code>IPrinter<\/code> is the role the caller uses. It is not one interface per method.<\/p>\n<h2>D \u2014 Dependency Inversion<\/h2>\n<p>High-level policy should depend on abstractions it owns, not on SQL or HTTP details. Details depend inward. <strong>Dependency injection<\/strong> is a technique (constructor args, a container). DIP is the direction of the source dependency. You can inject <code>SqlOrderRepository<\/code> everywhere and still violate DIP if the domain references that concrete type.<\/p>\n<pre><code>static void PlaceBroken(SqlConnection db)\n{\n    _ = db.ConnectionString;\n}\n\nstatic void Place(IOrders orders) =&gt; orders.Add(new Order());<\/code><\/pre>\n<p>Passing a <code>SqlConnection<\/code> into the use case is the break, even if a container constructed it. <code>Place<\/code> depends on the port.<\/p>\n<h2>Pitfalls<\/h2>\n<ul>\n<li>SOLID as a lint rule: \u201cmust have an interface for every class.\u201d<\/li>\n<li>LSP via inheritance of convenience (<code>Stack : List<\/code>).<\/li>\n<li>DIP as \u201cI used the container,\u201d with the domain still importing Entity Framework.<\/li>\n<\/ul>\n<p>See also <a href=\"\/soft\/architectural-patterns\/\">hexagonal architecture<\/a> and <a href=\"\/soft\/code-smells\/\">code smells<\/a>.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>SOLID is five object-design heuristics from Robert C. Martin\u2019s school of thought. They are about where change lands, not about [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"site-sidebar-layout":"default","site-content-layout":"","ast-site-content-layout":"default","site-content-style":"default","site-sidebar-style":"default","ast-global-header-display":"","ast-banner-title-visibility":"","ast-main-header-display":"","ast-hfb-above-header-display":"","ast-hfb-below-header-display":"","ast-hfb-mobile-header-display":"","site-post-title":"","ast-breadcrumbs-content":"","ast-featured-img":"","footer-sml-layout":"","ast-disable-related-posts":"","theme-transparent-header-meta":"","adv-header-id-meta":"","stick-header-meta":"","header-above-stick-meta":"","header-main-stick-meta":"","header-below-stick-meta":"","astra-migrate-meta-layouts":"default","ast-page-background-enabled":"default","ast-page-background-meta":{"desktop":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"ast-content-background-meta":{"desktop":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"footnotes":""},"categories":[4],"tags":[],"class_list":["post-17","post","type-post","status-publish","format-standard","hentry","category-software-design"],"_links":{"self":[{"href":"https:\/\/contentlabstudy.com\/soft\/wp-json\/wp\/v2\/posts\/17","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/contentlabstudy.com\/soft\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/contentlabstudy.com\/soft\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/contentlabstudy.com\/soft\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/contentlabstudy.com\/soft\/wp-json\/wp\/v2\/comments?post=17"}],"version-history":[{"count":5,"href":"https:\/\/contentlabstudy.com\/soft\/wp-json\/wp\/v2\/posts\/17\/revisions"}],"predecessor-version":[{"id":3220,"href":"https:\/\/contentlabstudy.com\/soft\/wp-json\/wp\/v2\/posts\/17\/revisions\/3220"}],"wp:attachment":[{"href":"https:\/\/contentlabstudy.com\/soft\/wp-json\/wp\/v2\/media?parent=17"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/contentlabstudy.com\/soft\/wp-json\/wp\/v2\/categories?post=17"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/contentlabstudy.com\/soft\/wp-json\/wp\/v2\/tags?post=17"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}