前文再續,上一回我們完成了用戶管理模塊的CURD(增刪改查)功能,功能層面,無甚大觀,但有一個結構性的缺陷顯而易見,那就是項目結構過度耦合,項目的耦合性(Coupling),也叫耦合度,進而言之,模塊之間的關係,是對項目結構中各模塊間相互聯繫緊密程度的一種量化。耦合的強弱取決於模塊間調用的複雜性、調 ...
前文再續,上一回我們完成了用戶管理模塊的CURD(增刪改查)功能,功能層面,無甚大觀,但有一個結構性的缺陷顯而易見,那就是項目結構過度耦合,項目的耦合性(Coupling),也叫耦合度,進而言之,模塊之間的關係,是對項目結構中各模塊間相互聯繫緊密程度的一種量化。耦合的強弱取決於模塊間調用的複雜性、調用模塊之間的方式以及通過函數或者方法傳送數據對象的多少。模塊間的耦合度是指模塊之間的依賴關係,包括包含關係、控制關係、調用關係、數據傳遞關係以及依賴關係。項目模塊的相互依賴越多,其耦合性越強,同時表明其獨立性越差,愈加難以維護。
項目結構優化
目前IrisBlog項目的問題就是獨立性太差,截至目前為止,項目結構如下:
.
├── README.md
├── assets
│ ├── css
│ │ └── style.css
│ └── js
│ ├── axios.js
│ └── vue.js
├── favicon.ico
├── go.mod
├── go.sum
├── main.go
├── model
│ └── model.go
├── mytool
│ └── mytool.go
├── tmp
│ └── runner-build
└── views
├── admin
│ └── user.html
├── index.html
└── test.html
一望而知,前端頁面(views)以及靜態文件(assets)的工程化尚可,不再需要進行分層操作,但是在後端,雖然模型層(model.go)和工具層(mytool.go)已經分離出主模塊,但主要業務代碼還是集中在入口文件main.go中:
package main
import (
"IrisBlog/model"
"IrisBlog/mytool"
"fmt"
"github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/mysql"
"github.com/kataras/iris/v12"
)
func main() {
db, err := gorm.Open("mysql", "root:root@(localhost)/irisblog?charset=utf8mb4&parseTime=True&loc=Local")
if err != nil {
fmt.Println(err)
panic("無法連接資料庫")
}
fmt.Println("連接資料庫成功")
//單數模式
db.SingularTable(true)
// 創建預設表
db.AutoMigrate(&model.User{})
// 邏輯結束後關閉資料庫
defer func() {
_ = db.Close()
}()
app := newApp(db)
app.HandleDir("/assets", iris.Dir("./assets"))
app.Favicon("./favicon.ico")
app.Listen(":5000")
}
func newApp(db *gorm.DB) *iris.Application {
app := iris.New()
tmpl := iris.HTML("./views", ".html")
// Set custom delimeters.
tmpl.Delims("${", "}")
// Enable re-build on local template files changes.
tmpl.Reload(true)
app.RegisterView(tmpl)
app.Delete("/admin/user_action/", func(ctx iris.Context) {
ID := ctx.URLParamIntDefault("id", 0)
db.Delete(&model.User{}, ID)
ret := map[string]string{
"errcode": "0",
"msg": "刪除用戶成功",
}
ctx.JSON(ret)
})
app.Put("/admin/user_action/", func(ctx iris.Context) {
ID := ctx.PostValue("id")
Password := ctx.PostValue("password")
user := &model.User{}
db.First(&user, ID)
user.Password = mytool.Make_password(Password)
db.Save(&user)
ret := map[string]string{
"errcode": "0",
"msg": "更新密碼成功",
}
ctx.JSON(ret)
})
app.Post("/admin/user_action/", func(ctx iris.Context) {
username := ctx.PostValue("username")
password := ctx.PostValue("password")
fmt.Println(username, password)
md5str := mytool.Make_password(password)
user := &model.User{Username: username, Password: md5str}
res := db.Create(user)
if res.Error != nil {
fmt.Println(res.Error)
ret := map[string]string{
"errcode": "1",
"msg": "用戶名不能重覆",
}
ctx.JSON(ret)
return
}
ret := map[string]string{
"errcode": "0",
"msg": "ok",
}
ctx.JSON(ret)
})
return app
}
入口文件main.go承載了太多業務,既需要負責資料庫結構體的創建,又得操心模板的渲染和介面邏輯的編寫,說白了:泥沙俱下,沉渣泛起。
事實上,像這樣把所有代碼都堆到一個文件中,還會帶來協作問題,比如,當你花了一整天的時間,好不容易完成了一段業務邏輯,也通過了本地測試,準備第二天提交線上測試,但是第二天上班時卻發現這個邏輯莫名其妙地開始報錯了,這通常是因為有同事在你走後修改了你編寫或者依賴的那個模塊,歸根結底,並不完全是協作的問題,項目結構也是因素之一。
多個研發同時修改了同一個源代碼文件。雖然在規模相對較小、人員較少的項目中,這種問題或許並不嚴重,但是隨著項目的增長,研發人員的增加,這種每天早上剛上班時都要經歷一遍的痛苦就會越來越多,甚至會嚴重到讓有的團隊在長達數周的時間內都不能發佈一個穩定的項目版本,因為每個人都在不停地修改自己的代碼,以適應其他人所提交的變更,周而複始,惡性迴圈。
所以我們必須把業務單獨抽離出來,比如用戶管理其實是後臺模塊功能,只有特定的管理員才可能在其頁面進行操作,所以我們可以單獨創建一個控制層:
mkdir handler
cd hanler
隨後編寫後臺控制邏輯admin.go:
package handler
import (
"github.com/kataras/iris/v12"
)
//用戶管理頁面模板
func Admin_user_page(ctx iris.Context) {
ctx.View("/admin/user.html")
}
這裡把用戶管理頁面的解析函數單獨抽離在handler包中,註意函數的首字母要進行大寫處理,因為首字母小寫函數是私有函數,只能在包內使用,無法被別的包調用。
隨後改造入口文件main.go邏輯:
app.Get("/admin/user/", handler.Admin_user_page)
路由匹配時,只需要引入handler包中的Admin_user_page函數就可以了。
隨後,對路由進行分組優化,同屬一個業務的模塊綁定在同一個分組中:
adminhandler := app.Party("/admin")
{
adminhandler.Use(iris.Compression)
adminhandler.Get("/user/", handler.Admin_user_page)
adminhandler.Get("/userlist/", handler.Admin_userlist)
adminhandler.Delete("/user_action/", handler.Admin_userdel)
adminhandler.Put("/user_action/", handler.Admin_userupdate)
adminhandler.Post("/user_action/", handler.Admin_useradd)
}
如此,業務和路由解析就徹底分開了,結構體創建函數也清爽了不少:
func newApp(db *gorm.DB) *iris.Application {
app := iris.New()
tmpl := iris.HTML("./views", ".html")
tmpl.Delims("${", "}")
tmpl.Reload(true)
app.RegisterView(tmpl)
adminhandler := app.Party("/admin")
{
adminhandler.Use(iris.Compression)
adminhandler.Get("/user/", handler.Admin_user_page)
adminhandler.Get("/userlist/", handler.Admin_userlist)
adminhandler.Delete("/user_action/", handler.Admin_userdel)
adminhandler.Put("/user_action/", handler.Admin_userupdate)
adminhandler.Post("/user_action/", handler.Admin_useradd)
}
return app
}
數據層結構優化
業務層進行了拆分,但是數據層還集成在入口文件中main.go:
package main
import (
"IrisBlog/handler"
"IrisBlog/model"
"fmt"
"github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/mysql"
"github.com/kataras/iris/v12"
)
func main() {
db, err := gorm.Open("mysql", "root:root@(localhost)/irisblog?charset=utf8mb4&parseTime=True&loc=Local")
if err != nil {
fmt.Println(err)
panic("無法連接資料庫")
}
fmt.Println("連接資料庫成功")
//單數模式
db.SingularTable(true)
// 創建預設表
db.AutoMigrate(&model.User{})
// 邏輯結束後關閉資料庫
defer func() {
_ = db.Close()
}()
app := newApp(db)
app.HandleDir("/assets", iris.Dir("./assets"))
app.Favicon("./favicon.ico")
app.Listen(":5000")
}
這裡的含義是,一旦進入入口邏輯,就立刻初始化資料庫,隨後執行業務代碼,當業務執行完畢後,利用延遲函數defer關閉資料庫鏈接。
這種邏輯的弊端是,一旦資料庫服務掛掉,整個項目服務也會受影響,再者,很多純靜態化頁面並不需要資料庫鏈接,每一次都鏈接資料庫,顯然是畫蛇添足。
所以單獨建立數據包:
mkdir database
cd database
建立數據層邏輯database.go:
package database
import (
"IrisBlog/model"
"fmt"
"github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/mysql"
)
func Db() *gorm.DB {
db, err := gorm.Open("mysql", "root:root@(localhost)/irisblog?charset=utf8mb4&parseTime=True&loc=Local")
if err != nil {
fmt.Println(err)
panic("無法連接資料庫")
}
fmt.Println("連接資料庫成功")
//單數模式
db.SingularTable(true)
// 創建預設表
db.AutoMigrate(&model.User{})
return db
}
這裡我們構建函數Db(),它返回一個資料庫操作的結構體指針,專門用來執行資料庫操作,需要註意的是,刪除函數內之前的延後defer關閉鏈接函數,否則鏈接在函數體內就關閉了,調用方就無法使用資料庫了。
調用上,直接調用database包中的Db(),就可以直接使用資料庫指針了:
//用戶列表介面
func Admin_userlist(ctx iris.Context) {
db := database.Db()
var users []model.User
res := db.Find(&users)
// 邏輯結束後關閉資料庫
defer func() {
_ = db.Close()
}()
ctx.JSON(res)
}
隨後,繼續優化入口文件:
package main
import (
"IrisBlog/handler"
"github.com/kataras/iris/v12"
)
func main() {
app := newApp()
app.HandleDir("/assets", iris.Dir("./assets"))
app.Favicon("./favicon.ico")
app.Listen(":5000")
}
func newApp() *iris.Application {
app := iris.New()
tmpl := iris.HTML("./views", ".html")
tmpl.Delims("${", "}")
tmpl.Reload(true)
app.RegisterView(tmpl)
adminhandler := app.Party("/admin")
{
adminhandler.Use(iris.Compression)
adminhandler.Get("/user/", handler.Admin_user_page)
adminhandler.Get("/userlist/", handler.Admin_userlist)
adminhandler.Delete("/user_action/", handler.Admin_userdel)
adminhandler.Put("/user_action/", handler.Admin_userupdate)
adminhandler.Post("/user_action/", handler.Admin_useradd)
}
}
這裡優化了main函數,使其邏輯更加簡明和清晰。
最後,優化數據層邏輯database.go:
package database
import (
"IrisBlog/model"
"fmt"
"github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/mysql"
_ "github.com/jinzhu/gorm/dialects/sqlite"
)
const db_type int = 1
func sqlite3() *gorm.DB {
db, err := gorm.Open("sqlite3", "/tmp/IrisBlog.db")
if err != nil {
fmt.Println(err)
panic("無法連接資料庫")
}
fmt.Println("連接sqlite3資料庫成功")
return db
}
func mysql() *gorm.DB {
db, err := gorm.Open("mysql", "root:root@(localhost)/irisblog?charset=utf8mb4&parseTime=True&loc=Local")
if err != nil {
fmt.Println(err)
panic("無法連接資料庫")
}
fmt.Println("連接mysql資料庫成功")
return db
}
func Db() *gorm.DB {
switch db_type {
case 0:
db := mysql()
//單數模式
db.SingularTable(true)
// 創建預設表
db.AutoMigrate(&model.User{})
return db
case 1:
db := sqlite3()
//單數模式
db.SingularTable(true)
// 創建預設表
db.AutoMigrate(&model.User{})
return db
default:
panic("未知的資料庫")
}
}
這裡我們分別封裝mysql和sqlite3資料庫指針函數,然後通過switch語句來根據不同的開發環境而進行切換和控制。
至此,項目結構的首次結構性優化就完成了,優化後的結構如下:
├── README.md
├── assets
│ ├── css
│ │ └── style.css
│ └── js
│ ├── axios.js
│ └── vue.js
├── database
│ └── database.go
├── favicon.ico
├── go.mod
├── go.sum
├── handler
│ └── admin.go
├── main.go
├── model
│ └── model.go
├── mytool
│ └── mytool.go
├── tmp
│ └── runner-build
└── views
├── admin
│ └── user.html
├── index.html
└── test.html
結語
為什麼我們一開始不直接採用低耦合高內聚的項目架構?因為別人的經驗並不是我們的經驗,只有真正經歷過才是真實的開發經驗,項目開發沒有標準答案,只有選擇,然後承擔後果,只有嘗試過苦澀的果實之後,下一次才會做出正確的選擇。該項目已開源在Github:https://github.com/zcxey2911/IrisBlog ,與君共觴,和君共勉。