Golang中的SOLID原则


SOLID原则是2002年提出的,关于这个原则的描述和应用大多都是用Java代码作为示例,对于在SOLID原则之后十多年才被创造出来的Golang跟Java这类古老的语言还是有明显的差异的,所以有必要看看如何基于SOLID原则设计Golang的程序。

单一职责原则

A class should have one, and only one, reason to change.

单一职责最大的好处是最小的代码量达成修改目标,提到最小修改代码量就是涉及到耦合和内聚问题。Go里面没有class的概念,在Go中所有的代码都在一个package中,通过import使两个package建立源码级别的耦合,因而从粗粒度上来看Go的单一职责发生在package,当然从函数和方法的角度也会涉及到单一职责问题,这个问题是各种语言都会碰到的。Go标准库中的一些优秀 package 示例:

  • net/http – 提供 http 客户端和服务端
  • os/exec – 执行外部命令
  • encoding/json – 实现 JSON 文档的编码和解码

开放/封闭原则

Software entities should be open for extension, but closed for modification.

如下面的代码所示,我们有一个go类型A ,有一个字段year和一个方法Greet。我们有第二种类型B,它嵌入了一个A,因为A嵌入,因此调用者看到 B 的方法覆盖了A的方法。因为A作为字段嵌入B ,B可以提供自己的Greet方法,掩盖了A的Greet方法。但嵌入不仅适用于方法,还可以访问嵌入类型的字段。如您所见,因为A和B都在同一个包中定义,所以B可以访问A的私有year字段,就像在B中声明一样。因此嵌入是一个强大的工具,允许go的类型对扩展开放。

package main

type A struct {
        year int
}

func (a A) Greet() { fmt.Println("Hello Golang", a.year) }

type B struct {
        A
}

func (b B) Greet() { fmt.Println("Welcome to Golang", b.year) }

func main() {
        var a A
        a.year = 2021
        var b B
        b.year = 2021
        a.Greet() // Hello Golang 2021
        b.Greet() // Welcome to Golang 2021
}

里氏替换原则

Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it.

在基于类的语言中,里氏替换原则通常被解释为,具有各种具体子类型的抽象基类的规范,但是go没有类和继承,所以没有办法根据抽象类层次结构实现替换,go中需要实现替换的是接口(即interface),这一点深刻影响了go的变成范式:go的类型(struct)不需要指定它们实现特定接口(interface)而是任何类型都可以实现接口,只要它具有的函数签名与接口声明的方法匹配即可

如下代码所示,crypto.randos.File都实现了io.Reader接口,分别操作/dev/random设备和磁盘文件:

// io.Reader
type Reader interface {
        // Read reads up to len(buf) bytes into buf.
        Read(buf []byte) (n int, err error)
}

// crypto.rand.Reader
type devReader struct {
    name string
    f    io.Reader
    mu   sync.Mutex
    used int32 // atomic; whether this devReader has been used
}
func (r *devReader) Read(b []byte) (n int, err error) {
    if atomic.CompareAndSwapInt32(&r.used, 0, 1) {
        // First use of randomness. Start timer to warn about
        // being blocked on entropy not being available.
        t := time.AfterFunc(60*time.Second, warnBlocked)
        defer t.Stop()
    }
    if altGetRandom != nil && r.name == urandomDevice && altGetRandom(b) {
        return len(b), nil
    }
    r.mu.Lock()
    defer r.mu.Unlock()
    if r.f == nil {
        f, err := os.Open(r.name)
        if f == nil {
            return 0, err
        }
        if runtime.GOOS == "plan9" {
            r.f = f
        } else {
            r.f = bufio.NewReader(hideAgainReader{f})
        }
    }
    return r.f.Read(b)
}

// os.File
type File struct {
    *file // os specific
}
func (f *File) Read(b []byte) (n int, err error) {
    if err := f.checkValid("read"); err != nil {
        return 0, err
    }
    n, e := f.read(b)
    return n, f.wrapErr("read", e)
}

接口隔离原则

Clients should not be forced to depend on methods they do not use.

接口隔离原则简单来说就是建立单一的接口, 不要建立臃肿庞大的接口。也就是接口尽量细化,同时接口中的方法尽量少,保持接口纯洁性。go的标准库io里面定义了大量的interface,每个interface的函数都不多,由于interface都比较小,所以他们能够方便的互相组合,比如ReadWriter是有Reader和Writer组成的。

// Reader is the interface that wraps the basic Read method.
//
// Read reads up to len(p) bytes into p. It returns the number of bytes
// read (0 <= n <= len(p)) and any error encountered. Even if Read
// returns n < len(p), it may use all of p as scratch space during the call.
// If some data is available but not len(p) bytes, Read conventionally
// returns what is available instead of waiting for more.
//
// When Read encounters an error or end-of-file condition after
// successfully reading n > 0 bytes, it returns the number of
// bytes read. It may return the (non-nil) error from the same call
// or return the error (and n == 0) from a subsequent call.
// An instance of this general case is that a Reader returning
// a non-zero number of bytes at the end of the input stream may
// return either err == EOF or err == nil. The next Read should
// return 0, EOF.
//
// Callers should always process the n > 0 bytes returned before
// considering the error err. Doing so correctly handles I/O errors
// that happen after reading some bytes and also both of the
// allowed EOF behaviors.
//
// Implementations of Read are discouraged from returning a
// zero byte count with a nil error, except when len(p) == 0.
// Callers should treat a return of 0 and nil as indicating that
// nothing happened; in particular it does not indicate EOF.
//
// Implementations must not retain p.
type Reader interface {
    Read(p []byte) (n int, err error)
}

// Writer is the interface that wraps the basic Write method.
//
// Write writes len(p) bytes from p to the underlying data stream.
// It returns the number of bytes written from p (0 <= n <= len(p))
// and any error encountered that caused the write to stop early.
// Write must return a non-nil error if it returns n < len(p).
// Write must not modify the slice data, even temporarily.
//
// Implementations must not retain p.
type Writer interface {
    Write(p []byte) (n int, err error)
}

// Closer is the interface that wraps the basic Close method.
//
// The behavior of Close after the first call is undefined.
// Specific implementations may document their own behavior.
type Closer interface {
    Close() error
}

// Seeker is the interface that wraps the basic Seek method.
//
// Seek sets the offset for the next Read or Write to offset,
// interpreted according to whence:
// SeekStart means relative to the start of the file,
// SeekCurrent means relative to the current offset, and
// SeekEnd means relative to the end.
// Seek returns the new offset relative to the start of the
// file and an error, if any.
//
// Seeking to an offset before the start of the file is an error.
// Seeking to any positive offset is legal, but the behavior of subsequent
// I/O operations on the underlying object is implementation-dependent.
type Seeker interface {
    Seek(offset int64, whence int) (int64, error)
}

// ReadWriter is the interface that groups the basic Read and Write methods.
type ReadWriter interface {
    Reader
    Writer
}

// ReadCloser is the interface that groups the basic Read and Close methods.
type ReadCloser interface {
    Reader
    Closer
}

// WriteCloser is the interface that groups the basic Write and Close methods.
type WriteCloser interface {
    Writer
    Closer
}

// ReadWriteCloser is the interface that groups the basic Read, Write and Close methods.
type ReadWriteCloser interface {
    Reader
    Writer
    Closer
}

// ReadSeeker is the interface that groups the basic Read and Seek methods.
type ReadSeeker interface {
    Reader
    Seeker
}

// WriteSeeker is the interface that groups the basic Write and Seek methods.
type WriteSeeker interface {
    Writer
    Seeker
}

// ReadWriteSeeker is the interface that groups the basic Read, Write and Seek methods.
type ReadWriteSeeker interface {
    Reader
    Writer
    Seeker
}

依赖倒置原则

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.

依赖调换

依赖倒置其实就是变换依赖方和被依赖方的位置:

  • 高层模块不应该依赖底层模块,都应该依赖于抽象
  • 抽象不依赖具体实现
  • 具体实现依赖抽象

如下图所示,对象A依赖对象B,经过依赖倒置之后对象A依赖变成了抽象A(interfaceA),然后对象B实现了抽象A。

package A

type B_Abstract interface {
    GetB() int
}

type A struct {
    Va int
}

func (a *A) Add(b B_Abstract) int {
    return b.GetB() + a.a
}
package B

type B struct {
    Vb int
}

func(b *B) GetB() int {
    return b.b
}
package main

import (
    "A"
    "B"
)

func main() {
    a := A.A{Va:1}
    b := B.B{Vb:1}
    a.Add(b)
}

依赖管理

如果go的项目已经遵守了前面的4个原则,那么代码应该已经被分解到不同的package中了,每个package都有一个明确定义的责任或目的。项目中代码应该根据接口描述其依赖关系,并且应该考虑这些接口以仅描述这些函数所需的行为。除此之外,还需要注意项目的依赖关系即项目的import graph结构,在go中import graph必须是非循环的。不遵守这种非循环要求将导致编译失败,但更为严重地是它代表设计中存在严重错误。

在所有条件相同的情况下,精心设计的go程序的import graph应该是宽的,相对平坦的,而不是高而窄的。 如果你有一个package,其函数无法在不借助另一个package的情况下运行,那么这或许表明代码没有很好地沿pakcage边界分解。

依赖倒置原则鼓励将特定的责任,沿着import graph尽可能的推向更高层级,推给main package或顶级处理程序,留下较低级别的代码来处理抽象接口。

总结来讲,如果按照SOLID原则来安排go的项目,cycle import等问题可以迎刃而解。

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s