Skip to content

mozey/solid

Repository files navigation

SOLID

SOLID principles applied to Go with examples.

Index

Origins of SOLID, DDD, and Software Ethics

  • Michael Feathers wrote to Bob (Robert C. Martin) and said if you rearrange the order of the design principles, it spells SOLID[InfoQ Podcast]
  • Clean Architecture is a way to develop software with low coupling
  • You find the bounded context of DDD at the innermost circles of a clean architecture[InfoQ Podcast]
  • Services do not form an architecture. They form a deployment pattern[InfoQ Podcast]
  • Advantage of clean architecture is that, the highest level decisions (those that make or save the money), are unaffected by GUI, schema, and framework changes. Dependencies must point inwards[InfoQ Podcast]
  • Protect the business rules from changes
  • Sidecar (service talks to localhost) and service mesh (allows the sidecar to discover other services)InfoQ Podcast
  • Where you want to live is between convenient code, and "clean" codeInfoQ Podcast
  • Create the boundaries anyway, and then remove them if performance matters. Worse may be better, example of function calls taking nanoseconds
  • Clean architecture requires some kind of indirection to cross boundaries, polymorphism (in an Object-Oriented language) or pointers to functionsInfoQ Podcast

Go is not Object-Oriented

Go is not an Object-Oriented language, but it has some similar features

  • Methods: Define methods on structs, similar to methods on objects
  • Interfaces: Define a set of methods that a type must implicitly implement (subtype polymorphism)
  • Encapsulation: Control the visibility of fields and methods using capitalization. Use packages to organise code

Go doesn't have

  • Classes: It has structs
  • Inheritance: Promotes composition over inheritance

Composition in Go

  • Embedding: Embedding types within a struct or interface. It's like saying "this struct has-a" instead of "this struct is-a"
  • Interfaces: Specify the behavior of an "object": if something can do this, then it can be used here. Define a set of methods that a type must implement. Allows you to write code that works with any type that implements the interface, regardless of its underlying type. E.g. io.Writer interface has the Write() method

Benefits of Composition

  • Code Reusability: Reuse existing types to build new ones, avoiding code duplication
  • Flexibility: Easily swap out implementations by using interfaces. Small interfaces lead to simple implementations[Go UK 2016]
  • Loose Coupling: Components are less dependent on each other, making it easier to modify or replace them
  • Testability: Smaller components are easier to test in isolation

SOLID Principles

SOLID is an acronym that represents five key principles, that may be applied when taking a software engineering approach to Object-Oriented Design. These principles are aimed at making software more maintainable, scalable, and flexible. It helps developers create code that is easier to understand, modify, and extend over time.[Wikipedia: SOLID]

TL;DR Applied to Go, we can summarise this approach as follows:[Go UK 2016] [Go Time #16: SOLID Go Design]

Interfaces let you apply the SOLID principles to Go programs

Here's a breakdown of each principle:

S - Single Responsibility Principle (SRP)

Concept:

A class should have only one reason to change. -- Robert C. Martin, "Agile Software Development, Principles, Patterns, and Practices", 2003

This means that each class should have a single responsibility or job within the software system. If different actors (stakeholder or user) require different changes, those changes should be separated into different classes.

Benefits:

  • Improved code organization and readability.
  • Easier to maintain and modify individual classes.
  • Reduced risk of unintended side effects when changes are made.

Applied to Go:

  • Coupling: two things that change together & Cohesion: pieces of code that naturally fit together[Go UK 2016]
  • Avoid miscellaneous packages, e.g. package server, utils, common
  • Structure functions, types, and methods into packages that exhibit natural cohesion, e.g. net/http, os/exec, and encoding/json[Go UK 2016]
  • The idea of a bounded context, to group things up by responsibility, folders for cmd, internal, and domain models
  • Go stops you from importing from other projects’ internal folders[Go Time #273: Domain-driven design with Go]
  • DDD can inform SRP: Understanding the domain helps you identify the appropriate responsibilities for your classes. By focusing on domain concepts, you can avoid classes that are overloaded with unrelated tasks
  • Use composition instead of inheritance
  • Methods can be defined on structs
  • Encapsulate private struct fields, use packages to organise code
  • Types may be embedded

SRP Examples

  • Common Go Mistakes: "type embedding can help avoid boilerplate code; ensure that doing so doesn’t lead to visibility issues where some fields should have remained hidden"[Possible problems with type embedding]

O - Open/Closed Principle (OCP)

Concept:

Software modules should be open for extension, but closed for modification. -- Bertrand Meyer, "Object-Oriented Software Construction", 1988

Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification. This means that you should be able to add new functionality to a class without altering its existing code.  

Benefits:

  • Enables extending functionality without introducing bugs in existing code.
  • Promotes code reusability and maintainability.
  • Facilitates easier adaptation to changing requirements.

Applied to Go:

  • Compose simple types with embedding rather than extending them through inheritance[Go UK 2016]
  • Types are open for extension by embedding[Go UK 2016]
  • The method set of a type cannot be altered by embedding it into other types, and is therefore closed to modification
  • Methods in Go are syntactic sugar around functions with a pre-declared formal parameter, that being it's receiver.
  • Use common interfaces to add functionality without changing existing code

OCP Examples

  • Common Go Mistakes: "Abstractions should be discovered, not created. To prevent unnecessary complexity, create an interface when you need it and not when you foresee needing it"[Interface pollution]

L - Liskov Substitution Principle (LSP)

Concept:

Substitutability: if S is a subtype of T, then objects of type T can be replaced with objects of type S without altering the desirable properties of the program -- Barbara Liskov, "Data abstraction and hierarchy", 1987

Objects of a derived class should be substitutable for objects of their base class without altering the correctness of the program.

Benefits:

  • Ensures that inheritance is used correctly and consistently.
  • Prevents unexpected behavior when using subclasses.
  • Supports polymorphism and code flexibility.

Applied to Go:

  • Two types are substitutable if they exhibit behaviour such that the caller is unable to tell the difference[Go UK 2016]
  • Types that implement the same interface (implicitly) are interchangeable
  • Express dependencies between packages in terms of interfaces, not concrete types[Go UK 2016]
  • Use interfaces for: common behaviour, decoupling, and restricting behaviour

LSP Examples

I - Interface Segregation Principle (ISP)

Concept:

Clients should not be forced to depend on methods that they don't use. -- Robert C. Martin

Instead, many specific interfaces are better than one general interface. This means that interfaces should be small and focused on specific sets of related methods.

Benefits:

  • Reduces dependencies and coupling between classes.
  • Improves code flexibility and maintainability.
  • Prevents unnecessary implementations of methods.

Applied to Go:

  • Interfaces are implemented implicitly in Go
  • Small interfaces lead to simple implementations
  • Accept interfaces, return structs[Go UK 2016]
  • Define functions and methods that depend only on the behaviour that they need[Go UK 2016]

ISP Examples

D - Dependency Inversion Principle (DIP)

Concept:

High-level modules should not depend on low-level modules. Both should depend on abstractions. Abstractions should not depend on details. Details should depend on abstractions. -- Robert C. Martin

This means that classes should depend on interfaces or abstract classes rather than concrete classes.  

Benefits:

  • Decouples classes and reduces dependencies.
  • Increases code reusability and testability.
  • Makes it easier to change implementations without affecting other parts of the system.

Note:

  • The DIP guides how you structure the dependencies between different parts of the software
  • Dependency Injection (DI): is how you create and provision dependencies

Applied to Go:

  • Avoid injecting implementations (structs), inject abstractions (interfaces)
  • Types of explicit DI: Constructor or Setter (Method)
  • Structure of the import graph must be acyclic, otherwise compilation error
  • Push specifics as high as possible up the import graph[Go UK 2016]
  • Refactor dependencies from compile time to run time[Go UK 2016]

DIP Examples

  • Common Go Mistakes: "Not using the functional options pattern". Use the functional options pattern to configure options in an API-friendly manner. Works well for dependency injection. It makes your constructors more flexible, readable, and maintainable, especially when dealing with multiple dependencies and configuration options, some of which may be optional[Functional options pattern]

DI container libs:

  • For complex projects with many dependencies, use a DI or Inversion of Control (IoC) container library. A framework that automates the process of dependency injection, instead of your code explicitly creating and injecting its dependencies
  • google/wire: Operates without runtime state or reflection
  • uber-go/dig: Reflection based dependency injection toolkit, resolves the object graph during process startup

Benefits of SOLID Principles

Clean Architecture is a way to develop software with low coupling. The architecture is divided into layers, with the core business logic in the center and external concerns (like UI, database) in the outer layers. Dependencies flow inwards, towards the core business logic. Inner layers don't depend on outer layers.

SOLID principles provide guidelines for creating well-designed components that fit within the layers of Clean Architecture.

By applying both SOLID principles and Clean Architecture, you can create software that is:

Maintainable:

Is easier to understand, modify, and maintain over time.

Reusable:

Encourages the creation of reusable code components that can be used in different parts of the system.

Flexible:

Enables code that is adaptable to changing requirements, and independent of the underlying technologies.

Testable:

Easier to write test for specific functionality. Write unit tests for individual components. Integration and end-to-end tests for groups of components, swapping out dependencies for stubs (test doubles with pre-programmed responses) and mocks (test doubles with expectations) as required

Scalable:

Clean Architecture is easier to optimise, new features and requirements can be implemented without negatively impacting the performance of existing code.

Error handling in Go

Error handling in Go:

  • For maximum flexibility treat all errors as opaque
  • In situations where you cannot do that, assert errors for behaviour, instead of type or value
  • When comparing type or value, minimise the number of Sentinel Errors

About

SOLID principles applied to Go with examples

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages