• 4 hours
  • Easy

Free online content available in this course.

course.header.alt.is_video

course.header.alt.is_certifying

Got it!

Last updated on 3/28/24

Manage Objects With Behavioral Design Patterns

Behavioral Design Patterns

The last category of design patterns is called behavioral. Behavioral design patterns identify common communication patterns between objects.

In this last chapter, we’ll look at three behavioral design patterns:

  • Observer

  • Strategy

  • State

Observer Pattern

The first behavioral design pattern is the observer pattern. This pattern defines a one-to-many dependency between objects, and when one object changes state, all its dependents are notified and updated automatically. 

Many observers (objects) observe a particular subject (another object) and want to be alerted when there is a change made inside the subject. If they no longer want to be notified, they can unregister from the subject. 

Let’s take a look at the UML class diagram below that shows the structure of the observer pattern:

Structure of Observer Pattern
Structure of the observer pattern

You will need the following for the pattern:

  • Subject: It knows its observers and provides an interface for attaching and detaching observer objects.

  • ConcreteSubject: To store the state of interest to ConcreteObserver and to send a notification to its observers when its state changes. 

  • Observer: To define an updating interface for objects that should be notified of changes in a subject.

  • ConcreteObserver: To maintain a reference to a ConcreteSubject object. It will store the state that should stay consistent with the subject. Finally, it implements the observer updating interface to keep its state consistent with the subject.

What’s a Use Case for This Pattern?

Whenever you need your application to notify someone or something, for example, a blog. An author can write a post, and others can subscribe to the blog to see if there are any new posts.

Let’s take a look at creating a blog using the observer pattern. First, create the observer, which is the interface,  IBlogReader:

public interface IBlogReader
{
    void NotificationReceived(string message);
}

Next, create the subject, which attaches the observer objects:

public interface INotificationService
{
    void AddReader(IBlogReader blogReader);
    void NotifyReaders(string notificationMessage);
}

Now, create the ConcreteSubject,  BlogWriter, which will manage the subscribers to the blog:

public class BlogWriter : INotificationService
{
    List<IBlogReader> readerList = new List<IBlogReader>();
 
    public void AddReaders(IBlogReader blogReader)
    {
        readerList.Add(blogReader);
    }
    public void NotifyReaders(string notificationMessage)
    {
        foreach(var reader in readerList)
        {
            reader.NotificationReceived(notificationMessage);
        }
    }
}

As you can see, the notification service is adding the type of subscribers, who are implementing the interface  IBlogReader, which means it’s adding subscribers who receive notifications through this interface.

Finally, ConcreteObserver is the class  BlogReader, which is a subscriber that will implement  IBlogReader  to manage itself or perform what it would like to do whenever it receives notifications.

public class BlogReader: IBlogReader
{
    private string _readerName;
 
    public BlogReader(string readerName)
    {
        _readerName = readerName;
    }
 
    public void NotificationReceived(string notificationMessage)
    {
        Console.WriteLine(_readerName + " " + notificationMessage);
}
}

The blog setup is done.

Let’s now write a console application to allow a blog writer to write and publish posts:

static void Main(string[] args)
{
    BlogWriter blogWriter = new BlogWriter();
 
    BlogReader reader1 = new BlogReader("Jay");
    BlogReader reader2 = new BlogReader("Mike");
 
    //add blog readers to the subscription list
    blogWriter.AddReaders(reader1);
    blogWriter.AddReaders(reader2);
 
    //notify about the first article posted
    blogWriter.NotifiyReaders("I have posted a new Blog about Cats!");
 
    Console.WriteLine(Environment.NewLine);
 
    //notify about the second article posted
    blogWriter.NotifyReaders("I have posted another new Blog about Dogs!");
}

From the above, you can see that we added subscribers to the list and call the  NotifyReaders  method with the notification message.

Here’s the output:

"Jay, I have posted a new Blog about Cats!"
"Mike, I have posted a new Blog about Cats!"

"Jay, I have posted a new Blog about Dogs!"
"Mike, I have posted a new Blog about Dogs!"

What Are the Advantages?

  • It supports loose coupling between objects that interact with each other.

  • It allows sending data to other objects without any change in the subject or observer classes. 

  • Observers can be added and removed at any point in time. 

Strategy Pattern

The next behavioral design pattern is the strategy pattern, which allows you to define several algorithms and then make them interchangeable by encapsulating each as an object.

The idea is that if you encapsulate behavior as objects, you can then select which objects to use. Thus, which behavior to implement based on some external input.

When should I use the strategy pattern?

It’s handy when you have different paths of logic that are available based on one or more conditions. Check to see if you have too many  if  or  switch  cases and need a cleaner alternative.

Let’s take a look at the UML class diagram below that shows the structure of the strategy pattern:

Structure of Strategy Pattern
Structure of the strategy pattern

You will need the following for the pattern:

  1. Strategy: It declares an interface common to all supported algorithms.

  2. ConcreteStrategy: To implement the algorithm using the strategy interface.

  3. Context: This is configured with the ConcreteStrategy object, maintains a reference to a strategy object, and may define an interface that lets strategy access its data.

An example of the strategy pattern can be seen when you think about different ways to cook food. You can grill, deep-fry, bake or broil. Each of these methods will cook the food, but by a different process. These processes, in our example, will be their own class.

For our code example, let’s look at a straightforward calculator that has four operations: add, subtract, multiply, and divide.

First, let’s create a new interface for our math operations, which is the strategy:

public interface IMathOperator
{
    int Operation(int a, int b);
}

Next, ConcreteStrategy is used to implement the interface for each algorithm. For now, let’s do add:

public class MathAdd: IMathOperator
{
    public int Operation(int a, int b)
    {
        return a+b;
    }
}

//implement remaining classes

Then, let’s put it into Context where we use the interface,  IMathOperator, in the code:

Dictionary<string, IMathOperator> strategies = new Dictionary<string, IMathOperator>();
strategies.Add("+", new MathAdd() );
strategies.Add("-", new MathSubtract() );
strategies.Add("*", new MathMultiply() );
strategies.Add("/", new MathDivide() ); 

So, let’s say the user inputs: +, 5, 2:

IMathOperator selectedStrategy = strategies["+"];
int result = selectedStrategy.Operation(5,2);
return result;

The output will be: 7.

As you can see, each math operation is its own class. It prevents us from using a larger class full of conditional events and gives better separation of responsibilities.

What Are the Advantages?

  • Each algorithm is in its own file. 

  • It decides on the appropriate behavior based on external inputs.

  • It follows the single responsibility principle, where each class has a single responsibility without having all the conditional events in one class. 

State Pattern

The last behavior pattern is the state pattern, which seeks to allow an object to change its behavior when its internal state changes. 

Think of an ATM. The internal state of the machine is “Debit card not inserted.” So, what are all the operations you can do?

  • You can insert the debit card.

  • You cannot eject the debit card because there isn't one in the machine.

  • You cannot enter the PIN and withdraw money because there’s no debit card inserted.

OK, now you have gone ahead and inserted your debit card. The operations available this time are:

  • You cannot insert a debit card because one is already inserted.

  • It allows you to eject the debit card.

  • You can enter the PIN and withdraw money.

As you can see, there are many different operations or behaviors, to perform that are dependent on the state of the debit card: inserted or not inserted.

Let’s take a look at the UML class diagram below that shows the structure of the state pattern:

Structure of the State Pattern
Structure of the state pattern

You will need the following for the pattern:

  • Context: It defines the interface of interest to clients.

  • State: It defines an interface for encapsulating the behavior associated with a particular state of the context.

  • ConcreteState: Each subclass implements a behavior associated with a state of context.

So, let’s use the ATM mentioned above as an example. First, let’s create the state interface:

public interface IAtmState
{
    void InsertedDebitCard();
    void EjectDebitCard();
    void EnterPin();
    void WithdrawMoney();
}

Next, remember that the ATM had two states: inserted and not inserted card. This is the ConcreteStates:

public class DebitCardInsertedState: IAtmState
{
    public void InsertedDebitCard()
    {
        Console.WriteLine("You cannot insert a Debit card because there already is one there.");
    }
 
    public void EnterPin()
    {
        Console.WriteLine("Pin number has been entered.");
    }
 
    //implement remaining methods
}
 
public class DebitCardNotInserted: IAtmState
{
    public void InsertedDebitCard()
    {
        Console.WriteLine("Debit Card inserted.");
    }
 
    public void EnterPin()
    {
        Console.WriteLine("You cannot enter your pin because no debit card is in the machine."); 
    }
}

As you can see, each concrete state provides its own implementation for a request. Therefore, when the context object state changes, its behavior will also change.

Now, create a context class that can have a number of internal states. Below we’ll do the  InsertDebitCard()  and  EnterPin()  state example:

public class AtmMachine : IAtmState
{
    public IAtmState atmMachineState { get; set; }
 
    public AtmMachine()
    {
        atmMachineState = new DebitCardNotInsertedState();
    }
 
    public void InsertDebitCardI()
    {
 
        if(atmMachineState is DebitCardNotInsertedState)
        {
            atmMachineState = new DebitCardInsertedState();
 
            Console.WriteLine("ATM machine state has been changed to Inserted.");
        }
    }
 
    …

In the main console application, let’s implement the ATM:

static void Main(string[] args)
{
    AtmMachine atmMachine = new AtmMachine();
 
    Console.WriteLine("ATM machine state is " + atmMachine.atmMachineState.GetType().Name);
 
    atmMachine.EnterPin();
    
    //implement remaining operations
}

Output:

The ATM state is DebitCardNotInsertedState.
You cannot enter your PIN because no debit card is in the machine.

What Are the Advantages?

  • It minimizes conditional complexity.

  • It is easier to add states.

  • It improves cohesion because the state-specific behaviors are aggregated into the ConcreteState classes.

Let’s Recap!

  • The observer pattern defines a one-to-many dependency between objects, and when one object changes state, all its dependents are notified and updated automatically. 

  • The strategy pattern defines several algorithms and then makes them interchangeable by encapsulating each as an object.

  • The state pattern seeks to allow an object to change its own behavior when its internal state changes. 

Great work! You’re now familiar with all three groups of design patterns. Let’s take a moment to recap everything we’ve covered in this course.

Example of certificate of achievement
Example of certificate of achievement