---
# System prepended metadata

title: SOLID - ISP & DIP & IOC
tags: [go, Design Pattern]

---

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/)