# Practical SOLID in Golang: Liskov Substitution Principle

- 9 minutes read - 1876 words

#### We continue our journey through the SOLID principles by presenting the one with the most complex definition: The Liskov Substitution Principle. I’m not really a fan of reading. Often, when I do read, I find myself losing track of the text’s topic for the past few minutes. Many times, I’ll go through an entire chapter without really grasping what it was all about in the end. It can be frustrating when I’m trying to focus on the content, but I keep realizing I need to backtrack. That’s when I turn to various types of media to learn about a topic. The first time I encountered this reading issue was with the SOLID principle, specifically the Liskov Substitution Principle. Its definition was (and still is) too complicated for my taste, especially in its formal format. As you can guess, LSP represents the letter “L” in the word SOLID. It’s not difficult to understand, although a less mathematical definition would be appreciated.

## When we do not respect The Liskov Substitution

The first time we encountered this principle was in 1988, thanks to Barbara Liskov. Later, Uncle Bob shared his perspective on this topic in a paper and eventually included it as one of the SOLID principles. Let’s take a look at what it says:

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`.

Well, good luck with that definition.

No, seriously, what kind of definition is this? Even as I write this article, I’m still struggling to fully grasp this definition, despite my fundamental understanding of LSP. Let’s give it another shot:

If `S` is a subtype of `T`, then objects of type `T` in a program may be replaced with objects of type `S` without altering any of the desirable properties of that program.

Okay, this is a bit clearer now. If `ObjectA` is an instance of `ClassA`, and `ObjectB` is an instance of `ClassB`, and `ClassB` is a subtype of `ClassA` – if we use `ObjectB` instead of `ObjectA` somewhere in the code, the application’s functionality must not break. We’re talking about classes and inheritance here, two concepts that aren’t prominent in Go. However, we can still apply this principle using interfaces and polymorphism.

Wrong implementation of Update method

``````type User struct {
ID uuid.UUID
//
// some fields
//
}

type UserRepository interface {
Update(ctx context.Context, user User) error
}

type DBUserRepository struct {
db *gorm.DB
}

func (r *DBUserRepository) Update(ctx context.Context, user User) error {
return r.db.WithContext(ctx).Delete(user).Error
}
``````

In this code example, we can see one that’s quite absurd and far from best practices. Instead of updating the `User` in the database, as the `Update` method claims, it actually deletes it. But that’s precisely the point here. We have an interface, `UserRepository`, followed by a struct, `DBUserRepository`. While this struct technically implements the interface, it completely diverges from what the interface is supposed to do. In fact, it breaks the functionality of the interface rather than fulfilling its expectations. This highlights the essence of the Liskov Substitution Principle (LSP) in Go: a struct must not violate the intended behavior of the interface.

Now, let’s explore some less ridiculous examples:

Multiple implementations of UserRepository

``````type UserRepository interface {
Create(ctx context.Context, user User) (*User, error)
Update(ctx context.Context, user User) error
}

type DBUserRepository struct {
db *gorm.DB
}

func (r *DBUserRepository) Create(ctx context.Context, user User) (*User, error) {
err := r.db.WithContext(ctx).Create(&user).Error
return &user, err
}

func (r *DBUserRepository) Update(ctx context.Context, user User) error {
return r.db.WithContext(ctx).Save(&user).Error
}

type MemoryUserRepository struct {
users map[uuid.UUID]User
}

func (r *MemoryUserRepository) Create(_ context.Context, user User) (*User, error) {
if r.users == nil {
r.users = map[uuid.UUID]User{}
}
user.ID = uuid.New()
r.users[user.ID] = user

return &user, nil
}

func (r *MemoryUserRepository) Update(_ context.Context, user User) error {
if r.users == nil {
r.users = map[uuid.UUID]User{}
}
r.users[user.ID] = user

return nil
}
``````

In this example, we have a new `UserRepository` interface and two implementations: `DBUserRepository` and `MemoryUserRepository`. As we can observe, `MemoryUserRepository` includes the `Context` argument, although it’s not actually needed. It’s there just to adhere to the interface, and that’s where the problem begins. We’ve adapted `MemoryUserRepository` to conform to the interface, even though this adaptation feels unnatural. Consequently, this approach allows us to switch data sources in our application, where one source is not a permanent storage solution.

The issue here is that the Repository pattern is intended to represent an interface to the underlying permanent data storage, such as a database. It should not double as a caching system, as in the case where we store `Users` in memory. Unnatural implementations like this one can have consequences not only in terms of semantics but also in the actual code. Such situations are more apparent during implementation and challenging to rectify, often requiring significant refactoring.

To illustrate this case, we can examine the famous example involving geometrical shapes. Interestingly, this example contradicts geometric principles.

Geometrical problem

``````type ConvexQuadrilateral interface {
GetArea() int
}

type Rectangle interface {
SetA(a int)
SetB(b int)
}

type Oblong struct {
Rectangle
a int
b int
}

func (o *Oblong) SetA(a int) {
o.a = a
}

func (o *Oblong) SetB(b int) {
o.b = b
}

func (o Oblong) GetArea() int {
return o.a * o.b
}

type Square struct {
Rectangle
a int
}

func (o *Square) SetA(a int) {
o.a = a
}

func (o Square) GetArea() int {
return o.a * o.a
}

func (o *Square) SetB(b int) {
//
// should it be o.a = b ?
// or should it be empty?
//
}
``````

In the example above, we can see the implementation of geometrical shapes in Go. In geometry, we can establish subtyping relationships among convex quadrilaterals, rectangles, oblongs, and squares. When translating this concept into Go code for implementing area calculation logic, we may end up with something similar to what we see here.

At the top, we have an interface called `ConvexQuadrilateral`, which defines only one method, `GetArea`. As a subtype of `ConvexQuadrilateral`, we define an interface called `Rectangle`. This subtype includes two methods, `SetA` and `SetB`, as rectangles have two sides relevant to their area.

Next, we have the actual implementations. The first one is `Oblong`, which can have either a wider width or a wider height. In geometry, it refers to any rectangle that is not a square. Implementing the logic for this struct is straightforward.

The second subtype of `Rectangle` is `Square`. In geometry, a square is considered a subtype of a rectangle. However, if we follow this subtyping relationship in software development, we encounter an issue. A square has all four sides equal, making the `SetB` method obsolete. To adhere to the initial subtyping structure we chose, we end up with obsolete methods in our code. The same issue arises if we opt for a slightly different approach:

Another Geometrical problem

``````type ConvexQuadrilateral interface {
GetArea() int
}

type EquilateralRectangle interface {
SetA(a int)
}

type Oblong struct {
EquilateralRectangle
a int
b int
}

func (o *Oblong) SetA(a int) {
o.a = a
}

func (o *Oblong) SetB(b int) {
// where is this method defined?
o.b = b
}

func (o Oblong) GetArea() int {
return o.a * o.b
}

type Square struct {
EquilateralRectangle
a int
}

func (o *Square) SetA(a int) {
o.a = a
}

func (o Square) GetArea() int {
return o.a * o.a
}
``````

In the example above, instead of using the `Rectangle` interface, we introduced the `EquilateralRectangle` interface. In geometry, this interface represents a rectangle with all four sides equal. In this case, by defining only the `SetA` method in our interface, we avoid introducing obsolete code in our implementation. However, this approach still breaks the Liskov Substitution Principle because we introduced an additional method, `SetB`, for the `Oblong` type, which is necessary to calculate the area, even though our interface implies otherwise.

Now that we’ve started grasping the concept of The Liskov Substitution Principle in Go, let’s summarize what can go wrong if we violate it:

1. It provides a false shortcut for implementation.
2. It can lead to obsolete code.
3. It can disrupt the expected code execution.
4. It can undermine the intended use case.
5. It can result in an unmaintainable interface structure.

So, once again, let’s proceed with some refactoring.

## How we do respect The Liskov Substitution

We can achieve subtyping in Go through interfaces by ensuring that each implementation adheres to the interface’s purpose and methods.

I won’t provide the corrected implementation for the first example we encountered, as the issue is quite obvious: the `Update` method should update the `User`, not delete it. Instead, let’s focus on resolving the problem with different implementations of the `UserRepository` interface:

Repositories and Caches

``````type UserRepository interface {
Create(ctx context.Context, user User) (*User, error)
Update(ctx context.Context, user User) error
}

type MySQLUserRepository struct {
db *gorm.DB
}

type CassandraUserRepository struct {
session *gocql.Session
}

type UserCache interface {
Create(user User)
Update(user User)
}

type MemoryUserCache struct {
users map[uuid.UUID]User
}
``````

In this example, we have divided the interface into two separate interfaces, each with distinct purposes and method signatures. We now have the `UserRepository` interface, which is dedicated to permanently storing user data in some storage. To fulfill this purpose, we have provided concrete implementations such as `MySQLUserRepository` and `CassandraUserRepository`.

On the other hand, we introduced the `UserCache` interface, which serves the specific function of temporarily caching user data. As a concrete implementation of `UserCache`, we can utilize `MemoryUserCache`. Now, let’s explore a more intricate scenario in the geometrical example:

Solving Geometrical problem

``````type ConvexQuadrilateral interface {
GetArea() int
}

SetA(a int)
}

SetA(a int)
SetB(b int)
}

SetAngle(angle float64)
}

type Oblong struct {
a int
b int
}

type Square struct {
a int
}

type Parallelogram struct {
a     int
b     int
angle float64
}

type Rhombus struct {
a     int
angle float64
}
``````

To support subtyping for geometrical shapes in Go, it’s crucial to consider all of their features to avoid broken or obsolete methods. In this case, we introduced three new interfaces: `EquilateralQuadrilateral` (representing a quadrilateral with all four equal sides), `NonEquilateralQuadrilateral` (representing a quadrilateral with two pairs of equal sides), and `NonEquiangularQuadrilateral` (representing a quadrilateral with two pairs of equal angles). Each of these interfaces provides additional methods necessary to supply the required data for area calculation.

Now, we can define a `Square` interface with only the `SetA` method, an `Oblong` interface with both `SetA` and `SetB` methods, and a `Parallelogram` interface with all these methods plus SetAngle. In this approach, we didn’t strictly adhere to subtyping but focused on including necessary features. With these fixed examples, we’ve restructured our code to consistently meet end-user expectations. This also eliminates obsolete methods without breaking any existing ones, resulting in stable code.

## Conclusion

The Liskov Substitution Principle teaches us the correct way to apply subtyping. We should avoid forced polymorphism, even if it mimics real-world situations. The LSP represents the letter L in the word SOLID. While it is typically associated with inheritance and classes, which are not supported in Go, we can still apply this principle to achieve polymorphism and interfaces.