kratos简介
概述
kratos app
kratos app用法和设计理念
Kratos 是一套 Go 语言编写的微服务框架,它提供了一套基础的、易于使用的微服务开发框架和工具集。
Kratos 提供了一套命名为 app
的接口和工具,它设计用来帮助更方便地管理和运行服务。
在 Kratos 中,一个 app
代表一个可以运行的应用。app.App
是一个接口,它定义了一个应用需要实现的基本方法,包括 Name()
、Run()
和 Stop()
等。
app.App
的设计理念是将一个应用的启动和停止过程标准化,并将它们与应用的具体业务逻辑解耦。这使得你可以在不改变应用内部实现的情况下,更改应用的启动和停止方式,或者在多个应用之间复用相同的启动和停止逻辑。
具体来说,app.App
的各个方法的含义如下:
Name()
: 返回应用的名称。这个名称通常在日志、指标等地方使用,用来标识是哪个应用生成的这些数据。Run()
: 运行应用。这个方法会阻塞,直到应用退出或者发生错误。Stop()
: 停止应用。这个方法会停止应用的运行,并做一些清理工作,例如关闭数据库连接、停止后台任务等。
通过这种方式,Kratos 提供了一种灵活而又统一的方式来管理应用的生命周期。无论你的应用是一个 HTTP 服务器、一个 gRPC 服务,还是一个消息队列消费者,都可以用相同的方式来启动和停止它们。这让你的代码更加模块化,也更容易测试和维护。
biz/data/service
在 Kratos 中,biz、data、service 是常用的三个概念:
-
biz(Business Logic):业务逻辑层。
biz 层主要负责处理业务逻辑,将复杂的业务逻辑封装为简单、可重用的业务接口,并暴露给上层的 service 层调用。biz 层通常包含多个 Use Case,每个 Use Case 对应一个具体的业务场景,可以通过调用 DAO 层来获取数据,处理数据并返回结果。
-
data(Data Access Object):数据访问层。
data 层主要负责与数据库交互,提供统一的访问接口和实现,屏蔽数据库的具体操作细节,以便于提高代码的可重用性和可维护性。data 层通常用来提供诸如增删改查等基本操作接口,以及对数据进行验证、格式化、转换等操作,确保数据的有效性和完整性。
-
service:微服务层。
service 层是微服务框架的核心部分,负责处理外部请求并调用相应的 biz 接口来处理业务逻辑。在 service 层中,通常会处理一些基本的服务功能,比如授权、身份验证、流量控制等,以保障整个服务的可靠性和安全性。
以上三层概念可以通过垂直拆分和水平拆分等方式进行组合和扩展,以构建出更复杂、更高效的微服务架构。在 Kratos 中,这些概念提供了一种通用的设计模式,旨在提高微服务的可维护性、可扩展性和可重用性,加速微服务的开发和部署流程。
biz
业务逻辑层(biz)是Kratos中最具有业务特性的一层,它主要负责业务逻辑的实现,包括业务规则、流程和算法等。在Kratos中,业务逻辑层通常包含了领域模型、业务逻辑和应用服务等组件。因此,理解biz的use case需要从具体的业务场景出发,考虑业务逻辑层需要实现哪些业务规则、流程和算法等,以及如何通过领域模型、业务逻辑和应用服务等组件来实现这些业务逻辑。具体来说,可以通过以下几个步骤来进一步理解biz的use case:
1. 确定业务场景:首先需要明确具体的业务场景,例如电商平台的订单管理、社交平台的好友关系管理等。
2. 分析业务规则:在确定业务场景后,需要分析业务规则,即业务逻辑层需要实现哪些业务规则,例如订单管理中的订单状态转换规则、好友关系管理中的好友关系验证规则等。
3. 设计领域模型:在分析业务规则后,需要设计领域模型,即业务逻辑层需要处理的业务对象及其属性和行为。例如订单管理中的订单对象、商品对象等。
4. 实现业务逻辑:在设计领域模型后,需要实现业务逻辑,即根据业务规则和领域模型来实现具体的业务逻辑。例如订单管理中的订单状态转换逻辑、商品库存检查逻辑等。
5. 提供应用服务:在实现业务逻辑后,需要提供应用服务,即将业务逻辑封装成可供其他组件调用的服务。例如订单管理中的订单服务、商品服务等。
通过以上步骤,可以进一步理解biz的use case,即业务逻辑层需要实现具体的业务规则、流程和算法等,以及如何通过领域模型、业务逻辑和应用服务等组件来实现这些业务逻辑。
biz/use case
在 Clean Architecture 中,Use Case 是一种特殊的对象,它代表了一个具体的业务操作或者说用户的一个用例。每个 Use Case 都会对应一个业务流程,并且包含了这个业务流程所有的业务逻辑。
在 Kratos 的架构中,biz
层就是用来定义这些 Use Case 的。具体来说,每个 Use Case 通常会被定义为一个 Go 接口,这个接口中的方法就对应了该 Use Case 可以执行的操作。然后,我们会为这个接口提供一个或多个实现,这些实现就是具体的业务逻辑。
以下是一个简单的例子:
package biz
type UserUseCase interface {
CreateUser(ctx context.Context, name string) (User, error)
GetUser(ctx context.Context, id int) (User, error)
}
type User struct {
ID int
Name string
}
在这个例子中,我们定义了一个 UserUseCase
,它有两个方法:CreateUser
和 GetUser
。这就代表了两个业务用例:创建用户和获取用户
通过这种方式,我们可以将业务逻辑从底层的数据存储和网络传输中解耦出来,这使得业务逻辑更容易编写和测试,并且也更加灵活。比如,我们可以根据需要为同一个 Use Case 提供多个实现,或者将一个 Use Case 的实现替换为另一个,而不需要修改其他的代码。
在 Kratos 架构中,biz
层定义的 Use Case 接口应当由 biz
层的具体实现类来实现,而不是在 data
层或 service
层。data
层和 service
层会与 biz
层进行交互,但它们本身不直接包含业务逻辑。
在 biz
层,会实现 Use Case 接口,这些实现类会依赖于底层的 data
层提供的数据存储和检索服务。实际的业务逻辑会在 biz
层实现类中完成。
以前面的例子为例,可以在 biz
层实现 UserUseCase
接口:
package biz
import "context"
type UserRepository interface {
Create(ctx context.Context, user User) error
Get(ctx context.Context, id int) (User, error)
}
type UserUseCaseImpl struct {
repo UserRepository
}
func NewUserUseCase(repo UserRepository) UserUseCase {
return &UserUseCaseImpl{repo: repo}
}
func (u *UserUseCaseImpl) CreateUser(ctx context.Context, name string) (User, error) {
user := User{Name: name}
err := u.repo.Create(ctx, user)
return user, err
}
func (u *UserUseCaseImpl) GetUser(ctx context.Context, id int) (User, error) {
return u.repo.Get(ctx, id)
}
在这个例子中,我们定义了一个 UserRepository
接口,它表示了数据存储和检索的抽象。然后,我们实现了 UserUseCaseImpl
,它实现了 UserUseCase
接口,并依赖于 UserRepository
接口来完成数据存储和检索的操作。这样,业务逻辑就完全封装在 biz
层中了。
data
层会实现 UserRepository
接口,而 service
层会调用 biz
层的 UserUseCase
接口来执行业务逻辑。这样,我们就实现了业务逻辑与底层数据存储和网络传输的解耦。
service是否可以直接调data?
在Kratos架构中,推荐的方式是让service
层通过biz
层来访问data
层。这样做的目的是为了保持架构的清晰和解耦,每一层都有明确的职责:
service
层:负责处理网络请求,转发请求到biz
层,并将biz
层返回的结果格式化为网络响应。biz
层:包含核心的业务逻辑,负责处理业务请求。它会调用data
层提供的服务来实现具体的数据操作。data
层:负责数据的存储和检索,为biz
层提供数据服务。
让service
层直接访问data
层可能会导致架构变得混乱,业务逻辑可能会分散在多个层次中,这使得代码更难理解和维护。此外,如果service
层直接依赖于data
层,那么当我们需要改变数据存储的方式时,可能需要修改service
层的代码,这违反了单一职责原则和开闭原则。
当然,这只是一种推荐的方式,实际上在某些特定的情况下,service
层直接访问data
层可能是更合适的选择。比如,在某些简单的场景下,例如只是简单的数据获取并没有涉及复杂的业务逻辑,这时候service
层直接访问data
层可能会更加直接和高效。但是,需要清楚的知道这样做的潜在风险,这可能使得你的应用的业务逻辑分散在service
层和biz
层之间,这可能使得后续的维护变得更复杂。
另外,在一些需要性能优化的场景,例如批量数据操作,如果经过biz
层会产生大量的不必要的数据转换,也可以考虑service
层直接访问data
层。
总的来说,这是一个权衡的问题,我们要在保持架构清晰和代码复杂性之间进行权衡。在大多数情况下,我们推荐保持架构的清晰,即让service
层通过biz
层来访问data
层,这样可以更好的应对业务的变化和扩展。在有特殊需要的情况下,可以适当的让service
层直接访问data
层,但是需要清楚的知道这样做的潜在风险。
data变化对biz的影响
如果改变了data
数据存储的方式,确实可能需要修改biz
层的实现,但这正是biz
层的职责之一:抽象和封装底层数据的操作,为service
层提供一致的接口。这样,service
层就可以避免直接依赖具体的数据存储方式,从而达到松耦合的效果。
当数据存储方式改变时,biz
层可能需要修改,但service
层并不需要知道这个变化,它依然调用biz
层的接口就可以了。这就是所谓的开闭原则:对扩展开放,对修改封闭。在这种架构下,当我们需要添加新的数据存储方式时,我们只需要在biz
层添加新的实现,而不需要修改service
层的代码。
这种分层的设计可以帮助我们更好地管理代码的复杂性,每一层只需要关注自己的职责,不需要关心其他层的具体实现。这样,当系统需要扩展或者修改时,我们可以更容易地进行修改,而不会影响到整个系统。
在大多数应用程序架构中,包括 Kratos,service
层和 biz
层具有不同的职责:
service
层:这一层主要处理网络协议和格式,比如 HTTP、gRPC、Thrift 等。它的职责是解析请求,调用适当的业务逻辑(在biz
层),然后将结果格式化为适当的响应。service
层并不应该包含任何业务逻辑,它只是一个“传递者”或“翻译者”。biz
层:这是应用程序的核心,包含了所有的业务逻辑。biz
层负责实现用例,并调用data
层以持久化数据。biz
层应该尽可能地独立于网络和数据存储,以便它们可以根据需要更改,而不会影响到业务逻辑。
如果你将数据访问逻辑直接放在 service
层,那么你就将业务逻辑(在 biz
层)和数据访问逻辑混合在了一起。这将使得你的代码更难维护和测试。此外,如果你将来需要更改数据存储方式,你可能需要更改 service
层的代码,这将导致更多的工作,并可能引入错误。
相反,如果你将数据访问逻辑放在 biz
层,那么 service
层就只需要关注网络协议和数据格式化,而不需要关心如何获取或存储数据。这使得你的代码更易于理解和维护。如果你需要更改数据存储方式,你只需要更改 biz
层的代码,而不需要更改 service
层的代码。
总的来说,将数据访问逻辑放在 biz
层而不是 service
层,可以使你的代码更易于维护,更具有灵活性,更易于测试,而且更符合关注点分离的原则。
开闭原则:对扩展开放,对修改封闭
"对扩展开放,对修改封闭" 是面向对象设计原则中的开闭原则(Open-Closed Principle, OCP)。它的核心理念是,软件实体(类、模块、函数等等)应该对扩展开放,对修改封闭。这意味着已有的代码是封闭的(不应该修改),新的功能是通过扩展来实现的(添加新的代码)。
以 Kratos 的 biz
层和 service
层为例。假设我们现在需要添加一个新的数据存储方式。如果我们在 service
层直接调用了 data
层,那么我们可能需要修改 service
层的代码来适配新的数据存储方式。这就违反了开闭原则,因为我们需要修改已有的 service
层代码。
然而,如果我们在 biz
层封装了数据操作的接口,那么 service
层就只需要调用这个接口,而不需要关心具体的数据存储方式。这样,当我们添加新的数据存储方式时,我们只需要在 biz
层添加新的接口实现,而不需要修改 service
层的代码。这就符合了开闭原则,因为我们通过扩展(添加新的 biz
层代码)来实现新的功能,而不是通过修改已有的代码。
总的来说,开闭原则鼓励我们使用接口和抽象类,使得我们的代码更具有灵活性和可维护性,更易于扩展和修改。
代码示例
简洁示例1
假设我们有一个用于绘制各种形状的系统,其中有一个 Draw
函数用于绘制形状:
type Shape struct {
Type string
}
func Draw(s Shape) {
if s.Type == "circle" {
fmt.Println("Draw a circle")
} else if s.Type == "rectangle" {
fmt.Println("Draw a rectangle")
}
}
现在,如果我们需要添加一个新的形状,比如三角形,那么我们需要修改 Draw
函数,这就违反了开闭原则。
我们可以通过引入接口来改进这个设计,使其符合开闭原则:
type Shape interface {
Draw()
}
type Circle struct{}
func (c Circle) Draw() {
fmt.Println("Draw a circle")
}
type Rectangle struct{}
func (r Rectangle) Draw() {
fmt.Println("Draw a rectangle")
}
func Draw(s Shape) {
s.Draw()
}
现在,如果我们需要添加一个新的形状,我们只需要创建一个实现了 Shape
接口的新类型,而不需要修改 Draw
函数:
type Triangle struct{}
func (t Triangle) Draw() {
fmt.Println("Draw a triangle")
}
这就是开闭原则的一个简单例子:通过扩展(添加新的类型)来增加新的功能,而不是通过修改已有的代码。这样的设计使得代码更具有灵活性和可维护性。
新增data层的数据存储方式示例
如何在 Kratos 的架构中应用开闭原则。假设项目已经有了对 MySQL 的支持,现在需要添加对 Postgres 的支持。
首先,在 data
层,我们定义一个 DataRepo
接口,这个接口定义了所有的数据操作:
package data
type DataRepo interface {
// 定义了所有需要的数据操作
// ...
}
在 data
层,我们分别为 MySQL 和 Postgres 实现这个接口:
type MySQLRepo struct {
// MySQL 的具体实现
// ...
}
func (r *MySQLRepo) ... {
// MySQL 的具体操作
// ...
}
type PostgresRepo struct {
// Postgres 的具体实现
// ...
}
func (r *PostgresRepo) ... {
// Postgres 的具体操作
// ...
}
然后,在 biz
层,我们依赖 DataRepo
接口,而不是具体的实现:
package biz
type Biz struct {
repo data.DataRepo
// ...
}
func NewBiz(repo data.DataRepo) *Biz {
return &Biz{repo: repo}
}
这样,当需要添加对新的数据存储方式的支持时,只需要在 data
层添加新的 DataRepo
接口的实现,而不需要修改 biz
层的代码。这就符合了开闭原则:对扩展开放,对修改封闭。
在应用启动时,根据配置或者其他逻辑,可以选择初始化哪种 DataRepo
的实现,并将其注入到 biz
层,这样 biz
层就可以透明地切换不同的数据存储方式。
var repo data.DataRepo
if useMySQL {
repo = &data.MySQLRepo{...}
} else if usePostgres {
repo = &data.PostgresRepo{...}
}
biz := biz.NewBiz(repo)
这样,就通过接口和依赖注入,实现了对开闭原则的遵守,保证了代码的可扩展性和可维护性。