Skip to content

Commit

Permalink
The mediator pattern implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
Ivan Sushkov committed Feb 28, 2024
1 parent 32485fb commit bd90698
Show file tree
Hide file tree
Showing 3 changed files with 364 additions and 1 deletion.
15 changes: 15 additions & 0 deletions grade/internal/application/seedwork/mediator/interfaces.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package mediator

import (
"github.com/emacsway/grade/grade/internal/domain/seedwork/disposable"
)

type Mediator interface {
Register(commandType any, handler any) (disposable.Disposable, error)
Unregister(commandType any)
Send(command any)

Subscribe(eventType any, handler any) (disposable.Disposable, error)
Unsubscribe(eventType any, handler any)
Publish(event any)
}
143 changes: 142 additions & 1 deletion grade/internal/infrastructure/seedwork/mediator/mediator.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,145 @@
package mediator

type MediatorImp struct {
import (
"errors"
"reflect"
"sync"

"github.com/emacsway/grade/grade/internal/domain/seedwork/disposable"
)

var (
ErrNonCallableHandler = errors.New("")
ErrUnsuitableHandlerSignature = errors.New("")

plug = struct{}{}
)

type RefUntypedMediator struct {
hLock sync.RWMutex
handlers map[string]reflect.Value

sLock sync.RWMutex
subscribers map[string]map[reflect.Value]struct{}
}

func NewRefUntypedMediator() *RefUntypedMediator {
return &RefUntypedMediator{
hLock: sync.RWMutex{},
handlers: map[string]reflect.Value{},

sLock: sync.RWMutex{},
subscribers: map[string]map[reflect.Value]struct{}{},
}
}

func (m *RefUntypedMediator) Send(command any) {
m.hLock.RLock()
defer m.hLock.RUnlock()

valueType := getValueType(command)
if handler, found := m.handlers[valueType]; found {
call(handler, command)
}
}

func (m *RefUntypedMediator) Register(command any, handler any) (disposable.Disposable, error) {

Check failure on line 46 in grade/internal/infrastructure/seedwork/mediator/mediator.go

View workflow job for this annotation

GitHub Actions / lint

paramTypeCombine: func(command any, handler any) (disposable.Disposable, error) could be replaced with func(command, handler any) (disposable.Disposable, error) (gocritic)
m.hLock.Lock()
defer m.hLock.Unlock()

if err := compareWithHandlerSignature(command, handler); err != nil {
return nil, err
}

commandType := getValueType(command)
m.handlers[commandType] = reflect.ValueOf(handler)

return disposable.NewDisposable(func() {
m.Unregister(command)
}), nil
}

func (m *RefUntypedMediator) Unregister(command any) {
m.hLock.Lock()
defer m.hLock.Unlock()

commandType := getValueType(command)
delete(m.handlers, commandType)
}

func (m *RefUntypedMediator) Subscribe(event any, handler any) (disposable.Disposable, error) {

Check failure on line 70 in grade/internal/infrastructure/seedwork/mediator/mediator.go

View workflow job for this annotation

GitHub Actions / lint

paramTypeCombine: func(event any, handler any) (disposable.Disposable, error) could be replaced with func(event, handler any) (disposable.Disposable, error) (gocritic)
m.sLock.Lock()
defer m.sLock.Unlock()

if err := compareWithHandlerSignature(event, handler); err != nil {
return nil, err
}

valueType := getValueType(event)
if _, found := m.subscribers[valueType]; !found {
m.subscribers[valueType] = map[reflect.Value]struct{}{}
}

handlerValue := reflect.ValueOf(handler)
m.subscribers[valueType][handlerValue] = plug

return disposable.NewDisposable(func() {
m.Unsubscribe(event, handler)
}), nil
}

func (m *RefUntypedMediator) Unsubscribe(event any, handler any) {

Check failure on line 91 in grade/internal/infrastructure/seedwork/mediator/mediator.go

View workflow job for this annotation

GitHub Actions / lint

paramTypeCombine: func(event any, handler any) could be replaced with func(event, handler any) (gocritic)
m.sLock.Lock()
defer m.sLock.Unlock()

eventType := getValueType(event)
handlerValue := reflect.ValueOf(handler)

delete(m.subscribers[eventType], handlerValue)
}

func (m *RefUntypedMediator) Publish(event any) {
m.sLock.RLock()
defer m.sLock.RUnlock()

eventType := getValueType(event)
for handler, _ := range m.subscribers[eventType] {

Check failure on line 106 in grade/internal/infrastructure/seedwork/mediator/mediator.go

View workflow job for this annotation

GitHub Actions / lint

S1005: unnecessary assignment to the blank identifier (gosimple)
call(handler, event)
}
}

func call(callable reflect.Value, args ...any) {
in := make([]reflect.Value, 0, len(args))
for _, arg := range args {
in = append(in, reflect.ValueOf(arg))
}

callable.Call(in)
}

func compareWithHandlerSignature(initiator any, handler any) error {

Check failure on line 120 in grade/internal/infrastructure/seedwork/mediator/mediator.go

View workflow job for this annotation

GitHub Actions / lint

paramTypeCombine: func(initiator any, handler any) error could be replaced with func(initiator, handler any) error (gocritic)
handlerType := reflect.TypeOf(handler)
if handlerType.Kind() != reflect.Func {
return ErrNonCallableHandler
}

if handlerType.NumIn() < 1 {
return ErrUnsuitableHandlerSignature
}

initiatorType := reflect.TypeOf(initiator)
if handlerType.In(0) != initiatorType {
return ErrUnsuitableHandlerSignature
}

return nil
}

func getValueType(t any) string {
v := reflect.ValueOf(t)
if v.Kind() == reflect.Ptr {
v = v.Elem()
}

return v.Type().String()
}
207 changes: 207 additions & 0 deletions grade/internal/infrastructure/seedwork/mediator/mediator_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
package mediator

import (
"testing"

"github.com/stretchr/testify/assert"
)

type (
Event struct {
name string

Check failure on line 11 in grade/internal/infrastructure/seedwork/mediator/mediator_test.go

View workflow job for this annotation

GitHub Actions / lint

`name` is unused (structcheck)
}
Command struct {
name string

Check failure on line 14 in grade/internal/infrastructure/seedwork/mediator/mediator_test.go

View workflow job for this annotation

GitHub Actions / lint

`name` is unused (structcheck)
}
)

func TestMediator(t *testing.T) {
t.Parallel()

tests := []struct {
name string

assertion func(t *testing.T, m *RefUntypedMediator)
}{
{
name: "test_publish",

assertion: func(t *testing.T, m *RefUntypedMediator) {
counter := 0
handler := func(t Event) {
counter++
}

_, err := m.Subscribe(Event{}, handler)
assert.NoError(t, err)

m.Publish(Event{})
assert.Equal(t, 1, counter)
},
},

{
name: "test_unsubscribe",
assertion: func(t *testing.T, m *RefUntypedMediator) {

times := 0
handler := func(e Event) {
times++
}

times2 := 0
handler2 := func(e Event) {
times2++
}

_, err := m.Subscribe(Event{}, handler)
assert.NoError(t, err)

_, err = m.Subscribe(Event{}, handler2)
assert.NoError(t, err)

m.Unsubscribe(Event{}, handler)
m.Publish(Event{})

assert.Equal(t, 0, times)
assert.Equal(t, 1, times2)
},
},

{
name: "test_disposable_event",
assertion: func(t *testing.T, m *RefUntypedMediator) {

times := 0
handler := func(e Event) {
times++
}

times2 := 0
handler2 := func(e Event) {
times2++
}

disposable, err := m.Subscribe(Event{}, handler)
assert.NoError(t, err)

_, err = m.Subscribe(Event{}, handler2)
assert.NoError(t, err)

disposable.Dispose()
m.Publish(Event{})

assert.Equal(t, 0, times)
assert.Equal(t, 1, times2)
},
},

{
name: "test_send",
assertion: func(t *testing.T, m *RefUntypedMediator) {
times := 0
handler := func(e Command) {
times++
}

_, err := m.Register(Command{}, handler)
assert.NoError(t, err)

m.Send(Command{})

assert.Equal(t, 1, times)
},
},

{
name: "test_unregister",
assertion: func(t *testing.T, m *RefUntypedMediator) {
times := 0
handler := func(e Command) {
times++
}

_, err := m.Register(Command{}, handler)
assert.NoError(t, err)
m.Unregister(Command{})

m.Send(Command{})

assert.Equal(t, 0, times)
},
},

{
name: "test_disposable_command",
assertion: func(t *testing.T, m *RefUntypedMediator) {
times := 0
handler := func(e Command) {
times++
}

disposable, err := m.Register(Command{}, handler)
assert.NoError(t, err)

disposable.Dispose()
m.Send(Command{})

assert.Equal(t, 0, times)
},
},

{
name: "test_unsuitable_params_type_handler",
assertion: func(t *testing.T, m *RefUntypedMediator) {
times := 0
handler := func(e Event) {
times++
}

_, err := m.Register(Command{}, handler)
assert.Equal(t, ErrUnsuitableHandlerSignature, err)
},
},

{
name: "test_unsuitable_params_count_handler",
assertion: func(t *testing.T, m *RefUntypedMediator) {
times := 0
handler := func(e Event, smt any) {
times++
}

_, err := m.Register(Command{}, handler)
assert.Equal(t, ErrUnsuitableHandlerSignature, err)
},
},

{
name: "test_unsuitable_typeof_handler",
assertion: func(t *testing.T, m *RefUntypedMediator) {
handler := 1

_, err := m.Subscribe(Command{}, handler)
assert.Equal(t, ErrNonCallableHandler, err)
},
},

{
name: "test_unsuitable_void_handler",
assertion: func(t *testing.T, m *RefUntypedMediator) {
handler := func() {}

_, err := m.Register(Command{}, handler)
assert.Equal(t, ErrUnsuitableHandlerSignature, err)
},
},
}

for _, tt := range tests {
tt := tt

t.Run(tt.name, func(t *testing.T) {
m := NewRefUntypedMediator()
tt.assertion(t, m)
})
}
}

0 comments on commit bd90698

Please sign in to comment.