LOADING...

加载过慢请开启缓存(浏览器默认开启)

loading

✨Some Nice Design Patterns in Robfig/cron

2024/7/8 golang

These days I read a nice project which is a great scheduled tasks tool used in golang–https://github.com/robfig/cron, and I find some funny design patterns in this project also classical in the software development. And I’ll share with you these.😊

1. 🐶The UML image

website:https://www.dumels.com/diagram

alt text

2. Design patterns

2.1. 🐱Observer Pattern

a. what is Observer Pattern

It is a good design pattern to implement the publish/subscribe functions.
Let’s see an example written in golang.
First, we write an observer interface, and the observers need to implement this interface.

// 观察者
type Observer interface {
    Update(int)
}

Then, we define the observed struct, it maintain a list of observers.

//被观察者
type Subject struct {
    observers []Observer
    state     int
}
// 添加观察者
func (s *Subject) Attach(o Observer) {
    s.observers = append(s.observers, o)
}
// 删除观察者
func (s *Subject) Detach(o Observer) {
    for i, obs := range s.observers {
        if obs == o {
            s.observers = append(s.observers[:i], s.observers[i+1:]...)
            break
        }
    }
}
// 通知所有观察者
func (s *Subject) Notify() {
    for _, obs := range s.observers {
        obs.Update(s.state)
    }
}
// 改变状态并通知观察者
func (s *Subject) SetState(state int) {
    s.state = state
    s.Notify()
}

Now we create an observer that implements the Observer interface.

type ConcreteObserver struct {
    name string
}

// 当状态改变时候被通知
func (o *ConcreteObserver) Update(state int) {
    fmt.Printf("%s received update: %d\n", o.name, state)
}

Now we run some examples

func main() {
    //创建被观察对象
    sj := &Subject{}
    //创建观察者
    obs1 := &ConcreteObserver{
        name: "obs1",
    }
    obs2 := &ConcreteObserver{
        name: "obs2",
    }
    //将观察者添加到被观察对象
    sj.Attach(obs1)
    sj.Attach(obs2)
    //改变被观察对象状态,观察者会收到通知
    sj.SetState(1)
    sj.SetState(2)
    //删除一个观察者
    sj.Detach(obs1)
    sj.SetState(3)
}

As we can see above , when the state of Subject changed, it will change all the state of observers.

b. The Observer Pattern in robfig/cron project

type Cron struct {
    entries   []*Entry
    chain     Chain
    stop      chan struct{}
    add       chan *Entry
    remove    chan EntryID
    snapshot  chan chan []Entry
    running   bool
    logger    Logger
    runningMu sync.Mutex
    location  *time.Location
    parser    ScheduleParser
    nextID    EntryID
    jobWaiter sync.WaitGroup
}

This Cron struct is a Subject and is observed by the list of Entry which contain all jobs need to be executed, when it’s on the specified time , Cron will iterate entries and execute corresponding jobs, this is very suitable for the timed tasks.

2.2 🐱Command Pattern

// Entry consists of a schedule and the func to execute on that schedule.
type Entry struct {
    // ID is the cron-assigned ID of this entry, which may be used to look up a
    // snapshot or remove it.
    ID EntryID

    // Schedule on which this job should be run.
    Schedule Schedule

    // Next time the job will run, or the zero time if Cron has not been
    // started or this entry's schedule is unsatisfiable
    Next time.Time

    // Prev is the last time this job was run, or the zero time if never.
    Prev time.Time

    // WrappedJob is the thing to run when the Schedule is activated.
    WrappedJob Job

    // Job is the thing that was submitted to cron.
    // It is kept around so that user code that needs to get at the job later,
    // e.g. via Entries() can do so.
    Job Job
}
// Job is an interface for submitted cron jobs.
type Job interface {
    Run()
}

As the Entry struct above, Each object of an Entry has a Job that was submitted to cron, this design can decouple the execution logic
and trigger logic.

2.3 🐱Strategy Pattern

a. What is Strategy Pattern

First, we create an interface which include the functions that define some specific behavior.

type PaymentStrategy interface {
    Pay(amount float64)
}

Now, we have some concrete classes implement the PaymentStrategy interface

type WechatPayStrategy struct {
}

func (w *WechatPayStrategy) Pay(amount float64) {
    fmt.Printf("Paying %.2f using WeChat Pay\n", amount)
}

type AliPay struct {
}

func (a *AliPay) Pay(amount float64) {
    fmt.Printf("Paying %.2f using zhifubao Pay\n", amount)
}

Then, we define a proxy class, PaymentContext holds a field of PaymentStrategy interface type and provides a unified Pay (amount float64) method. So that it can be implemented by others and execute different strategy of Pay() function.

type PaymentContext struct {
    strategy PaymentStrategy
}

func NewPaymentContext(strategy PaymentStrategy) *PaymentContext {
    return &PaymentContext{
        strategy: strategy,
    }
}

func (c *PaymentContext) Pay(amount float64) {
    c.strategy.Pay(amount)
}
func main() {
    wxPay := NewPaymentContext(&WechatPayStrategy{})
    aliPay := NewPaymentContext(&AliPay{})
    wxPay.Pay(100.23)
    aliPay.Pay(200.3433)
}

b. The Strategy Pattern in robfig/cron project

The Cron Struct above implements the Job interface just like the PaymentContext in the example.

func New(opts ...Option) *Cron {
    c := &Cron{
        entries:   nil,
        chain:     NewChain(),
        add:       make(chan *Entry),
        stop:      make(chan struct{}),
        snapshot:  make(chan chan []Entry),
        remove:    make(chan EntryID),
        running:   false,
        runningMu: sync.Mutex{},
        logger:    DefaultLogger,
        location:  time.Local,
        parser:    standardParser,
    }
    for _, opt := range opts {
        opt(c)
    }
    return c
}

This New function is the same as NewPaymentContext in the example, it return a Cron type job runner.

func (c *Cron) Run() {
    c.runningMu.Lock()
    if c.running {
        c.runningMu.Unlock()
        return
    }
    c.running = true
    c.runningMu.Unlock()
    c.run()
}

This function is the same as Pay() function in the example.

2.4 🐱装饰器模式

a. What is Decorator Pattern

For example, First we define a base interface, with a function.

type Logger interface{
    Logg(msg string)
}

We define a decorator struct implements the Logger, which should be extended by classes.

type LoggerDecorator struct{
    logger Logger
}
func (d *LoggerDecorator) Logg(msg string){
    d.logger.Logg(msg)
}

This is 🐱TimestampLogger, it can print this time append with the msg incomed.

type TimestampLogger struct{
    LoggerDecorator
}
func (l *TimestampLogger) Logg(msg string) {
    timestamp := time.Now().Format("2006-01-02 15:04:05")
    l.logger.Logg(fmt.Sprintf("[%s] %s", timestamp, msg))
}

This is 🐱UppercaseLogger , it can change the msg to uppercase letter.

type UppercaseLogger struct {
    LoggerDecorator
}

func (l *UppercaseLogger) Logg(msg string) {
    l.logger.Logg(strings.ToUpper(msg))
}

This is 🐱ConsoleLogger, it can print the msg on terminal.

type ConsoleLogger struct {
}

func (l *ConsoleLogger) Logg(msg string) {
    fmt.Println(msg)
}

Let’s begin to use this design pattern.

func main(){
    baseLogger := &ConsoleLogger{}
    //use 🐱TimestampLogger to decorate the baseLogger
    timestampLogger := &TimestampLogger{
        LoggerDecorator: LoggerDecorator{
            logger: baseLogger,
        }
    }
    // Use 🐱UppercaseLogger to decorate timestampLogger
    decoratedLogger := &UppercaseLogger{
        LoggerDecorator: LoggerDecorator{
            logger: timestampLogger,
        },
    }

    // 使用装饰后的日志记录器
    go func() {
        for {
            decoratedLogger.Logg("hello, world")
            time.Sleep(100 * time.Millisecond)
        }
    }()
    <-time.After(10 * time.Second)
}

b. The Decorator Pattern in this project

// FuncJob is a wrapper that turns a func() into a cron.Job
type FuncJob func()

func (f FuncJob) Run() { f() }

这个结构体也继承自Job, 它是函数类型的

func (c *Cron) AddFunc(spec string, cmd func()) (EntryID, error) {
    return c.AddJob(spec, FuncJob(cmd))
}

其中cmd也是一个函数类型的参数,FuncJob(cmd)将cmd方法转换成了FuncJob类型,这样就可以将一个用户传过来的自定义函数,转换成实现Job接口类型的函数,从而实现复用,执行Run()方法就可以执行用户传来的自定义函数。

img_show