SOLID - ISP & DIP & IOC ============================ ###### tags: `Design Pattern` `go` # SOLID Principle 1. SRP 2. OCP 3. LSP 4. ISP 5. DIP 6. LKP 最小知識原則, 外面知道的越少, 耦合度越低 7. Composite, 拋棄繼承, 改用組合的方式 [物件導向設計SOLID](https://clouding.city/php/solid/) ```go type A struct { //B InterfaceB } fun (a *A) METHOD(b InterfaceB){ b.xxxx } ``` # Dependency Inversion Principle - 是Open-Closed Priniciple的結論 - 高層模組不應該依賴低階模組, 兩者都應該依賴其抽象 - 抽象不應該依賴細節實做 - 細節應該依賴抽象 ## 實現方式 1. Constructor inject 2. Setter inject 3. Interface inject ## Bad example ```go func BetSlip(w http.ResponseWriter, r *http.Request) { var err error if idGenerator, err = unique.NewUniqueIdGenerator(0); err != nil { BetSlipResult(w, ERR_PROTO_UNMARSHAL_ERROR, -1, err) return } betSlipReq := &protoLocal.PlaceBetRequest{} body, err := ioutil.ReadAll(r.Body) if err != nil { BetSlipResult(w, ERR_PROTO_UNMARSHAL_ERROR, -1, err) return } unmarshalReceive := proto.Unmarshal(body, betSlipReq) if unmarshalReceive != nil { BetSlipResult(w, ERR_PROTO_UNMARSHAL_ERROR, -1, err) return } var isBalance bool cid := betSlipReq.Cid //cid totalStake := betSlipReq.TotalStake //all stake acceptPrice := betSlipReq.AcceptPrice //accept odds change or not selections := betSlipReq.Selections //how many selection in the bet // step 1: check balance // if no enough return message money not enough wallet := modelInDbBetDb.Wallet{} if isBalance, wallet, err = CheckBalance(int64(cid), totalStake); err != nil { BetSlipResult(w, ERR_BET_INSUFF_BALANCE, -1, err) return } if !isBalance { BetSlipResult(w, ERR_BET_INSUFF_BALANCE, -1, nil) return } //Setp 1-2 : check from RISK' var isRisk bool if isRisk, err = CheckRisk(cid); err != nil { BetSlipResult(w, ERR_RISK_CHECK_ERROR, -1, err) return } if !isRisk { BetSlipResult(w, ERR_RISK_CHECK_ERROR, -1, nil) return } // step 2 : use selection id check selection can bet or not // if one of single not bet return message bet is error var isCheckSelection bool brSelections := []protoLocal.SelectionList{} for _, item := range selections { selectionId := item.OutcomeId way := item.Way market := modelInDbSourceDb.Market{} marketVariableSelection := modelInDbSourceDb.MarketVariableSelection{} if market, marketVariableSelection, err = GetWayMarket(selectionId, way); err != nil { BetSlipResult(w, "bet is error", -1, err) return } // only selection result not open return if isCheckSelection, err = CheckSelection(market, marketVariableSelection, way); err != nil { BetSlipResult(w, "bet is error", -1, err) return } if isCheckSelection { brSelections = append(brSelections, *item) } //} } // step 3 : todo operator check now always true // step 4 : check odds change // if not agree odds change but has odds change return not agree for _, brSelection := range brSelections { var isChange bool // var acceptPrice int32 market := modelInDbSourceDb.Market{} marketVariableSelection := modelInDbSourceDb.MarketVariableSelection{} if market, marketVariableSelection, err = GetWayMarket(brSelection.OutcomeId, brSelection.Way); err != nil { BetSlipResult(w, "bet is error", -1, err) return } if isChange, err = CheckOddsChange(market, marketVariableSelection, brSelection.Way, brSelection.Odds); err != nil { BetSlipResult(w, "odds change not agree", -1, err) return } // acceptPrice 0:not accept change 1: accept change if isChange && acceptPrice == 0 { BetSlipResult(w, "odds change not agree", -1, err) return } } // step 5 : process combination ex: single/multi get bet change money transId := strconv.FormatInt(idGenerator.NextID().Int64(), 10) cartId := int64(idGenerator.NextID()) betSelectionTx := modelInDbBetDb.BetSelectionFactory().Begin() betTx := modelInDbBetDb.BetFactory().Begin() orderTx := modelInDbBetDb.OrderFactory().Begin() if err = ProcessBetSelection(selections, cartId, betSelectionTx); err != nil { betSelectionTx.Rollback() betTx.Rollback() orderTx.Rollback() BetSlipResult(w, "bet single fail", -1, err) return } for _, selectionItem := range selections { if selectionItem.Stake == 0 { continue } var orderId int64 if err, orderId = ProcessOrder(wallet, cartId, 1, transId, selectionItem.Stake, betSlipReq, orderTx); err != nil { betSelectionTx.Rollback() betTx.Rollback() orderTx.Rollback() BetSlipResult(w, "bet single fail", -1, err) return } if err, _ = ProcessBet(selectionItem.Stake, selectionItem.Odds, orderId, 1, 1, betTx); err != nil { betSelectionTx.Rollback() betTx.Rollback() orderTx.Rollback() BetSlipResult(w, "bet single fail", -1, err) return } } if err = ProcessComboMulti(wallet, cartId, transId, selections, betSlipReq, orderTx, betTx); err != nil { betSelectionTx.Rollback() betTx.Rollback() orderTx.Rollback() BetSlipResult(w, "bet combo multi fail", -1, err) return } if err = ProcessSmallMulti(wallet, cartId, transId, selections, betSlipReq, orderTx, betTx); err != nil { betSelectionTx.Rollback() betTx.Rollback() orderTx.Rollback() BetSlipResult(w, "bet small multi fail", -1, err) return } if err = ProcessFullMulti(wallet, cartId, transId, selections, betSlipReq, orderTx, betTx); err != nil { betSelectionTx.Rollback() betTx.Rollback() orderTx.Rollback() BetSlipResult(w, "bet full multi fail", -1, err) return } //step 6 : grpc withdraw withdrawRequest := protoLocal.WithdrawRequest{} withdrawRequest.Cid = int64(betSlipReq.Cid) withdrawRequest.Oid = int64(betSlipReq.Cid) withdrawRequest.Amount = betSlipReq.TotalStake withdrawRequest.TransId = transId if reData := grpc.WithdrawClient(withdrawRequest); reData.Message == "N" { betSelectionTx.Rollback() betTx.Rollback() orderTx.Rollback() BetSlipResult(w, "with draw fail", -1, nil) return } // step 7 : return success betSelectionTx.Commit() betTx.Commit() orderTx.Commit() BetSlipResult(w, "success", 1, nil) return } ``` 模組依賴圖 ![](https://i.imgur.com/CZlFmA5.png) ![](https://i.imgur.com/nQO5Ctf.png) 高階模組BetSlip完全依賴於低階的功能模組(uniqgueid)或者是外部模組(xx service, xx db) 各自模組無法獨立演化. 且無法做單元測試. *Golang是滿足**接口編程**的語言. 雖然不是OOP就是了* 引入接口, 並依賴接口 ![](https://i.imgur.com/yydMDUz.png) ```go // constraint methods of IUnigueIDGenerator type IUnigueId interface{ // Get next unigue id Next() int64 } // constraint methods of IWalletService type IWalletService interface{ // check and get wallet CheckBalance(int64, float64) (bool, Wallet, error) } // constraint methods of IRiskService type IRiskService interface{ // check risk by client CheckRisk(int32) (bool, error) } // constraint methods of ISouceDB type ISouceDB interface { // get OutcomeA by 2way selection market GetOutcomeAMarket(int64) (Market, error) // get OutcomeB by 2way selection market GetOutcomeBMarket(int64) (Market, error) // get Outcomes by multiple selection market GetMarketVariableSelection(int64) (MarketVariableSelection, error) } // constraint methods of IBetDB type IBetDB interface { // create Bet Selection BetSelectionCreate(BetSelection) error // create Order OrderCreate(Order) error // create Bet BetCreate(Bet) error } ``` ***我熊熊發現, DK的框架根本沒實做IOC容器*** 難怪它沒寫過半個單元測試XD # IoC Ioc(Inversion of Control)控制反轉. 利用"分離組件Component"的配置與使用, 反轉依賴關係, 來降低模組之間的耦合程度. ![](https://i.imgur.com/ZJCBAXF.png) ## 簡易版IOC Container ```go package di import ( "errors" "reflect" "strings" "sync" ) var ErrFactoryNotFound = errors.New("factory not found") // factory method 工廠方法, 只負責生產物件 type factory = func() (interface{}, error) type Container struct { sync.Mutex singletons map[string]interface{} // 儲存singleton factories map[string]factory // 儲存prototype } // 容器實例, 保持在全局且唯一 func NewContainer() *Container { return &Container{ singletons: make(map[string]interface{}), factories: make(map[string]factory), } } // 註冊singleton object func (c *Container) SetSingleton(name string, singleton interface{}) { c.Lock() c.singletons[name] = singleton c.Unlock() } // 取得singleton object func (c *Container) GetSingleton(name string) interface{} { return c.singletons[name] } // 註冊prototype實例 func (c *Container) SetPrototype(name string, factory factory) { c.Lock() c.factories[name] = factory c.Unlock() } // 取得prototype實例所動態創建對象 func (c *Container) GetPrototype(name string) (interface{}, error) { var factory factory var ok bool if factory, ok = c.factories[name]; ok { return factory() } return nil, ErrFactoryNotFound } // 注入依賴, 歷尋instance所有的properties member是否是singleton或prototype func (c *Container) Ensure(instance interface{}) error { elemType := reflect.TypeOf(instance).Elem() ele := reflect.ValueOf(instance).Elem() for i := 0; i < elemType.NumField(); i++ { fieldType := elemType.Field(i) tag := fieldType.Tag.Get("di") // 取得struct tag diName := c.injectName(tag) if diName == "" { continue } var ( diInstance interface{} err error ) if c.isSingleton(tag) { diInstance = c.GetSingleton(diName) } if c.isPrototype(tag) { diInstance, err = c.GetPrototype(diName) } if err != nil { return err } if diInstance == nil { return errors.New(diName + " dependency not found") } ele.Field(i).Set(reflect.ValueOf(diInstance)) } return nil } // 需要獲得注入的依賴名稱 func (c *Container) injectName(tag string) string { tags := strings.Split(tag, ",") if len(tags) == 0 { return "" } return tags[0] } // 檢測是否是singleton依賴 func (c *Container) isSingleton(tag string) bool { tags := strings.Split(tag, ",") for _, name := range tags { if name == "prototype" { return false } } return true } // 檢查是否是實例物件的依賴 func (c *Container) isPrototype(tag string) bool { tags := strings.Split(tag, ",") for _, name := range tags { if name == "prototype" { return true } } return false } ``` Use Case ```go package main import ( "database/sql" "time" "fmt" "os" "github.com/tedmax100/http-demo/di" _ "github.com/go-sql-driver/mysql" ) // 高階模組A, A組合了B type A struct { Db *sql.DB `di:"db"` // singleton tag Db1 *sql.DB `di:"db"` B *B `di:"b,prototype"` // prototype B1 *B `di:"b,prototype"` } func NewA() *A { return &A{} } func (p *A) Version() (string, error) { rows, err := p.Db.Query("SELECT VERSION() as version") if err != nil { return "", err } defer rows.Close() var version string if rows.Next() { if err := rows.Scan(&version); err != nil { return "", err } } if err := rows.Err(); err != nil { return "", err } return version, nil } // 低階模組B type B struct { Name string } func NewB() *B { return &B{ Name: time.Now().String(), } } func main() { container := di.NewContainer() db, err := sql.Open("mysql", "root:root@tcp(localhost)/sampledb") if err != nil { fmt.Printf("error: %s\n", err.Error()) os.Exit(1) } // 注入db singleton container.SetSingleton("db", db) // 注入b的prototype container.SetPrototype("b", func() (interface{}, error) { return NewB(), nil }) a := NewA() // 遍尋容器 if err := container.Ensure(a); err != nil { fmt.Println(err) return } fmt.Printf("db: %p\ndb1: %p\nb: %p\nb1: %p\n", a.Db, a.Db1, &a.B, &a.B1) } /* db: 0xc00015e000 db1: 0xc00015e000 b: 0xc000136270 b1: 0xc000136278 */ ``` #### 強力推薦使用 [dig](https://github.com/uber-go/dig) # Interface Segregation Principle Interface(接口) 主要充當彼此交互的兩個對象之間的一種契約. 每個對象並不是直接依賴于彼此, 而是依賴于中間接口. 也不該強迫對象去依賴它根本不使用的方法. *無用的接口則稱為(interface pollution)* 所以使用多個隔離的接口, 比使用單個接口要來的貼切. 如此降低對象之間的耦合程度. 就如同微服務在拆分系統和Bounded Context, 應滿足業務, 並且滿足SRP. ## 應避免 1. 繼承後"空"實作, 表示該接口不是屬於該業務所特定擁有 2. 接口過多(Fat Interface), 細分至滿足SRP [ISP](https://clouding.city/php/solid/)