© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2023
V. SarcarSimple and Efficient Programming with C# https://doi.org/10.1007/978-1-4842-8737-8_6

6. Separate Changeable Code Using Factories

Vaskaran Sarcar1  
(1)
Kolkata, West Bengal, India
 

A developer’s end goal is to make an application that meets the requirements of the customer. But the code must be easily extensible, maintainable, and stable enough to meet future needs too. It is tough to write the best version of a program on the very first attempt. You may need to analyze the code and refactor it several times. In this chapter, you will see such a scenario and learn how to use factories. To simplify the discussion, I start with a small program. We’ll keep analyzing the program and modifying it accordingly.

To make a stable application, an expert programmer attempts to make a loosely coupled system. The programmer tries to identify the code that can vary a bit. Once this is done, the programmer separates that part of the code from the remaining part of the codebase. Factories are best in this type of scenario.

POINTS TO REMEMBER

The obvious question is, what is a factory? In simple words, a factory is a code segment that handles the details of an object creation process. Any method that uses a factory is called a client of the factory. You should note that a factory can have many clients. These clients can use the factory to get an object, and later as per their needs, they invoke the methods of the object. So, it is up to a client how to use the object that is received from a factory. This is why separating the object creation process into a shared place is beneficial. Otherwise, you end up with duplicate code in multiple clients.

The Problem Statement

Assume that you write a program to demonstrate the behavior of two different animals, say tigers and cats. But you have a constraint that says you should not instantiate the animal object inside the client code. Why do you have this constraint? Here are some reasons:
  • You want to hide the instantiation logic from a client. You know that “change” is the only constant in the programming world. Let’s see what happens if the object’s instantiation logic resides on the client side. When you enhance your application to support a new type of object, you need to update the client code too. This demands retesting the client code as well.

  • There may be separate classes with methods that can also create cats or tigers. So, it is better to separate the code that instantiates a cat or a tiger in a shared place. In such a case, it does not matter how many clients use this code. Every client can refer to the common location to instantiate an object.

So, how can you separate the instantiation logic from the client code? You’ll see this in the upcoming demonstrations.

Initial Program

Following this requirement, let’s suppose that you write the following program that is shown in demonstration 1. Before you go through the complete program, here are some important points:
  • In this program, the AnimalFactory class is responsible for instantiating an object. It contains a method, called CreateAnimal(), to create a tiger or a cat instance.

  • The CreateAnimal() method is nonstatic. However, you can make it static. I discuss the pros and cons of using a static method in Chapter 14 of this book.

  • The AnimalFactory class acts like a factory class in this example. It contains a method, called CreateAnimal(), to create instances. So, you can say that this is the factory method in this example.

  • Inside the client code, you instantiate this factory class to get an animal. This is why the client uses the following code to get an animal and display its behavior:

AnimalFactory animalFactory = new ();
IAnimal animal = animalFactory.CreateAnimal("cat");
animal.DisplayBehavior();

Demonstration 1

Here is the complete program:
Console.WriteLine("***Creating animals and learning
  about them. ***");
AnimalFactory animalFactory = new();
IAnimal animal = animalFactory.CreateAnimal("cat");
animal.DisplayBehavior();
animal = animalFactory.CreateAnimal("tiger");
animal.DisplayBehavior();
interface IAnimal
{
    void DisplayBehavior();
}
class Tiger : IAnimal
{
    public Tiger()
    {
        Console.WriteLine(" A tiger is created.");
    }
    public void DisplayBehavior()
    {
        Console.WriteLine("""
         It roars.
         It loves to roam in the jungle.
         """);
    }
}
class Cat : IAnimal
{
    public Cat()
    {
        Console.WriteLine(" A cat is created.");
    }
    public void DisplayBehavior()
    {
        Console.WriteLine("""
         It meows.
         It loves to stay at a home.
         """);
    }
}
class AnimalFactory
{
    public IAnimal CreateAnimal(string animalType)
    {
        IAnimal animal;
        if (animalType.Equals("cat"))
        {
            animal = new Cat();
        }
        else if (animalType.Equals("tiger"))
        {
            animal = new Tiger();
        }
        else
        {
            Console.WriteLine("You can create either a
              cat or a tiger. ");
            throw new ApplicationException("An unknown
                animal cannot be instantiated.");
        }
        return animal;
    }
}

Output

Here is the output:
***Creating animals and learning about them. ***
A cat is created.
It meows.
It loves to stay at a home.
A tiger is created.
It roars.
It loves to roam in the jungle.

Analysis

The approach you use in demonstration 1 is quite common in programming. In the programming world, this has a name; we call it a simple factory pattern. Now let us analyze this program. Here are the important points:
  • You may need to enhance this application, because this application may need to create a different type of animal, say, a monkey, in the future. How can you proceed? You need to modify the AnimalFactory class and increase the if-else chain to consider the monkeys. But when you do this, you violate the OCP, and as a result, you need to retest the AnimalFactory class.

POINTS TO REMEMBER

When you see the switch statements, or an if-else chain to create different types of objects in a similar example, you get a clue that you may need to reopen the code to accommodate future changes. In the worst case, this code is replicated in several parts of an application. As a result, you keep violating the OCP, which can cause a serious maintenance problem in the future.

  • In this program, Visual Studio shows you a message that says, “CA1822: Member 'CreateAnimal' does not access instance data and can be marked as static. It also keeps saying: Members that do not access instance data or call instance methods can be marked as static. After you mark the methods as static, the compiler will emit non-virtual call sites to these members. This can give you a measurable performance gain for performance-sensitive code.”

  • I do not consider taking this suggestion now. The reasons are simple. First, performance sensitivity is not the key goal right now. Second, though a static method allows me to call the method without instantiating an object, it has disadvantages too. For example, you cannot change the behavior of a static method at runtime. As I said earlier, you’ll learn about this more in Chapter 14.

Better Program

By following the OCP principle, you can make the program better. So, in the upcoming demonstration, you will see a new hierarchy. I have kept the comments in to help you understand the code.
#region Factory hierarchy
/// <summary>
/// This class contains the "factory method"
/// </summary>
abstract class AnimalFactory
{
    // Deferring the instantiation process
    // to the subclasses.
    public abstract IAnimal CreateAnimal();
}
/// <summary>
/// CatFactory creates cats.
/// </summary>
class CatFactory : AnimalFactory
{
    public override IAnimal CreateAnimal()
    {
        return new Cat();
    }
}
/// <summary>
/// TigerFactory creates tigers.
/// </summary>
class TigerFactory : AnimalFactory
{
    public override IAnimal CreateAnimal()
    {
        return new Tiger();
    }
}
#endregion
Why is this helpful? In the upcoming demonstration, you’ll see that I use this construct in a way that the entire code segment is closed for modification. In the future, if you need to support a new animal type, say, a monkey, you need to do the following:
  • Create a Monkey class that will implement the IAnimal interface

  • Create a MonkeyFactory that will implement the AnimalFactory and provide the implementation for the CreateAnimal() method

So, it is enough for you to test the new classes only. Your existing code is untouched, and it is closed for modification.

Before you see the complete demonstration, I want you to note these two separate inheritance hierarchies, where one is for the animal hierarchy and another one is for the factory hierarchy. I have marked them inside this code for your ready reference. I also include Figure 6-1 for better clarity.
Figure 6-1

The class diagram shows the two different hierarchies in demonstration 2

Demonstration 2

Here is the complete demonstration:
Console.WriteLine("***Modified version of demonstration 1. ***");
// The CatFactory creates cats
AnimalFactory animalFactory = new CatFactory();
IAnimal animal = animalFactory.CreateAnimal();
animal.DisplayBehavior();
// The TigerFactory creates tigers
animalFactory = new TigerFactory();
animal = animalFactory.CreateAnimal();
animal.DisplayBehavior();
#region Animal hierarchy
interface IAnimal
{
    void DisplayBehavior();
}
class Tiger : IAnimal
{
    public Tiger()
    {
        Console.WriteLine(" A tiger is created.");
    }
    public void DisplayBehavior()
    {
        Console.WriteLine("""
         It roars.
         It loves to roam in the jungle.
         """);
    }
}
class Cat : IAnimal
{
    public Cat()
    {
        Console.WriteLine(" A cat is created.");
    }
    public void DisplayBehavior()
    {
        Console.WriteLine("""
         It meows.
         It loves to stay at a home.
         """);
    }
}
#endregion
#region Factory hierarchy
/// <summary>
/// This class contains the "factory method"
/// </summary>
abstract class AnimalFactory
{
    // Deferring the instantiation process
    // to the subclasses.
    public abstract IAnimal CreateAnimal();
}
/// <summary>
/// CatFactory creates cats.
/// </summary>
class CatFactory : AnimalFactory
{
    public override IAnimal CreateAnimal()
    {
        return new Cat();
    }
}
/// <summary>
/// TigerFactory creates tigers.
/// </summary>
class TigerFactory : AnimalFactory
{
    public override IAnimal CreateAnimal()
    {
        return new Tiger();
    }
}
#endregion

Output

Except for the first line, this output is the same as the previous output:
***Modified version of demonstration 1. ***
A cat is created.
It meows.
It loves to stay at a home.
A tiger is created.
It roars.
It loves to roam in the jungle.

Analysis

We can summarize this modified implementation with the following points:
  • Inside the client code, you decide which animal factory to use, a CatFactory or a TigerFactory.

  • The subclasses of AnimalFactory create a Cat instance or a Tiger instance.

  • Following this, we are supporting the OCP. As a result, it is a better and more extensible solution.

A New Requirement

In Chapter 4, I said that it is not always easy to fully implement this principle, but partial OCP compliance too can generate greater benefits for you. A new requirement can demand many changes in an application. In such a case, based on the situation, you have to choose one technique over another.

In the previous demonstrations, each factory could produce only one type of object, either a cat or a tiger, but there is no variation. For example, it makes perfect sense if a cat factory makes cats of different colors. Similarly, a tiger factory can produce tigers of different colors. If you receive such a requirement, how can you proceed? One option is to pass a color attribute inside the constructor and update the program accordingly.

The following is a sample demonstration for this. I have highlighted the important changes in bold.

Demonstration 3

Here is the complete implementation:
Console.WriteLine("***Modifying demonstration 2
  now. ***");
// The CatFactory creates cats
AnimalFactory animalFactory = new CatFactory();
IAnimal animal = animalFactory.CreateAnimal("black");
animal.DisplayBehavior();
// The TigerFactory creates tigers
animalFactory = new TigerFactory();
animal = animalFactory.CreateAnimal("white");
animal.DisplayBehavior();
#region Animal hierarchy
interface IAnimal
{
    void DisplayBehavior();
}
class Tiger : IAnimal
{
    public Tiger(string color)
    {
        Console.WriteLine($" A {color} tiger is created.");
    }
    public void DisplayBehavior()
    {
        Console.WriteLine("""
         It roars.
         It loves to roam in the jungle.
         """);
    }
}
class Cat : IAnimal
{
    public Cat(string color)
    {
        Console.WriteLine($" A {color} cat is created.");
    }
    public void DisplayBehavior()
    {
        Console.WriteLine("""
         It meows.
         It loves to stay at a home.
         """);
    }
}
#endregion
#region Factory hierarchy
abstract class AnimalFactory
{
    public abstract IAnimal CreateAnimal(string color);
}
class CatFactory : AnimalFactory
{
    public override IAnimal CreateAnimal(string color)
    {
        return new Cat(color);
    }
}
class TigerFactory : AnimalFactory
{
    public override IAnimal CreateAnimal(string color)
    {
        return new Tiger(color);
    }
}
#endregion

Output

This program produces the following output:
***Modifying demonstration 2 now. ***
A black cat is created.
It meows.
It loves to stay at a home.
A white tiger is created.
It roars.
It loves to roam in the jungle.

Analysis

You can see that lots of changes are required in this case. Is there any alternative way? I think so. This is why I have made demonstration 4 for you.

Since AnimalFactory is an abstract class, you can modify this class too to accommodate this change. In this alternative demonstration, I introduce a new method, MakeAnimal(), which accepts the color attribute before it calls the CreateAnimal() method to make an animal instance. Here is the code:
  abstract class AnimalFactory
    {
        public IAnimal MakeAnimal(string color)
        {
            Console.WriteLine($" The following animal
              color is {color}.");
            IAnimal animal= CreateAnimal();
            return animal;
        }
        public abstract IAnimal CreateAnimal();
    }

In the client code, you call the MakeAnimal() method instead of CreateAnimal() to see the effect of the updated code. Demonstration 4 shows you the complete example.

Demonstration 4

Here is the complete implementation:
Console.WriteLine("***Modifying demonstration 2 (an
  alternative approach).***");
// The CatFactory creates cats
AnimalFactory animalFactory = new CatFactory();
IAnimal animal = animalFactory.MakeAnimal("black");
animal.DisplayBehavior();
// The TigerFactory creates tigers
animalFactory = new TigerFactory();
animal = animalFactory.MakeAnimal("white");
animal.DisplayBehavior();
#region Animal hierarchy
interface IAnimal
{
    void DisplayBehavior();
}
class Tiger : Ianimal
{
    public Tiger()
    {
        Console.WriteLine("A tiger is created.");
    }
    public void DisplayBehavior()
    {
        Console.WriteLine("""
         It roars.
         It loves to roam in the jungle.
         """);
    }
}
class Cat : Ianimal
{
    public Cat()
    {
        Console.WriteLine("A cat is created.");
    }
    public void DisplayBehavior()
    {
        Console.WriteLine("""
         It meows.
         It loves to stay at a home.
         """);
    }
}
#endregion
#region Factory hierarchy
abstract class AnimalFactory
{
    public Ianimal MakeAnimal(string color)
    {
        Console.WriteLine($" The following animal
          color is {color}.");
        IAnimal animal = CreateAnimal();
        return animal;
    }
    public abstract IAnimal CreateAnimal();
}
class CatFactory : AnimalFactory
{
    public override IAnimal CreateAnimal()
    {
        return new Cat();
    }
}
class TigerFactory : AnimalFactory
{
    public override IAnimal CreateAnimal()
    {
        return new Tiger();
    }
}
#endregion

Output

Here is the output:
***Modifying demonstration 2 (an alternative approach).***
The following animal color is black.
A cat is created.
It meows.
It loves to stay at a home.
The following animal color is white.
A tiger is created.
It roars.
It loves to roam in the jungle.

Summary

Factories provide an alternative way to create objects. In this chapter, you saw the advantage of using factories. This chapter started with a simple factory class, which helps you to separate the code that is likely to vary from the other parts of the code. You put the instantiation logic inside the factory class to provide a uniform way to create objects.

Following the OCP principle, you further modified the application. In demonstration 2, you used a new hierarchy for factories where all concrete factories inherit from AnimalFactory, and you passed the details of object creation to a concrete factory (CatFactory or TigerFactory). Since you are following the OCP principle, you can add a new concrete factory, say MonkeyFactory, to create monkeys. And when you consider this scenario, you do not need to reopen the existing code. Instead, the new MonkeyFactory class can inherit from AnimalFactory, and following the rules, it can create monkeys. In this case, you need to create a Monkey class using the same approach I used for the Tiger class or the Cat class. Notice that you do not need to reopen the existing code.

I created demonstration 3 and demonstration 4 to support a new requirement. Maintaining the OCP in the current structure to accommodate the new requirement was tough because the color attribute was not considered at the beginning. In demonstration 3, you saw the use of a parameterized factory method to accommodate a new requirement. Finally, in demonstration 4, you saw that inside the abstract factory class, you can set a common rule that all derived concrete factories must follow. This approach helped you accommodate the particular change requirement with minimum changes.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.143.231.26