英文:
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 = $1`
return GLOBAL_PSQL.Query(sql, accountID)
}
而我的BLL函数通常是这样的:
func NewCustomerAccountBLL(accountID int) (*CustomerAccountBLL) {
rows, err := appdal.GetCustomerAccountInfo(accountID)
// 创建一个CustomerAccountBLL实例(bll)并扫描rows....
return &bll
}
我经常发现我的BLL实际上与数据库模式耦合在一起,因为扫描需要我知道查询读取了哪些列,所以我发现将一些DAL函数合并到BLL中(例如将查询合并到BLL中)并不是一个坏主意。此外,拥有一个DAL还增加了我需要维护的代码量。
然而,许多软件架构师也鼓励分层设计,并且在每个层上分配明确的责任是有道理的。
虽然我也理解设计和模式不一定依赖于编程语言,但我经常发现在我的项目中同时拥有BLL和DAL并没有太多好处。我是否在设计或Go方面遗漏了一些重要的东西?谢谢!
英文:
In many Go programming books, the author usually put data access logic within the same function that handles business logic. While I understand that this may be merely for teaching purposes, but I wonder if people actually separate BLL from DAL in real world development.
I have tried to apply layered design to my Go project but have not felt any benefits from it. For instance, my DAL functions are typically like this (in appdal package):
func GetCustomerAccountInfo (accountID int) (*sql.Rows, error) {
sql := `SELECT * FROM CUSTOMER_ACCOUNT WHERE ID = $1`
return GLOBAL_PSQL.Query(sql, accountID)
}
And my typical BLL functions would be something like this:
func NewCustomerAccountBLL (accountID int) (* CustomerAccountBLL) {
rows, err := appdal.GetCustomerAccountInfo(accountID)
// create an instance of CustomerAccountBLL (bll) and scan rows....
return &bll
}
Often I found that my BLL is essentially coupled with the database schema since scanning requires that I know which column that my query has read, so I found it's not a bad idea to merge some DAL function into BLL (such as merge the query to BLL instead). In addition, having a DAL also increases the amount of code that I have to maintain as well.
However, many software architects also encourages layered designs, and it makes sense to have BLL and DAL and assign clear responsibilities on each layer.
While I also understand design and patterns are not necessarily dependent on programming languages, but I often found little benefit from having both BLL and DAL in my project. Am I missing something important in design or Go? Thanks!
答案1
得分: 5
正如你所指出的,这个问题并不特定于Go语言,可以适用于任何语言。
以下是我认为你应该考虑的一些要点:
-
与其他设计问题一样,没有一种“正确的方法”来处理这个问题,但一般的做法是将业务逻辑与数据访问实际分离开来。
-
业务逻辑不应与实际的数据访问实现绑定在一起,这样,如果你决定从SQL转移到在普通文件中保存对象或者使用No-SQL存储,你不需要改变业务逻辑层。
-
在你的情况下,
GetCustomerAccountInfo
返回的是sql.Rows
。这实际上将你的业务逻辑与特定的实现耦合在一起。通常的做法是返回实际的模型对象(例如CustomerAccount
)。 -
还要注意,你的示例非常简单,所以即使分离它,你可能看不到很多好处。但有时候事情并不那么简单。
-
数据访问逻辑可能涉及更复杂的查询,比如连接表,甚至在数据库事务中进行单独的查询。通过分离这些逻辑,你不会在业务逻辑中污染这些底层细节。此外,你可以通过仅在数据访问层进行更改来更改底层表结构,而无需更改业务逻辑层。
-
业务逻辑可能还包括更复杂的计算,比如合并不同的对象,应用默认值和执行领域验证。将这个逻辑(与使用的存储无关)分离出来,可以在不必改变数据访问逻辑的情况下更改业务逻辑。
-
基本上,通过分离这些逻辑,你可以分别开发(并且非常重要的是:测试)业务逻辑和数据访问逻辑,并拥有一个更模块化的设计。
希望对你有所帮助。
英文:
As noted by you, this question is not Go-specific and can apply to any language.
Here are some points I think you should consider regarding this:
-
As with other design matters, there is no right way of doing this, but the general practice is to actually separate Business Logic from Data Access.
-
Business Logic should not be tied to the actual Data Access implementation so that if you then decide to move away from SQL and save objects in plain files, or in a No-SQL storage you don't necessarily need to change the Business Logic layer.
-
In your case, the
GetCustomerAccountInfo
returnssql.Rows
. This actually couples your Business Logic to that particular implementation. The usual practice is to return actual model objects (aCustomerAccount
for example). -
Also note that your example is quite simple so it's true that even by separating it you might not see a lot of benefit. But sometimes things are not that simple.
-
Data Access logic might involve more complex queries joining tables, or even making separate queries inside a database transaction. By separating this you don't pollute Business Logic with these low level details. Also, you can change the underlying table structure by only making changes on the Data Access layer, without altering business logic layer.
-
Business Logic might also in term consist on more complex calculations such as merging different objects, applying defaults and perform domain validations. Separating this logic (which is independent of the storage being used) allows you to change Business Logic without having to necessarily change Data Access logic.
-
Basically, by separating this, you can develop (and also importantly: test) each of the Business and Data Access logic separately and have a more modular design.
I hope that helps.
答案2
得分: 2
如果你正在寻找实际的答案,这是我的一个想法。
假设你想获取客户账户并根据API的输入进行相应修改。
因此,编写数据层或查询的代码可能如下所示:
type CustomerAccount struct{
id string // 这个数据类型将根据你的数据库而有所不同。
Name string
Address string
Age int
// 其他属性。这只是一个示例。
}
func (ca *CustomerAccount)GetCustomerAccount (id int) (CustomerAccount,error) {
var ca CostumerAccount
// 使用任何数据库编写查询。
// 如果在查询数据库时发生错误,则返回错误。
return ca,nil
}
func (ca *CustomerAccount)SaveCustomerAccount(ca CustomerAccount) error {
// 根据给定的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,所以我们在这里使用处理程序(handler):
func EditCustomerAccount(ca CustomerAccountInterface) http.Handler {
return http.HandleFunc(func(w http.ResponseWritter, r *http.Request){
// 使用*http.Request获取用户的所有输入,如id和其他输入。
// 获取要修改的CustomerAccount数据
customerAccount,err := ca.GetAccountCustomer(id)
// 根据输入数据修改customerAccount
customerAccount.Name = inputName // 在这里你可以根据需要更改数据。在这个例子中,我们只修改了名称以作为示例。
// 将customerAccount保存到数据库
err := ca.SaveCustomerAccount(customerAccount)
// 如果没有错误发生,则发送200 OK响应
w.WriteHeader(http.StatusOk)
resp := response{} // 你可以在其他地方创建你的响应结构体。
resp.Message = "success update data"
json.NewEncoder(w).Encode(resp)
})
}
通过上述方法,我们将处理程序(handler)与数据访问或查询数据库分离,以便我们可以为处理程序的业务逻辑创建单元测试,例如:
创建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{} // 返回一个空数据
},
},
}
for _, testObject := range testObjects {
actualResponse := createRequestToHandler(testObject.CMock)
// 在这里你可以检查从调用请求测试到处理程序的响应。
}
}
上述代码只是为了给出我如何分离数据层和业务逻辑层的思路。你可以参考我完整的源代码。该代码涉及到另一个测试用例,即更新驱动程序数据,但是使用的是相同的方法。
但是,这种方法也有一些缺点,对我来说,就像写一篇长篇文章一样,当涉及到测试时,你必须要有耐心!
所以回答你的问题:
在Go Web应用程序中,是否有必要有DAL和BLL?
是的,有必要。将数据访问与业务逻辑层分离非常重要,这样我们才能进行单元测试。
在上面的示例中,逻辑相当简单,但是想象一下,如果你有一个复杂的逻辑来操作数据,并且你没有将DAL和BLL分离。这将在将来以及其他开发人员在更改逻辑或查询时给你带来麻烦。
在你的职业生涯中,你肯定不希望发生害怕更改和遇到问题时感到沮丧的情况。
英文:
If you are looking for practical answer here is one of my thought.
Lets say that you wanted to get the Customer Account and then modify it accordingly to the input from the API.
So writing the data layer or query would look something like this :
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
}
Save the code above naming customer_account.go
.
Now lets say that you want to decouple the database query from your business logic or in this case your DAL with BLL. you can use interface for that. Creating an interface type that match witch your model query method above like this :
type CustomerAccountInterface interface {
GetCustomerAccount (id int) (CustomerAccount,error)
SaveCustomerAccount(ca CustomerAccount) error
}
save it as customer_account_interface.go
.
And now we would like to write a business logic which will be responsible for the modifying the data and we will call the CusomerAccountInterface
to the business Logic. Since we're creating an API so wen we used handler for this :
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)
})
}
From the above approach we have decoupled the handler which is the business logic with the data access or query database so that we can create a unit test for the business Logic in the handler something like this :
Creating CustomerAccountMock
for to mock the result query from data access :
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
}
Now we can write out test something like this :
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.
}
}
Above just for getting the idea how to I approach on separate the data layer and business logic layer. you can refer to my complete source code here. The code refers to another test case like updating driver data but it is the same approach.
But there are some cons on this approach though, for me it is like writing a thousands article when it comes to testing, you have to be patience!.
So coming to your question
> Is it necessary to have DAL and BLL in Go Web App?
Yes, it does. Separating the data access with the business logic layer it's important so that we can unit test it.
In the above example the logic is pretty simple, but imagine if you have a complex logic to manipulate the data and you are not separate the DAL and BLL. It will hurt you in the future and other developer when it comes to changes the logic or query.
Feeling afraid to change and frustrated when something gone wrong is definitely you want to avoid to happen in your professional life.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论