> 2021年11月21日信息消化 ### Practical SOLID in Golang: Liskov Substitution Principle origin: [Practical SOLID in Golang: Liskov Substitution Principle](https://levelup.gitconnected.com/practical-solid-in-golang-liskov-substitution-principle-e0d2eb9dd39) We continue a journey through the SOLID principles by presenting the one which has the most complicated definition — The Liskov Substitution Principle. #### When we do not respect The Liskov Substitution The first time we heard about this principle was in 1988, by [Barbara Liskov](https://en.wikipedia.org/wiki/Barbara_Liskov). Later, [Uncle Bob](https://twitter.com/unclebobmartin) gave his [opinion](https://web.archive.org/web/20151128004108/http://www.objectmentor.com/resources/articles/lsp.pdf) on this topic in his paper and later used it as one of the SOLID principles. Let us see 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. Good luck with this definition. No, seriously, what kind of definition is it? While writing this article, I still could not catch this definition even after understanding LSP fundamentally. Let us give it another try: > 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. Ok, this is now a little bit better. 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 `ObjectA` somewhere in the code, the functionality of the application must not be broken. We talk here about classes and inheritance, two paradigms that we do not recognize in Go. Still, we may apply this principle through the usage of **interfaces** and **polymorphism**. ```go 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 } ``` Here we can see one code example. And, to be honest, I hardly could find worse and more dummy than this one. Like, instead of updating the `User` in the database, like the `Update` method says, it deletes it. But, hey, this is the point. We can see an interface, `UserRepository`. Following the interface, we have a struct, `DBUserRepository`. Although this struct implements the initial interface — it does not do what the interface claims it should. It breaks the functionality of the interface instead of following the expectation. Here is the point of LSP in Go: **struct must not violate the purpose of the interface** 它破坏了界面的功能而不是遵循预期。这是 Go 中 LSP 的要点:struct 不能违反接口的目的。 Let us now check less ridiculous examples: ```go 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 } ``` Here we have new `UserRepository` interface and its two implementations: `DBUserRepository` and `MemoryUserRepository`. As we can see, `MemoryUserRepository` does need the`Context` argument, but it is still there to respect the interface. 这里我们有新的 `UserRepository `接口及其两个实现:`DBUserRepository `和 `MemoryUserRepository`。正如我们所见, `MemoryUserRepository `确实需要 `theContext `参数,但它仍然存在以尊重接口。 Here problem begins. We adapted `MemoryUserRepository` to support the interface, even though this intention is unnatural. Consequently, we can switch data sources in our application, where one source is not permanent storage. 这里问题开始了。我们调整了`MemoryUserRepository` 来支持接口,尽管这种意图是不自然的。因此,我们可以在我们的应用程序中切换数据源,其中一个源不是永久存储。 The purpose of the [Repository](https://docs.microsoft.com/en-us/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/infrastructure-persistence-layer-design) pattern is to represent the interface to the underlying permanent data storage, like a database. It should not play the role of cache system, like here where we store `Users` in memory. Repository 模式的目的是表示底层永久数据存储的接口,如数据库。它不应该扮演缓存系统的角色,就像这里我们将用户存储在内存中一样。 Sometimes unnatural implementations have consequences in coding itself, not just semantically. These situations are more evident for realization and the most difficult for solving, as they require major refactoring. 有时,不自然的实现会对编码本身产生影响,而不仅仅是在语义上。这些情况更容易实现,也最难解决,因为它们需要重大重构。 To show this case, we can check the [famous example ](http://stg-tud.github.io/sedc/Lecture/ws13-14/3.3-LSP.html#mode=document)about geometrical shapes. Interesting about this example is that it contradicts the fact in geometry. 为了说明这种情况,我们可以查看有关几何形状的著名示例。这个例子有趣的是它与几何中的事实相矛盾。 ```go type ConvexQuadrilateral interface { GetArea() int } type Rectangle interface { ConvexQuadrilateral 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 implementation for geometrical shapes in Go. In geometry, we can compare convex quadrilateral, rectangle, oblong, and square with [subtyping](https://en.wikipedia.org/wiki/Quadrilateral#Convex_quadrilaterals). 在上面的例子中,我们可以看到 Go 中几何形状的实现。在几何中,我们可以将凸四边形、矩形、长方形和正方形与子类型进行比较。 If we move that to Go code for implementing logic for area calculation, we may end up with something similar to what we can see above. On the top, we have an interface `ConvexQuadrilateral`. This interface defines only one method, `GetArea`. As a subtype of `ConvexQuadrilateral` we can define an interface `Rectangle`. This subtype has two sides involving its area, so we must provide `SetA` and `SetB`. The next is actual implementations. The first one is `Oblong`, which can have wider width or wider height. In geometry, it is any rectangle that is not square. Implementing the logic for this struct is easy. The second subtype of `Rectangle` is `Square`. In geometry, a square is a subtype of a rectangle, but if we follow this in software development, we can only make an issue in our implementation. Square has all four equal sides. So, that makes `SetB` obsolete. To respect the subtyping we had chosen initially, we realized that our code has obsolete methods. The same issue is if we pick a slightly different path: So, we already started catching the idea of The Liskov Substitution Principle in Go. So we can summarize what can go wrong if we break it: ```go type ConvexQuadrilateral interface { GetArea() int } type EquilateralRectangle interface { ConvexQuadrilateral 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 } ``` So, we already started catching the idea of The Liskov Substitution Principle in Go. So we can summarize what can go wrong if we break it: 1. It provides a false shortcut for implementation. 2. It can cause obsolete code. 3. It can damage the expected code execution. 4. It can break the desired use case. 5. It can cause an unmaintainable interface structure. 6. … So, here we go again, let us do some refactoring. #### How we do respect The Liskov Substitution > We could provide subtyping in Go through interfaces only by respecting the interfaces' purpose and methods. I will avoid adding the proper implementation for the first example we had, as it is more than clear — the `Update` method should update the `User` instead of deleting it. So, let us first jump into fixing the issue for different implementations of the `UserRepository` interface: ```go 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 split the interface into two, with clear purpose and signatures of different methods. Now, we have the `UserRepository` interface and the `UserCache` interface. 在这个例子中,我们将接口一分为二,目的明确,不同方法的签名。现在,我们有了 `UserRepository `接口和 `UserCache `接口。 `UserRepository` purpose is now definitely to permanently store user data into some storage. For it, we prepared concrete implementations like `MySQLUserRepository` and `CassandraUserRepository`. `UserRepository `现在的目的肯定是将用户数据永久存储到某个存储中。为此,我们准备了 `MySQLUserRepository `和 `CassandraUserRepository `等具体实现。 On the other hand, we have the `UserCache` interface with a clear understanding that we need it for temporarily keeping user data in some cache. As Concrete implementation, we can use `MemoryUserCache`. Now we can switch to geometrical example, where the situation is a bit more complex: ```go type ConvexQuadrilateral interface { GetArea() int } type EquilateralQuadrilateral interface { ConvexQuadrilateral SetA(a int) } type NonEquilateralQuadrilateral interface { ConvexQuadrilateral SetA(a int) SetB(b int) } type NonEquiangularQuadrilateral interface { ConvexQuadrilateral SetAngle(angle float64) } type Oblong struct { NonEquilateralQuadrilateral a int b int } type Square struct { EquilateralQuadrilateral a int } type Parallelogram struct { NonEquilateralQuadrilateral NonEquiangularQuadrilateral a int b int angle float64 } type Rhombus struct { EquilateralQuadrilateral NonEquiangularQuadrilateral a int angle float64 } ``` #### Conclusion The Liskov Substitution Principle teaches us what the correct way of subtyping is. We should never make forced polymorphism, even that it mirrors the real-world situtaions. Liskov 替换原则告诉我们什么是正确的子类型化方法。我们永远不应该强制多态,即使它反映了现实世界的情况。 The LSP stands for the letter *L* in the word *SOLID*. Although it is bound to inheritance and classes that are not supported in Go, we can still use this principle for polymorphism and interfaces. LSP 代表单词 SOLID 中的字母 L。虽然绑定了 Go 不支持的继承和类,但我们仍然可以将这个原则用于多态和接口。 ### Misc - [Vertiwalk Vertical Walking](https://www.vertiwalk.com/) ([vertiwalk.com](https://news.ycombinator.com/from?site=vertiwalk.com)) - 科技是有温度的