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
}
```
模組依賴圖


高階模組BetSlip完全依賴於低階的功能模組(uniqgueid)或者是外部模組(xx service, xx db)
各自模組無法獨立演化. 且無法做單元測試.
*Golang是滿足**接口編程**的語言. 雖然不是OOP就是了*
引入接口, 並依賴接口

```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"的配置與使用, 反轉依賴關係, 來降低模組之間的耦合程度.

## 簡易版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/)