对上层保持简洁,对下层保持抽象
在 Golang 中,接口是一组方法签名。 当类型为接口中的所有方法提供定义时,它被称为实现接口。 它与 OOP(面向对象编程) 非常相似。 接口指定了类型应该具有的方法,类型决定了如何实现这些方法。
结构体
结构体#
Golang 提供了一种自定义数据类型,可以封装多个基本数据类型,这种数据类型叫结构体,英文名称 struct
传入接口#
传入接口意味着,可以使调用方更加具有灵活性、可扩展、且更易于编写单元测试.
可扩展性#
如下面这个例子,UserService
需要一个存储对象完成一些操作,如果传入db struct
实例的话,意味着如果下次新增了redis store
那么UserService
的逻辑需要进行修改,如果传入接口,只要相关的store实现了接口中定义的方法即可为UserService
使用,不需要额外修改代码
(推荐)#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
| // db.go mysql
package db
type Store struct {
db *sql.DB
}
func NewDB() *Store { ... } // func to initialise DB
func (s *Store) Insert(item interface{}) error { ... } // insert item
func (s *Store) Get(id int) error { ... } // get item by id
------------------------------------------------------------------------------
// user.go
package user
type UserStore interface {
Insert(item interface{}) error
Get(id int) error
}
type UserService struct {
store UserStore
}
// 接受接口
func NewUserService(s UserStore) *UserService {
return &UserService{
store: s,
}
}
func (u *UserService) CreateUser() { ... }
func (u *UserService) RetrieveUser(id int) User { ... }
|
(不推荐,扩展性差)#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
| //postgres.go
package db
type Store interface {
Insert(item interface{}) error
Get(id int) error
}
type MyStore struct {
db *sql.DB
}
func InitDB() Store { ... } //func to initialise DB
func (s *Store) Insert(item interface{}) error { ... } //insert item
func (s *Store) Get(id int) error { ... } //get item by id
----------------------------------------------------------------
//user.go
package user
type UserService struct {
store db.Store
}
func NewUserService(s db.Store) *UserService {
return &UserService{
store: s,
}
}
func (u *UserService) CreateUser() { ... }
func (u *UserService) RetrieveUser(id int) User { ... }
|
可测试#
1
2
3
4
5
6
7
8
| //user_test.go
func TestCreateUser(t *testing.T) {
s := new(inMemStore) // 实现了接口方法的mock实例
service := NewUserService(s)
//... test the CreateUser() function
}
|
返回结构体#
该 NewPerson
函数返回一个 Person
表示个人信息的结构。通过直接返回结构,我们可以在调用代码中显式访问其字段 ( , Name
, Age
Email
)。这种明确性使我们能够轻松检索和利用所需的特定数据。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| type Person struct {
Name string
Age int
Email string
}
func NewPerson(name string, age int, email string) Person {
return Person{
Name: name,
Age: age,
Email: email,
}
}
func main() {
person := NewPerson("John Doe", 30, "[email protected]")
fmt.Println("Name:", person.Name)
fmt.Println("Age:", person.Age)
fmt.Println("Email:", person.Email)
}
|
简洁性#
在定义特定行为和方法时,结构返回提供了无与伦比的控制和灵活性。您可以自由地设计具有所需确切功能的结构。这种根据您的特定要求定制结构的能力可以产生更简洁、更集中的代码,从而精确地满足您的应用程序需求
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| type Person struct {
Name string
Age int
Email string
}
func (p Person) Greet() {
fmt.Println("Hello, my name is", p.Name)
}
func main() {
person := Person{
Name: "John Doe",
Age: 30,
Email: "[email protected]",
}
person.Greet()
}
|
在此更新的示例中,我们向结构中 Person
添加了一个 Greet
方法。通过返回结构,我们能够直接在结构类型上定义特定行为。该 Greet
方法利用 Person
结构的 Name
字段来个性化问候语。这种灵活性使我们能够将相关行为封装在结构本身中,从而促进更简洁、更易于维护的代码。
耦合性高(注意)#
虽然从函数返回结构类型提供了显式、灵活性和控制性,但重要的是要意识到调用代码和特定结构实现之间紧密耦合的潜在挑战。当对结构的实现细节进行修改或引入新功能时,也可能需要更新调用代码。这种紧密耦合增加了代码维护,并给应用程序随时间推移的发展带来了挑战。
为了减轻紧密耦合的影响,在结构设计中采用良好的实践是必不可少的。仔细考虑并记录结构的预期行为和契约,以管理期望并降低中断性变更的风险。通过提前规划未来的更改,您可以在返回结构的优点和最小化紧密耦合的潜在缺点之间取得平衡。
返回接口#
抽象化#
返回接口通过隐藏底层结构的实现细节来实现更高级别的抽象。返回接口时,会显示对象所遵循的明确协定或行为集,而不会暴露内部工作原理。这种抽象有助于分离关注点并促进更简洁的代码结构。通过针对接口进行编程,您可以专注于对象可以做什么,而不是它如何做。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| type Shape interface {
Area() float64
}
type Circle struct {
Radius float64
}
func (c Circle) Area() float64 {
return math.Pi * c.Radius * c.Radius
}
type Rectangle struct {
Width float64
Height float64
}
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}
func GetShape() Shape {
if someCondition {
return Circle{Radius: 5}
} else {
return Rectangle{Width: 10, Height: 5}
}
}
|
在此示例中 Shape
,我们使用 Area
方法定义接口。我们有两个结构类型,和 Rectangle
, Circle
用于实现此接口。该 GetShape
函数返回一个 Shape
,根据某些条件,它可以是 a 或 a Circle
Rectangle
。通过返回接口,调用代码可以将返回的对象视为 Shape
,专注于它所保证的行为( Area
方法),而不关心每个形状的具体实现细节。
多态性#
接口的强大功能之一是它们能够实现多态性。接口允许多个结构类型根据它们对接口协定的遵守来互换使用。这意味着可以通过接口统一处理不同的结构实现,从而提供灵活性和代码可重用性。
1
2
3
4
5
6
7
8
9
10
11
12
| // 接受 Shape 接口
func PrintArea(s Shape) {
fmt.Println("Area:", s.Area())
}
func main() {
circle := Circle{Radius: 5}
rectangle := Rectangle{Width: 10, Height: 5}
PrintArea(circle) // Polymorphically treat Circle as Shape
PrintArea(rectangle) // Polymorphically treat Rectangle as Shape
}
|
在这个扩展的示例中,我们有一个 PrintArea
接受 Shape
接口作为参数的函数。我们可以将 Circle
和 Rectangle
结构体传递给这个函数,将它们多态地视为 Shape
。这允许我们编写可重用的代码,这些代码对接口定义的常见行为进行操作,而不是为每个特定的结构类型复制逻辑。
松耦合#
返回接口会促进代码库不同部分之间的松散耦合。调用代码只需要知道接口中定义的方法,而不需要知道实现它的特定结构。这种松散耦合允许在不影响调用代码的情况下更轻松地交换不同的实现。它实现了模块化、可测试性和更好的关注点分离。
相反,返回结构会在调用代码和特定结构实现之间创建更紧密的耦合。如果对结构的实现细节进行了更改或引入了新功能,则可能还需要修改调用代码。这种紧密耦合会使代码更加僵化,对更改的适应性降低。
总而言之,返回接口提供了更高级别的抽象,允许分离关注点和更简洁的代码结构。它支持多态性,促进代码的可重用性和灵活性。此外,返回接口会促进松耦合,从而可以更轻松地交换不同的实现,而不会影响调用代码。这使您能够构建模块化且可维护的代码,这些代码可以轻松适应不断变化的需求。
构造函数传入接口能让你的代码具有灵活性和可测试性,但是返回接口还是结构体完全取决你自己对项目的把控。一般对于底层的 store
层和 usecase
层我偏向传入接口返回接口,因为这样可以是扩展性更好,并且不需要做大改动,至于偏上的启动层和controller层可以返回结构体,因为更加清晰明了的。总结起来就是一句话:对上层保持简洁,对下层保持抽象。
参考文章#