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

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()方法就可以执行用户传来的自定义函数。
