Skip to content

Instantly share code, notes, and snippets.

@pieterlouw
Last active February 9, 2017 12:38
Show Gist options
  • Save pieterlouw/ea0d119c0bbe3c34db595fa712925cf7 to your computer and use it in GitHub Desktop.
Save pieterlouw/ea0d119c0bbe3c34db595fa712925cf7 to your computer and use it in GitHub Desktop.
How interfaces can better your design by using a notification system as an example
package main
//notification defines the concrete data type that
//will be used in the notification system
type notification struct {
recipient string
message string
subject string
//other fields associated with notification
}
//smsNotifier is used to send the notification as an SMS
type smsNotifier struct {
//fields specific to sending SMS's
}
func (e *smsNotifier) initialize(config string) {
//setup configuration specific to send SMS
}
func (e smsNotifier) sendNotification(n notification) {
//logic to send SMS
}
//emailNotifier is used to send the notification as an email
type emailNotifier struct {
//fields specific to sending emails
}
func (e *emailNotifier) initialize(config string) {
//setup configuration specific to send emails
}
func (e emailNotifier) sendNotification(n notification) {
//logic to send email
}
/*add notifier interface type to be used by notificationManager instead*/
type notifier interface {
sendNotification(notification)
}
type notificationManager struct {
notifiers []notifier //slice of registered notifiers
notifications []notification //slice of notifications to process
//rest of fields
}
func (n notificationManager) processNotifications() {
//read through notifications to send
for _, nf := range n.notifications {
//loop through all notifiers and send notification
for _, N := range n.notifiers {
N.sendNotification(nf)
}
}
}
func (n *notificationManager) registerNotifier(N notifier) {
//adds notifier to notifiers slice
}
func (n *notificationManager) addNotification(nf notification) {
//adds notification to notifications slice
}
//now we can add a new implementation of the interface without changing notificationManager, i.e gcmNotifier
type gcmNotifier struct {
//fields specific to sending gcm notifications
}
func (g *gcmNotifier) initialize(config string) {
//setup configuration specific to gcm notifications
}
func (g gcmNotifier) sendNotification(n notification) {
//logic to send gcm notification
}
//after adding gcmNotifier implementation it can be used by caller in main instead of using smsNotifier
func main() {
//create gcmNotifier object
var gcmSender gcmNotifier
gcmSender.initialize("the-gcm-api-config-details")
//create emailNotifier object
var smtpSender emailNotifier
smtpSender.initialize("the-smtp-details-to-use")
var nm notificationManager
nm.registerNotifier(&gcmSender)
nm.registerNotifier(&smtpSender)
//add some notifications
nm.addNotification(notification{ /*...*/ })
nm.addNotification(notification{ /*...*/ })
nm.addNotification(notification{ /*...*/ })
nm.addNotification(notification{ /*...*/ })
nm.addNotification(notification{ /*...*/ })
nm.processNotifications()
}
package main
//notification defines the concrete data type that
//will be used in the notification system
type notification struct {
recipient string
message string
subject string
//other fields associated with notification
}
//smsNotifier is used to send the notification as an SMS
type smsNotifier struct {
//fields specific to sending SMS's
}
func (e *smsNotifier) initialize(config string) {
//setup configuration specific to send SMS
}
func (e smsNotifier) sendNotification(n notification) {
//logic to send SMS
}
//emailNotifier is used to send the notification as an email
type emailNotifier struct {
//fields specific to sending emails
}
func (e *emailNotifier) initialize(config string) {
//setup configuration specific to send emails
}
func (e emailNotifier) sendNotification(n notification) {
//logic to send email
}
//notificationManager collects notifications and
//send the notification as an email and/or sms, depending
//on which notifier is set
type notificationManager struct {
*smsNotifier
*emailNotifier
notifications []notification //slice of notifications to process
//rest of fields
}
func (n *notificationManager) registerSMSNotifier(s *smsNotifier) {
n.smsNotifier = s
}
func (n *notificationManager) registerEmailNotifier(e *emailNotifier) {
n.emailNotifier = e
}
func (n *notificationManager) addNotification(nf notification) {
//adds a notification to the notifications slice
}
//processNotifications loops over the given notifications
//and send them as email and/or sms
func (n *notificationManager) processNotifications() {
//read through notifications to send
for _, nf := range n.notifications {
//check if sms notifier is set
if n.smsNotifier != nil {
n.smsNotifier.sendNotification(nf)
}
//check if email notifier is set
if n.emailNotifier != nil {
n.emailNotifier.sendNotification(nf)
}
}
}
func main() {
//create smsNotifier object
var smsSender smsNotifier
smsSender.initialize("the-sms-api-config-details")
//create emailNotifier object
var smtpSender emailNotifier
smtpSender.initialize("the-smtp-details-to-use")
//create notificationManager object and
//set which notifiers must be used
var nm notificationManager
nm.registerSMSNotifier(&smsSender)
nm.registerEmailNotifier(&smtpSender)
//add some notifications
nm.addNotification(notification{ /*...*/ })
nm.addNotification(notification{ /*...*/ })
nm.addNotification(notification{ /*...*/ })
nm.addNotification(notification{ /*...*/ })
nm.addNotification(notification{ /*...*/ })
//send them off
nm.processNotifications()
}

In this example we look at a notification system that can send notifications as an sms and email.

Let's have a look at the main function:

func main() {

	//create smsNotifier object
	var smsSender smsNotifier
	smsSender.initialize("the-sms-api-config-details")

	//create emailNotifier object
	var smtpSender emailNotifier
	smtpSender.initialize("the-smtp-details-to-use")

	//create notificationManager object and
	//register which notifiers must be used
	var nm notificationManager
	nm.registerSMSNotifier(&smsSender)
	nm.registerSMTPNotifier(&smtpSender)

	//add some notifications
	nm.addNotification(notification{ /*...*/ })
	nm.addNotification(notification{ /*...*/ })
	nm.addNotification(notification{ /*...*/ })
	nm.addNotification(notification{ /*...*/ })
	nm.addNotification(notification{ /*...*/ })

	//send them off
	nm.processNotifications()

}

Nothing fancy here. Basically the email and sms objects that will be used by the notification manager are created. Then we create the notification manager and set the notifiers it will use. Then we add some notifications and call the method to do the processing.

Here's the basic types:

//notification defines the concrete data type that
//will be used in the notification system
type notification struct {
	recipient string
	message   string
	subject   string
	//other fields associated with notification

}
//smsNotifier is used to send the notification as an SMS
type smsNotifier struct {
	//fields specific to sending SMS's
}

func (e *smsNotifier) initialize(config string) {
	//setup configuration specific to send SMS
}

func (e smsNotifier) sendNotification(n notification) {
	//logic to send SMS
}
//emailNotifier is used to send the notification as an email
type emailNotifier struct {
	//fields specific to sending emails
}

func (e *emailNotifier) initialize(config string) {
	//setup configuration specific to send emails
}

func (e emailNotifier) sendNotification(n notification) {
	//logic to send email
}

The notificationManager object is where the notifications are collected and the sending is coordinated:

//notificationManager collects notifications and
//send the notification as an email and/or sms, depending
//on which notifiers are registered
type notificationManager struct {
	*smsNotifier
	*emailNotifier

	notifications []notification //slice of notifications to process
	//rest of fields
}

func (n *notificationManager) registerSMSNotifier(s *smsNotifier) {
	n.smsNotifier = s
}

func (n *notificationManager) registerEmailNotifier(e *emailNotifier) {
	n.emailNotifier = e
}

func (n *notificationManager) addNotification(nf notification) {
	//adds a notification to the notifications slice
}

//processNotifications loops over the given notifications
//and send them as email and/or sms
func (n *notificationManager) processNotifications() {
	//read through notifications to send
	for _, nf := range n.notifications {

		//check if sms notifier is set
		if n.smsNotifier != nil {
			n.smsNotifier.sendNotification(nf)
		}

		//check if email notifier is set
		if n.emailNotifier != nil {
			n.emailNotifier.sendNotification(nf)
		}
	}
}

So this all works and basically is doing what it should do according to the spec. We've made it work. But it needs some refactoring.

What if the client tells us that there's an Android app out and we need to replace the sms notifications with mobile app push notifications?

Hhmm, if there's an Android app out, then there's a good chance that in the near future the client would want to send out Apple notifications as well.

To make these changes we would need to create a new notifier: androidNotifier

Also, notificationManager will need to change as well. The new notifier object need to be added, smsNotifier need to be removed, and the relevant registerAndroidNotifier method need to be added and the setSMSNotifier method must go.

In addition to that the processNotifications method also need to change.

That's quite a bit of change, and the possibility of another new notifier type (Apple) in the near future is good.

Already we see that there's some code coupling and bringing change to the system require changes to many parts of the system. The more change we bring the greater the risk that existing things will break.

Let's see how we can make the design better and more adaptive to change?

By looking at the methods of the notifier types we see some common behavior: the sendNotification method. There's also duplication in the register*Notifier method in the notificationManager type.

Let's have a look at the notificationManager type. We can spot duplication in the properties (smsNotifier and emailNotifier types) as well as in the register*Notifier methods and processNotifications method.

All this duplication and common behavior can be cleaned up by creating and using an interface type.

Time for an interface adventure...

type notifier interface {
	sendNotification(notification)
}

We create an interface type and call it notifier. The new type have one method called sendNotification. All the notifier concrete types satisfy this interface as all of them have a method called sendNotification.

Now we can change the notificationManager to have a slice of the interface type notifier instead of using the concrete types:

type notificationManager struct {
	notifiers []notifier //slice of registered notifiers

	notifications []notification //slice of notifications to process

	//rest of fields
}

By doing that we can reduce our register*Notifier methods to one method called registerNotifier that accepts a notifier type and adds it to the slice of notifiers:

func (n *notificationManager) registerNotifier(N notifier) {
	//adds notifier to notifiers slice
}

We can also clean up processNotifications by removing the multiple if statements that check if a particular notifier is registered and rather loop over the slice of notifications and call their underlying sendNotification method:

func (n notificationManager) processNotifications() {
	//read through notifications to send
	for _, nf := range n.notifications {
		//loop through all notifiers and send notification
		for _, N := range n.notifiers {
			N.sendNotification(nf)
		}
	}
}

If the client request the anticipated Apple notification then we simply add a new concrete type called appleNotifier with it's sendNotification method. From the calling main function we create a new object of the appleNotifier type and simply register it by calling the registerNotifier method.

Notice that there are absolutely no change to the notificationManager type when this happens. Also notice that after we created the new notifier interface type that we did not have to add a keyword implements to the existing concrete types (i.e emailNotifier) because of Go's implicit satisfaction of interface types.

Our design are more adaptive to change with less breaking risk if there's a change.

This feels right..

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment