Text Adventure Library - Builder Pattern


Welcome back fellow developers! Today I am going over how I build complex objects for my text adventure. I created the Noun class as the base class for all objects in my game, there are three derived classes, Person, Place and Thing. Person is for characters, Place is for locations and Thing is for items. I use a ECS approach and I use a dictionary of type <string, object> called attributes for the components. The components define the objects, so Persons all need specific attributes that all Persons will need and the same for Places and Things. This all makes creating instances of these objects a complicated thing and I needed a way to simplify it. A common way to simplify the creation of objects is to use the factory pattern. 

The Factory Pattern is a way to make creating objects easier. Instead of making each object directly, you ask a "factory" to make it for you. The factory knows how to make different kinds of objects and gives you the one you want. It's like ordering from a menu instead of cooking, you get what you want without having to do all the work of making it yourself.

The Factory Pattern is very useful and seemed like the right choice for me, but you can't customize it during the construction process. This makes it hard to create all the different objects in my game. 

Imagine you're running a toy factory that produces different types of toys—cars, dolls, robots, and more. Using the Factory Pattern, you'd need a separate factory for each type of toy. This means if you have ten different types of toys, you'd need ten separate factories.

Now, imagine each factory needs to handle not just one but multiple variations of the same type of toy—for example, cars with different colors or features. You'd either need to create a multitude of factories, each handling a specific combination of features, or you'd end up with a single, overly complex factory that tries to handle all possible variations.

In either case, managing all these factories becomes cumbersome and unwieldy. Adding new types of toys or variations would require modifying existing factories or creating entirely new ones, leading to a tangled web of dependencies and making maintenance and scaling a nightmare.

Enter the Builder Pattern! The Builder Pattern is a creational design pattern used to construct complex objects step by step. It separates the construction of a complex object from its representation, allowing the same construction process to create different representations. The Builder Pattern is particularly suitable for constructing complex objects that require multiple steps or configurations. It excels in scenarios where object creation involves intricate initialization or dependencies.

Unlike the Factory Pattern, which creates objects in one go, the Builder Pattern facilitates incremental object construction. It achieves this by caching the reference to the object being built within the builder itself and passing the builder instance as an argument to subsequent builder methods. This allows each method to modify the partially constructed object and maintain its state throughout the construction process, ultimately resulting in a fully configured object when the construction is complete.

So it's decided, the builder pattern is the way to go for this situation. Lets write the code!

Sure, let's break down the NounBuilder class and provide a detailed tutorial explaining each line:

public class NounBuilder<T> where T : Noun, new() 
{
     private T noun;
     private string[] requiredAttributes;
      // Constructor
     public NounBuilder(params string[] requiredAttributes)
     {
         noun = new T();
         this.requiredAttributes = requiredAttributes;
     } 
  • The NounBuilder<T> class is declared as a generic class, allowing it to work with various types of objects, but uses the new() constraint ensures that T must be a subclass of Noun.
  • Two private fields are declared: noun, which holds the object being constructed, and requiredAttributes, which stores an array of required attribute names. I do this to ensure that the objects created have everything they need to function properly.
  • The constructor initializes a new instance of T and assigns it to the noun field. It also receives a variable number of strings representing required attribute names and stores them in the requiredAttributes field.
     // Method to set a single attribute
     public NounBuilder<T> WithAttribute(string key, object value)
     {
         noun.AddOrSetAttribute(key, value);
         return this;
     }
     // Method to set multiple attributes
     public NounBuilder<T> WithAttributes(Dictionary<string, object> attributes)
     {
         foreach (var kvp in attributes)
         {
             noun.AddOrSetAttribute(kvp.Key, kvp.Value);
         }
         return this;
     } 
  • The WithAttribute method sets a single attribute of the object being constructed. It takes a key-value pair representing the attribute name and value, calls the AddOrSetAttribute method on the noun object, and returns the builder instance (this) to support method chaining.
  • The WithAttributes method sets multiple attributes of the object being constructed. It iterates over the key-value pairs in the provided dictionary, calls the AddOrSetAttribute method for each pair, and returns the builder instance (this) to support method chaining.

The Builder Pattern allows chaining which enables the sequential invocation of builder methods to set various attributes or configurations of an object. Chaining allows developers to set object attributes or configurations one after the other without needing to store intermediate results in variables. Each method in the builder class returns a reference to the builder instance (this), enabling subsequent method calls to be chained together seamlessly.

// Create a NounBuilder for constructing Person objects with required attributes "Name" and "Age" 
var builder = new NounBuilder<Person>("Name", "Age");
// Set attributes using method chaining 
var person = builder
     .WithAttribute("Name", "John Doe")
     .WithAttribute("Age", 30)
     .TryBuild(); 

The builder can support any number ofWithAttribute calls to specialize the object anyway you want. I included The WithAttributes method so I could also pass in dictionaries of attributes already defined to use as templates for specific objects.

Ensuring objects have all necessary attributes.

     // Method to check if all required attributes are present
     private bool CheckRequiredAttributes()
     {
         var presentAttributes = noun.Attributes.Keys;
         return requiredAttributes.All(attr => presentAttributes.Contains(attr));
     } 
  • The CheckRequiredAttributes method verifies whether all required attributes have been set for the object being constructed.
  • It retrieves the keys of the attributes currently set in the noun object and compares them with the required attribute names stored in the requiredAttributes field.
  • If all required attributes are present, it returns true; otherwise, it returns false.

Building the Object

     // Method to attempt building the object
     public T TryBuild()
     {
         if (CheckRequiredAttributes())
             return Build();
         else
             return null;
     }
     // Private method to finalize the construction process
     private T Build()
     {
         return noun;
     }
 } 
  • The TryBuild method is used to attempt building the object. It calls the CheckRequiredAttributes method to verify if all required attributes are present.
  • If all required attributes are present, it calls the Build method to finalize the construction process and return the fully constructed object; otherwise, it returns null.
  • The Build method is a private helper method that simply returns the noun object, representing the fully constructed object. I made the Build method private to prevent people from building improper objects, ones that do not have all the required attributes.

In conclusion, opting for the Builder Pattern over the Factory Pattern can significantly enhance the flexibility and precision of object construction in C#. While both patterns serve to simplify object creation, the Builder Pattern's ability to facilitate incremental construction makes it the right choice in this situation. Choose the Builder Pattern for incremental, customizable object construction, and elevate your coding experience to new heights. Happy building!

Leave a comment

Log in with itch.io to leave a comment.