這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
原文在此,續前……
——–翻譯分隔線——–
在 Go 應用中使用簡明架構(4)
介面層
關於這點,必須說,所有東西都得有編碼智慧,不論是真實的商業還是我們的應用用例。讓我們看看對於介面層的代碼這意味著什麼。不像在各個內部層次中,所有代碼都屬於一個邏輯,介面層是由若干獨立的部分構建而成。因此,我們將這個層次的代碼拆分為若干個檔案。
由於我們的商店要通過 Web 存取,就從 Web 服務開始吧:
package interfacesimport ("fmt""io""net/http""strconv""usecases")type OrderInteractor interface {Items(userId, orderId int) ([]usecases.Item, error)Add(userId, orderId, itemId int) error}type WebserviceHandler struct {OrderInteractor OrderInteractor}func (handler WebserviceHandler) ShowOrder(res http.ResponseWriter, req *http.Request) {userId, _ := strconv.Atoi(req.FormValue("userId"))orderId, _ := strconv.Atoi(req.FormValue("orderId"))items, _ := handler.OrderInteractor.Items(userId, orderId)for _, item := range items {io.WriteString(res, fmt.Sprintf("item id: %d\n", item.Id))io.WriteString(res, fmt.Sprintf("item name: %v\n", item.Name))io.WriteString(res, fmt.Sprintf("item value: %f\n", item.Value))}}
由於各個 Web 服務看起來都差不多,所以這裡並沒有實現所有的。在實際的應用中,添加商品到訂單和管理用途的展示訂單也應當作為 Web 服務。
關於這段代碼的作用,最為明顯的就是它沒有做太多工作!介面層,正確的設計就是簡單,這是因為他們的主要任務是在不同層之間傳輸資料。這是要點。實際情況是這段代碼其實無法識別某個 HTTP 要求是來自於用例層的。
再次強調,注入是為了用來處理依賴。OrderInteractor 在生產環境可能實際上是 usecases.OrderInteractor,這意味著在單元測試的時候可以被替換,讓 Web 服務在類比環境下被測試。也就是說單元測試可以僅僅測試處理 Web 服務本身的這部分行為(在調用 OrderInteractor.Items 的時候是使用“userId”作為第一個請求參數嗎?)。
討論一下完整的 Web 服務可能的樣子是值得的。這裡沒有對身份進行驗證,確信請求提供的 userId 是合法的——在真實應用中,Web 服務處理常式可能需要從會話中擷取請求的使用者,例如從 cookie 中獲得。
呃,等等,我們已經有了客戶或者使用者,現在又有了會話和 cookie?或多或少,總有些什麼是一樣的東西吧?
當然,或多或少,這就死要點。它們存在於不同的概念層次中。cookie 是非常底層的機制,處理那些在瀏覽器記憶體和 HTTP 頭的位元組包。會話就更加的抽象了,是用於確定不同的無狀態請求屬於某個用戶端的概念——而 cookie 是具體的實現形式。
使用者是相當高的層次——一個非常抽象的說法“一個可以被確認的、與應用進行互動的人”——用會話來作為具體的實現形式。最後,就是關於客戶,這個實體可以被認為是純粹的商業術語——用使用者來……好吧,你應該懂的。
我建議讓這些明確成為不同的東西,而不是面對由於在不同的概念層次使用使用相同的表達而帶來的痛苦。當在會話的傳輸機制上,需要使用 SSL 用戶端驗證來代替 cookie 時,只需要在基礎層的底層實現上引入一個新的庫來進行驗證,並且需要修改介面層的代碼,以便確保會話使用這些底層的 HTTP 細節——而使用者和客戶並不知道這些變化。
在介面層,同樣有用於從用例層的資料結構建立 HTML 響應的代碼。在真實的應用中,這可能是通過一個在基礎層中的模板庫來完成的。
現在來看看我們應用的最後一塊磚:持久化。我們已經有了可以工作的領域模型,有了讓領域生效的用例,並且實現了允許使用者通過 web 訪問我們應用的介面。現在,剩下的全部工作就是將商業和應用資料儲存到硬碟中,然後我們就準備好 IPO 了。
為了完成這個,就需要實現領域和用例層的抽象的儲存介面。由於儲存一邊是底層的資料庫,一邊是高層的業務,所以這個實現屬於介面層。儲存的任務就是從其中一個傳遞給另一個。
鑒於其用途,對於介面層以及更低層次來說,某些儲存的實現可能是受到限制的,例如編寫運行時的純記憶體的對象緩衝,或者為了單元測試類比一個儲存。而大多數真實世界的儲存都需要同外部的持久化機制進行通訊,例如資料庫,也可能使用庫來處理底層串連和查詢細節——這些是在系統的基礎層。因此,跟在其他層次一樣,我們需要確保不會違反依賴原則。
這不是說儲存是資料庫透明的!它必然會知道要跟 SQL 資料庫通訊。但是它只需要關心高層次,或者說,“邏輯”方面的內容。從這個表擷取資料,將資料放到那個表。低層次,或者說“物理”方面,不在這個範圍內——例如從網路連接到資料庫,使用從庫讀主庫寫,處理逾時等等,這都是基礎層的破事兒。
換句話說,我們的儲存更像是恰當的使用一個高層次介面,而將那些討厭的基礎細節加以隱藏,並且只是決定將哪些 SQL 發給伺服器,就這樣,就可以工作了。
現在在 src/interfaces/repositories.go 中建立這個介面:
type DbHandler interface { Execute(statement string) Query(statement string) Row }type Row interface { Scan(dest ...interface{}) Next() bool}
這確實是一個很有限的介面,不過它有了儲存所需要的所有的操作:增、刪、查、改行記錄。
在基礎層,將實現一些膠水代碼,使用 sqlite3 庫來和實際的資料庫進行通訊,以便滿足這個介面。不過首先,還是先完整實現儲存:
$GOPATH/src/interfaces/repositories.go
package interfacesimport ("domain""fmt""usecases")type DbHandler interface {Execute(statement string)Query(statement string) Row}type Row interface {Scan(dest ...interface{})Next() bool}type DbRepo struct {dbHandlers map[string]DbHandlerdbHandler DbHandler}type DbUserRepo DbRepotype DbCustomerRepo DbRepotype DbOrderRepo DbRepotype DbItemRepo DbRepofunc NewDbUserRepo(dbHandlers map[string]DbHandler) *DbUserRepo {dbUserRepo := new(DbUserRepo)dbUserRepo.dbHandlers = dbHandlersdbUserRepo.dbHandler = dbHandlers["DbUserRepo"]return dbUserRepo}func (repo *DbUserRepo) Store(user usecases.User) {isAdmin := "no"if user.IsAdmin {isAdmin = "yes"}repo.dbHandler.Execute(fmt.Sprintf(`INSERT INTO users (id, customer_id, is_admin) VALUES ('%d', '%d', '%v')`, user.Id, user.Customer.Id, isAdmin))customerRepo := NewDbCustomerRepo(repo.dbHandlers)customerRepo.Store(user.Customer)}func (repo *DbUserRepo) FindById(id int) usecases.User {row := repo.dbHandler.Query(fmt.Sprintf(`SELECT is_admin, customer_id FROM users WHERE id = '%d' LIMIT 1`, id))var isAdmin stringvar customerId introw.Next()row.Scan(&isAdmin, &customerId)customerRepo := NewDbCustomerRepo(repo.dbHandlers)u := usecases.User{Id: id, Customer: customerRepo.FindById(customerId)}u.IsAdmin = falseif isAdmin == "yes" {u.IsAdmin = true}return u}func NewDbCustomerRepo(dbHandlers map[string]DbHandler) *DbCustomerRepo {dbCustomerRepo := new(DbCustomerRepo)dbCustomerRepo.dbHandlers = dbHandlersdbCustomerRepo.dbHandler = dbHandlers["DbCustomerRepo"]return dbCustomerRepo}func (repo *DbCustomerRepo) Store(customer domain.Customer) {repo.dbHandler.Execute(fmt.Sprintf(`INSERT INTO customers (id, name) VALUES ('%d', '%v')`, customer.Id, customer.Name))}func (repo *DbCustomerRepo) FindById(id int) domain.Customer {row := repo.dbHandler.Query(fmt.Sprintf(`SELECT name FROM customers WHERE id = '%d' LIMIT 1`, id))var name stringrow.Next()row.Scan(&name)return domain.Customer{Id: id, Name: name}}func NewDbOrderRepo(dbHandlers map[string]DbHandler) *DbOrderRepo {dbOrderRepo := new(DbOrderRepo)dbOrderRepo.dbHandlers = dbHandlersdbOrderRepo.dbHandler = dbHandlers["DbOrderRepo"]return dbOrderRepo}func (repo *DbOrderRepo) Store(order domain.Order) {repo.dbHandler.Execute(fmt.Sprintf(`INSERT INTO orders (id, customer_id) VALUES ('%d', '%v')`, order.Id, order.Customer.Id))for _, item := range order.Items {repo.dbHandler.Execute(fmt.Sprintf(`INSERT INTO items2orders (item_id, order_id) VALUES ('%d', '%d')`, item.Id, order.Id))}}func (repo *DbOrderRepo) FindById(id int) domain.Order {row := repo.dbHandler.Query(fmt.Sprintf(`SELECT customer_id FROM orders WHERE id = '%d' LIMIT 1`, id))var customerId introw.Next()row.Scan(&customerId)customerRepo := NewDbCustomerRepo(repo.dbHandlers)order := domain.Order{Id: id, Customer: customerRepo.FindById(customerId)}var itemId intitemRepo := NewDbItemRepo(repo.dbHandlers)row = repo.dbHandler.Query(fmt.Sprintf(`SELECT item_id FROM items2orders WHERE order_id = '%d'`, order.Id))for row.Next() {row.Scan(&itemId)order.Add(itemRepo.FindById(itemId))}return order}func NewDbItemRepo(dbHandlers map[string]DbHandler) *DbItemRepo {dbItemRepo := new(DbItemRepo)dbItemRepo.dbHandlers = dbHandlersdbItemRepo.dbHandler = dbHandlers["DbItemRepo"]return dbItemRepo}func (repo *DbItemRepo) Store(item domain.Item) {available := "no"if item.Available {available = "yes"}repo.dbHandler.Execute(fmt.Sprintf(`INSERT INTO items (id, name, value, available) VALUES ('%d', '%v', '%f', '%v')`, item.Id, item.Name, item.Value, available))}func (repo *DbItemRepo) FindById(id int) domain.Item {row := repo.dbHandler.Query(fmt.Sprintf(`SELECT name, value, available FROM items WHERE id = '%d' LIMIT 1`, id))var name stringvar value float64var available stringrow.Next()row.Scan(&name, &value, &available)item := domain.Item{Id: id, Name: name, Value: value}item.Available = falseif available == "yes" {item.Available = true}return item}
你會說:不只一個人認為,這是糟糕的代碼!許多重複,沒有錯誤處理,還有一股怪味兒。不過這個指南的要點既不是代碼樣式,也不是設計模式——這是關於應用的架構的,因此我希望這些隨意編寫的簡單代碼是直白,容易理解的,無關優雅和精明——噢,當然,我還是個 Go 的初學者,你們看到了。
值得注意的是,在每個儲存中都有 dbHandlers map,這樣就可以不放棄依賴注入的前提下讓儲存之間相互調用。如果某個儲存使用了與其他不同的 DbHandler 實現,那麼這些用到其他儲存的儲存也不需要明確知道誰用的什麼;這也算是某種窮人的依賴注入容器吧。
讓我們進一步解釋下一個有趣的方法,DbUserRepo.FindById()。在我們的架構中,這是個很好的樣本,說明了介面所做的一切就是從一個層次向另一個層次傳遞資料。FindById 從資料庫讀取行記錄,並且產生領域和用例實體。我故意讓資料庫中的 User.IsAdmin 屬性比正常更複雜了一些,用“yes”和“no”的 varchar 在資料庫中進行排序。在用例實體 User 中,它被表達為一個布爾值。對這些不同的表達進行轉換是儲存的主要工作。
User 執行個體有一個 Customer 屬性,這是一個領域實體;User 儲存直接使用了 Customer 儲存來擷取它需要的實體。
現在不難想像當應用增長時,架構是如何為我們提供協助的了。通過遵循依賴原則,可以對實體的細節進行重構而無需變更實體本身。我們可能會將 User 實體的拆分到多個表中,儲存需要處理這些細節,從多個表中擷取資料,放到一個實體裡,但是儲存的使用者並不知道這些情況。
——–翻譯分隔線——–
這篇東西拖得太久了,爭取在 2012 年內完成吧……