Go Web App 中必须要有 DAL 和 BLL 吗?
Is it necessary to have DAL and BLL in Go Web App?
在许多 Go 编程书籍中,作者通常将数据访问逻辑放在处理业务逻辑的同一个函数中。虽然我知道这可能只是出于教学目的,但我想知道人们是否真的在现实世界的开发中将 BLL 与 DAL 分开。
我尝试过将分层设计应用到我的 Go 项目中,但没有感受到任何好处。例如,我的 DAL 函数通常是这样的(在 appdal 包中):
func GetCustomerAccountInfo (accountID int) (*sql.Rows, error) {
sql := `SELECT * FROM CUSTOMER_ACCOUNT WHERE ID = `
return GLOBAL_PSQL.Query(sql, accountID)
}
我的典型 BLL 函数是这样的:
func NewCustomerAccountBLL (accountID int) (* CustomerAccountBLL) {
rows, err := appdal.GetCustomerAccountInfo(accountID)
// create an instance of CustomerAccountBLL (bll) and scan rows....
return &bll
}
我经常发现我的 BLL 本质上与数据库模式耦合,因为扫描需要我知道我的查询读取了哪一列,所以我发现将一些 DAL 函数合并到 BLL 中是个不错的主意(例如而是将查询合并到 BLL)。此外,拥有 DAL 也增加了我必须维护的代码量。
但是,很多软件架构师也鼓励分层设计,BLL和DAL在每一层都有明确的职责分配是有意义的。
虽然我也理解设计和模式不一定依赖于编程语言,但我经常发现在我的项目中同时使用 BLL 和 DAL 没有什么好处。我是否遗漏了设计或 Go 中的重要内容?谢谢!
正如您所指出的,这个问题不是特定于 Go 的,可以适用于任何语言。
以下是我认为您应该考虑的几点:
与其他设计问题一样,没有正确的方法,但通常的做法是将业务逻辑与数据访问分开。
业务逻辑不应与实际的数据访问实现相关联,因此如果您随后决定离开 SQL 并将对象保存在普通文件中,或者在 No-SQL存储你不一定需要改变业务逻辑层。
在您的情况下,GetCustomerAccountInfo
returns sql.Rows
。这实际上将您的业务逻辑耦合到该特定实现。通常的做法是 return 实际模型对象(例如 CustomerAccount
)。
另请注意,您的示例非常简单,因此即使将它分开,您也可能看不到太多好处。但有时候事情并没有那么简单。
数据访问逻辑可能涉及更复杂的查询加入 tables,甚至在数据库事务中进行单独的查询。通过将其分开,您不会用这些低级别的细节污染业务逻辑。此外,您可以通过仅在数据访问层上进行更改而不更改业务逻辑层来更改基础 table 结构。
业务逻辑也可能包括更复杂的计算,例如合并不同的对象、应用默认值和执行域验证。分离此逻辑(独立于正在使用的存储)允许您更改业务逻辑而不必更改数据访问逻辑。
基本上,通过将其分开,您可以分别开发(同样重要的是:测试)每个业务和数据访问逻辑,并拥有更加模块化的设计。
希望对您有所帮助。
如果您正在寻找实用的答案,这是我的想法之一。
假设您想要获取客户帐户,然后根据 API 的输入相应地修改它。
所以编写数据层或查询看起来像这样:
type CustomerAccount struct{
id string // this data type will differ depends on your database.
Name string
Address string
Age int
// and any other attribute. this is just for example.
}
func (ca *CustomerAccount)GetCustomerAccount (id int) (CustomerAccount,error) {
var ca CostumerAccount
// write your query using any databases.
// return an error if error happens when you do query to the database.
return ca,nil
}
func (ca *CustomerAccount)SaveCustomerAccount(ca CustomerAccount) error {
// find and update the data from given CustomerAccount
return nil
}
保存上面命名的代码customer_account.go
。
现在假设您想要将数据库查询与您的业务逻辑分离,或者在本例中是将 DAL 与 BLL 分离。你可以为此使用界面。创建一个匹配 上面的模型查询方法的接口类型,如下所示:
type CustomerAccountInterface interface {
GetCustomerAccount (id int) (CustomerAccount,error)
SaveCustomerAccount(ca CustomerAccount) error
}
另存为customer_account_interface.go
。
现在我们要编写一个负责修改数据的业务逻辑,我们将调用 CusomerAccountInterface
业务逻辑。因为我们正在创建一个 API 所以我们为此使用了处理程序:
func EditCustomerAccount(ca CustomerAccountInterface) http.Handler {
return http.HandleFunc(func(w http.ResponseWritter, r *http.Request){
// get all the input from user using *http.Request like id and other input.
// get our CustomerAccount Data to modify it
customerAccount,err := ca.GetAccountCustomer(id)
// modify customerAccount Accordingly from the input data, for example
customerAccount.Name = inputName // you can change what ever you want with the data here. In this case we change the name only for example purpose.
// save your customerAccount to your database
err := ca.SaveCustomerAccount(customerAccount)
// send the response 200 ok resonse if no error happens
w.WriteHeader(http.StatusOk)
resp := response{} // you can create your response struct in other places.
resp.Message = "success update data"
json.NewEncoder(w).Encode(resp)
})
}
通过上述方法,我们将作为业务逻辑的处理程序与数据访问或查询数据库分离,以便我们可以为处理程序中的业务逻辑创建单元测试,如下所示:
正在创建 CustomerAccountMock
以模拟来自数据访问的结果查询:
type CustomerAccountMock struct {
err error
Data CutstomerAccount
}
func (ca *CustomerAccountMock)GetCustomerAccount (id int) (CustomerAccount,error) {
return ca.Data,nil
}
func (ca *CustomerAccountMock)SaveCustomerAccount(ca CustomerAccount) error {
return ca.err
}
现在我们可以写出这样的测试:
func TestEditCustomerAccount(t *testing.T){
testObjects := []struct{
CMock CutomerAccountMock
}{
{
CMock : CustomerAccountMock{
err : errors.New("Test error")
Data : CustomerAccount{} // return an empty data
},
},
}
for _, testObject := range testObjects {
actualResponse := createRequestToHandler(testObject.CMock)
// here you can check your response from calling your request testing to your handler.
}
}
以上只是为了了解如何处理分离数据层和业务逻辑层的想法。你可以参考我的完整source code here。该代码引用了另一个测试用例,例如更新驱动程序数据,但它是相同的方法。
但是这种方法也有一些缺点,对我来说,这就像写一篇关于测试的数千篇文章,你必须要有耐心!
所以来回答你的问题
Is it necessary to have DAL and BLL in Go Web App?
是的,确实如此。将数据访问与业务逻辑层分开很重要,这样我们就可以对其进行单元测试。
在上面的示例中,逻辑非常简单,但想象一下,如果您有一个复杂的逻辑来操作数据并且您没有将 DAL 和 BLL 分开。在更改逻辑或查询时,它会在将来伤害您和其他开发人员。
当出现问题时感到害怕改变和沮丧是你在职业生涯中绝对要避免发生的事情。
在许多 Go 编程书籍中,作者通常将数据访问逻辑放在处理业务逻辑的同一个函数中。虽然我知道这可能只是出于教学目的,但我想知道人们是否真的在现实世界的开发中将 BLL 与 DAL 分开。
我尝试过将分层设计应用到我的 Go 项目中,但没有感受到任何好处。例如,我的 DAL 函数通常是这样的(在 appdal 包中):
func GetCustomerAccountInfo (accountID int) (*sql.Rows, error) {
sql := `SELECT * FROM CUSTOMER_ACCOUNT WHERE ID = `
return GLOBAL_PSQL.Query(sql, accountID)
}
我的典型 BLL 函数是这样的:
func NewCustomerAccountBLL (accountID int) (* CustomerAccountBLL) {
rows, err := appdal.GetCustomerAccountInfo(accountID)
// create an instance of CustomerAccountBLL (bll) and scan rows....
return &bll
}
我经常发现我的 BLL 本质上与数据库模式耦合,因为扫描需要我知道我的查询读取了哪一列,所以我发现将一些 DAL 函数合并到 BLL 中是个不错的主意(例如而是将查询合并到 BLL)。此外,拥有 DAL 也增加了我必须维护的代码量。
但是,很多软件架构师也鼓励分层设计,BLL和DAL在每一层都有明确的职责分配是有意义的。
虽然我也理解设计和模式不一定依赖于编程语言,但我经常发现在我的项目中同时使用 BLL 和 DAL 没有什么好处。我是否遗漏了设计或 Go 中的重要内容?谢谢!
正如您所指出的,这个问题不是特定于 Go 的,可以适用于任何语言。
以下是我认为您应该考虑的几点:
与其他设计问题一样,没有正确的方法,但通常的做法是将业务逻辑与数据访问分开。
业务逻辑不应与实际的数据访问实现相关联,因此如果您随后决定离开 SQL 并将对象保存在普通文件中,或者在 No-SQL存储你不一定需要改变业务逻辑层。
在您的情况下,
GetCustomerAccountInfo
returnssql.Rows
。这实际上将您的业务逻辑耦合到该特定实现。通常的做法是 return 实际模型对象(例如CustomerAccount
)。另请注意,您的示例非常简单,因此即使将它分开,您也可能看不到太多好处。但有时候事情并没有那么简单。
数据访问逻辑可能涉及更复杂的查询加入 tables,甚至在数据库事务中进行单独的查询。通过将其分开,您不会用这些低级别的细节污染业务逻辑。此外,您可以通过仅在数据访问层上进行更改而不更改业务逻辑层来更改基础 table 结构。
业务逻辑也可能包括更复杂的计算,例如合并不同的对象、应用默认值和执行域验证。分离此逻辑(独立于正在使用的存储)允许您更改业务逻辑而不必更改数据访问逻辑。
基本上,通过将其分开,您可以分别开发(同样重要的是:测试)每个业务和数据访问逻辑,并拥有更加模块化的设计。
希望对您有所帮助。
如果您正在寻找实用的答案,这是我的想法之一。
假设您想要获取客户帐户,然后根据 API 的输入相应地修改它。
所以编写数据层或查询看起来像这样:
type CustomerAccount struct{
id string // this data type will differ depends on your database.
Name string
Address string
Age int
// and any other attribute. this is just for example.
}
func (ca *CustomerAccount)GetCustomerAccount (id int) (CustomerAccount,error) {
var ca CostumerAccount
// write your query using any databases.
// return an error if error happens when you do query to the database.
return ca,nil
}
func (ca *CustomerAccount)SaveCustomerAccount(ca CustomerAccount) error {
// find and update the data from given CustomerAccount
return nil
}
保存上面命名的代码customer_account.go
。
现在假设您想要将数据库查询与您的业务逻辑分离,或者在本例中是将 DAL 与 BLL 分离。你可以为此使用界面。创建一个匹配 上面的模型查询方法的接口类型,如下所示:
type CustomerAccountInterface interface {
GetCustomerAccount (id int) (CustomerAccount,error)
SaveCustomerAccount(ca CustomerAccount) error
}
另存为customer_account_interface.go
。
现在我们要编写一个负责修改数据的业务逻辑,我们将调用 CusomerAccountInterface
业务逻辑。因为我们正在创建一个 API 所以我们为此使用了处理程序:
func EditCustomerAccount(ca CustomerAccountInterface) http.Handler {
return http.HandleFunc(func(w http.ResponseWritter, r *http.Request){
// get all the input from user using *http.Request like id and other input.
// get our CustomerAccount Data to modify it
customerAccount,err := ca.GetAccountCustomer(id)
// modify customerAccount Accordingly from the input data, for example
customerAccount.Name = inputName // you can change what ever you want with the data here. In this case we change the name only for example purpose.
// save your customerAccount to your database
err := ca.SaveCustomerAccount(customerAccount)
// send the response 200 ok resonse if no error happens
w.WriteHeader(http.StatusOk)
resp := response{} // you can create your response struct in other places.
resp.Message = "success update data"
json.NewEncoder(w).Encode(resp)
})
}
通过上述方法,我们将作为业务逻辑的处理程序与数据访问或查询数据库分离,以便我们可以为处理程序中的业务逻辑创建单元测试,如下所示:
正在创建 CustomerAccountMock
以模拟来自数据访问的结果查询:
type CustomerAccountMock struct {
err error
Data CutstomerAccount
}
func (ca *CustomerAccountMock)GetCustomerAccount (id int) (CustomerAccount,error) {
return ca.Data,nil
}
func (ca *CustomerAccountMock)SaveCustomerAccount(ca CustomerAccount) error {
return ca.err
}
现在我们可以写出这样的测试:
func TestEditCustomerAccount(t *testing.T){
testObjects := []struct{
CMock CutomerAccountMock
}{
{
CMock : CustomerAccountMock{
err : errors.New("Test error")
Data : CustomerAccount{} // return an empty data
},
},
}
for _, testObject := range testObjects {
actualResponse := createRequestToHandler(testObject.CMock)
// here you can check your response from calling your request testing to your handler.
}
}
以上只是为了了解如何处理分离数据层和业务逻辑层的想法。你可以参考我的完整source code here。该代码引用了另一个测试用例,例如更新驱动程序数据,但它是相同的方法。
但是这种方法也有一些缺点,对我来说,这就像写一篇关于测试的数千篇文章,你必须要有耐心!
所以来回答你的问题
Is it necessary to have DAL and BLL in Go Web App?
是的,确实如此。将数据访问与业务逻辑层分开很重要,这样我们就可以对其进行单元测试。
在上面的示例中,逻辑非常简单,但想象一下,如果您有一个复杂的逻辑来操作数据并且您没有将 DAL 和 BLL 分开。在更改逻辑或查询时,它会在将来伤害您和其他开发人员。
当出现问题时感到害怕改变和沮丧是你在职业生涯中绝对要避免发生的事情。