单一职责原则的理解
5 min read

单一职责原则的理解

单一职责原则的理解
Photo by Guilherme Lahmann / Unsplash

SRP

单一职责原则(Simple Responsibility Principle, SRP)指不要存在一个以上导致类变更的原因。假设有一个 Class 负责两个职责,一旦发生需求变更,修改其中一个职责的逻辑代码,有可能会导致另一个职责的功能发生故障。分别用两个 Class 来实现两个职责,进行解耦。总体来说就是一个 Class、Interface、Method 只负责一项职责。

SRP 理解

一个类只负责完成一个职责或者功能。不要设计大而全的类,要设计粒度小、功能单一的类。单一职责原则是为了实现代码高内聚、低耦合,提高代码的复用性、可读性、可维护性。

换个角度来讲就是,一个类包含了两个或者两个以上业务不相干的功能,那我们就说它职责不够单一,应该将它拆分成多个功能更加单一、粒度更细的类。

单一职责原则是最简单但又最难运用的原则,需要设计人员发现类的不同职责并将其分离,再封装到不同的类或模块中。而发现类的多重职责需要设计人员具有较强的分析设计能力和相关重构经验。

如果遵循单一职责原则将有以下优点:

  • 降低类的复杂度。一个类只负责一项职责,其逻辑肯定要比负责多项职责简单得多。
  • 提高类的可读性。复杂性降低,自然其可读性会提高。
  • 提高系统的可维护性。可读性提高,那自然更容易维护了。
  • 变更引起的风险降低。变更是必然的,如果单一职责原则遵守得好,当修改一个功能时,可以显著降低对其他功能的影响。

Golang Demo

Demo 场景:

  • 某线上学院提供直播课和录播课两种产品
  • 直播课可以播放和暂停, 不支持快进快退
  • 录播课支持播放, 暂停, 快进和快退
  • 如果把直播课和录播课实现在一个 class 里面, 则快进和快退的处理会比较麻烦
  • 将直播课和录播课分开 class 实现, 从而遵循单一职责原则

Bad SRP

不好的示例: 把两种课程的处理放在一个 class, BadCourse承担了多种职责

package simple_responsibility

import "fmt"

type IBadCourse interface {
    ID() int
    Name() string
    Play()
    Pause()
    Forward(int)
    Backward(int)
}

type BadCourse struct {
    iID int
    sName string
}

func NewBadCourse(id int, name string) *BadCourse {
    return &BadCourse{
        iID: id,
        sName: name,
    }
}

func (me *BadCourse) ID() int {
    return me.iID
}

func (me *BadCourse) Name() string {
    return me.sName
}

func (me *BadCourse) Play() {
    fmt.Printf("%v play\n", me.Name())
}

func (me *BadCourse) Pause() {
    fmt.Printf("%v pause\n", me.Name())
}


func (me *BadCourse) Forward(seconds int) {
    if me.Name() == "录播课" {
        fmt.Printf("%v forward %v seconds\n", me.Name(), seconds)
    } else {
        fmt.Printf("%v cannot forward\n", me.Name())
    }
}


func (me *BadCourse) Backward(seconds int) {
    if me.Name() == "录播课" {
        fmt.Printf("%v backward %v seconds\n", me.Name(), seconds)
    } else {
        fmt.Printf("%v cannot backward\n", me.Name())
    }
}

Good SRP

更好的示例, 定义课程接口和课程控制接口。

Course 抽象

package simple_responsibility

type IGoodCourse interface {
    ID() int
    Name() string
    Controller() IPlayControl
}

type IPlayControl interface {
    Play()
    Pause()
}

type IReplayControl interface {
    IPlayControl
    Forward(seconds int)
    Backward(seconds int)
}

type CourseInfo struct {
    iID int
    sName string
}


func (me *CourseInfo) ID() int {
    return me.iID
}

func (me *CourseInfo) Name() string {
    return me.sName
}

Course 实例:live course

LiveCourse通过集成CourseInfo实现IGoodCourse接口, 同时实现了IPlayControl接口

package simple_responsibility

import (
    "fmt"
)

type LiveCourse struct {
    CourseInfo
}

func NewLiveCourse(id int, name string) IGoodCourse {
    return &LiveCourse{
        CourseInfo{
            iID: id,
            sName: name,
        },
    }
}

func (me *LiveCourse) Controller() IPlayControl {
    return me
}


func (me *LiveCourse) Play() {
    fmt.Printf("%v play\n", me.Name())
}

func (me *LiveCourse) Pause() {
    fmt.Printf("%v pause\n", me.Name())
}

Course 实例:replay course

ReplayCourse通过集成CourseInfo实现IGoodCourse接口, 同时实现了IReplayControl接口

package simple_responsibility

import (
    "fmt"
)

type ReplayCourse struct {
    CourseInfo
}

func NewReplayCourse(id int, name string) IGoodCourse {
    return &ReplayCourse{
        CourseInfo{
            iID: id,
            sName: name,
        },
    }
}

func (me *ReplayCourse) Controller() IPlayControl {
    return me
}

func (me *ReplayCourse) Play() {
    fmt.Printf("%v play\n", me.Name())
}

func (me *ReplayCourse) Pause() {
    fmt.Printf("%v pause\n", me.Name())
}

func (me *ReplayCourse) Forward(seconds int) {
    fmt.Printf("%v forward %v\n", me.Name(), seconds)
}

func (me *ReplayCourse) Backward(seconds int) {
    fmt.Printf("%v backward %v\n", me.Name(), seconds)
}

Good SRP test

package main

import (
    "learning/gooop/principles/simple_responsibility"
    "testing"
)

func Test_SimpleResponsibility(t *testing.T) {
    fnTestBadCourse := func(bc *simple_responsibility.BadCourse) {
        bc.Play()
        bc.Pause()
        bc.Forward(30)
        bc.Backward(30)
    }
    fnTestBadCourse( simple_responsibility.NewBadCourse(1, "直播课"))
    fnTestBadCourse( simple_responsibility.NewBadCourse(2, "录播课"))


    fnTestGoodCourse := func(gc simple_responsibility.IGoodCourse) {
        pc := gc.Controller()
        pc.Play()
        pc.Pause()
        if rc, ok := pc.(simple_responsibility.IReplayControl);ok {
            rc.Forward(30)
            rc.Backward(30)
        }
    }

    fnTestGoodCourse(simple_responsibility.NewLiveCourse(11, "直播课"))
    fnTestGoodCourse(simple_responsibility.NewReplayCourse(12, "录播课"))
}
$ go test -v simple_responsibility_test.go 
=== RUN   Test_SimpleResponsibility
直播课 play
直播课 pause
直播课 cannot forward
直播课 cannot backward
录播课 play
录播课 pause
录播课 forward 30 seconds
录播课 backward 30 seconds
直播课 play
直播课 pause
录播课 play
录播课 pause
录播课 forward 30
录播课 backward 30
--- PASS: Test_SimpleResponsibility (0.00s)
PASS
ok      command-line-arguments  0.003s

References

  1. 手撸golang 架构设计原则 单一职责原则
  2. 单一职责原则(SRP) - JustNote
  3. Go设计模式学习——单一职责原则
  4. golang设计模式系列(五)面向对象设计原则-单一职责原则
  5. Golang中的单一职责原则

Public discussion