doc.dev1x.org

合成関数を用いたSLA原則の実現

1. 目的

SLA原則に準拠したコードを書きたい

2. 課題

3. 解決策

処理のステップを関数化し、関数の羅列で処理のステップを表現する

def step1(dto):
    dto.value = dto.value + 10
    return dto

def step2(dto):
    dto.value = dto.value + 40
    return dto

def step3(dto):
    dto.value = dto.value * 2
    return dto

関数間で共有されるデータはDTOを引き回す

class Dto:
    def __init__(value, has_exit = False):
        self.value = value
        self.has_exit = has_exit

関数合成でパイプラインを構築する

def exec(dto, *fns):
    for f in fns:
        dto = f(dto)
        if dto.has_exit: break
    return dto
if __name__=="__main__":
    dto = dto(0, False)
    result = exec(
        dto,
        step1,
        step2,
        step3
    )
    print(result.value)

4. メリット

制御構造とビジネスロジックを分解できる

def exec(dto, *fns):
    for f in fns:
        dto = f(dto)
        if dto.has_exit:
            break
    return dto
def step1(dto):
    dto.value = dto.value + 10
    return dto

def step2(dto):
    dto.value = dto.value + 40
    return dto

def step3(dto):
    dto.value = dto.value * 2
    return dto

処理の追加/削除が容易になる

ステップ毎のテストが容易になる

5. デメリット

ロジックが分散するのでプログラムの理解が困難になる

プログラマに一定の抽象化能力が要求される

DTOの状態を意識する必要がある

6. 注意事項

参考資料

Appendix-1 ソースコード全体

class Dto:
    def __init__(value, has_exit=False):
        self.value = value
        self.has_exit = has_exit

def exec(dto, *fns):
    for f in fns:
        dto = f(dto)
        if dto.has_exit: break
    return dto

def step1(dto):
    dto.value = dto.value + 10
    return dto

def step2(dto):
    dto.value = dto.value + 40
    return dto

def step3(dto):
    dto.value = dto.value * 2
    return dto


if __name__=="__main__":
    dto = dto(0, False)
    result = exec(
        dto,
        step1,
        step2,
        step3
    )
    print(result.value)     # => 100

Appendix-2 Go言語による実装

type (
    StepFunc func(result *State) (*State, error)
    State    struct {
        Value    int
        HasBreak bool
    }
)

func Step1(state *State) (*State, error) {
    if state.Value < 0 {
        return state, errors.New("Error")
    }
    return &State{
        Value:    state + 10,
        HasBreak: state.HasBreak,
    }, nil
}

func Step2(state *State) (*State, error) {
    return &State{
        Value:    state + 40,
        HasBreak: state.HasBreak,
    }, nil
}

func Step3(state *State) (*State, error) {
    return &State{
        Value:    state * 2,
        HasBreak: state.HasBreak,
    }, nil
}

func Do(state *State, funcs ...StepFunc) (*State, error) {
    var (
        result *State = state
        err    error  = nil
    )
    for _, f := range funcs {
        result, err = f(result)
        if err != nil {
            return result, err
        }
        if result.HasBreak {
            break
        }
        fmt.Println(result.Value)
    }

    return result, nil
}

func main() {
    initValue := &State{
        Value:    0,
        HasBreak: false,
    }
    result, err := Do(
        initValue,
        Step1,
        Step2,
        Step3,
    )
    fmt.Println(result.Value)   // => 100
}