DEV Community

Cover image for The Flyweight Pattern and Value Objects
Ahmed Shirin
Ahmed Shirin

Posted on • Updated on

The Flyweight Pattern and Value Objects

As software engineers and modelers, we usually come across different code design problems on a daily basis; most of these puzzles have already been solved and best practices have been laid to define a contract among developers detailing how these problems should be tackled. Out of the box solutions usually yield more complexity and a deviation from the norm, and that's why - in my opinion - I believe that deep knowledge of design patterns is an imperative factor affecting the quality of the team's codebase. However, knowledge of these patterns is just one half of the equation, the other half is being capable of choosing the appropriate situation to apply a design pattern solution, and being equally capable of spotting out when a pattern is being abused or when it just doesn't make sense at all.

One interesting Structural pattern is the Flyweight (Gamma et al, 1995). Often, developers face a situation where too many identical objects are being created; managing this process will define the difference between a stable system and an unusable one, and thats where the Flyweight comes to rescue.

But first of all, what does it mean for two objects to be identical?

Let's revisit an important distinction between two categories of objects in our models: Entities and Value Objects.

Conceptually, each human being is a separate "Entity" having their own identity and thread of continuity; a person can change their name, their hair color, their gender and their Nationality, yet their identity remains the same, implying that two persons having the exact same attributes/characteristics cannot be considered identical.

On the other hand, there exists several concepts in our perceivable world that lack an identity yet still they convey some meaning, a famous example is a one dollar bill; we don't really care which one dollar bill we possess as much as we care about its value (Value Object), and thus we would normally view all the one dollar bills as identical to each other.

When designing a model, the process of deciding whether an object (representing some concept in the domain sphere) possesses or lacks an identity is not a trivial issue and requires some understanding of the problem at hand, adding that it is highly likely that this decision could vary from one domain to the other.

Value Objects are more liberating than Entities, two Value Objects having the same attributes are considered identical, unlike Entities which require an identity based comparison mechanism. From this point, it is safe to state that Value Objects are interchangeable, meaning that multiple objects can reference/share an instance of a value object without leading to some conceptual dilemma. For example, a shipping application that doesn't need to uniquely identify each Vehicle in its fleet - implying that a Vehicle in this model is a Value Object - can safely create dozens of Shipments referencing a single instance of a Vehicle, when applicable.

As a corollary of the previous statement, Value Objects should be immutable; interchangeability implies that an operation having a side effect on an instance of the interchangeable object will result in a rippling effect across the system. Therefore, Commands applicable to a Value Object should be side effect free and return a new instance of the object instead of mutating it.

Conceptually, the immutability of Value Objects holds true, since a Value Object will always describe an observation of some concept in the universe. For example the color "Red" will always describe a distinct color having some observable characteristics. Adding the color "Green" to "Red" won't change the observable characteristics of the color "Red" but will instead yield a new color "Yellow" (This is analogous to a Command on a Value Object which returns a new instance of the object).

So back to the original problem where a system needs to manage the creation and existence of numerous identical objects. Logically, these objects should be Value Objects for a Flyweight Pattern refactoring to apply; the interchangeability of Value Objects allows developers to introduce an abstraction (Flyweight Factory) that is capable of reusing the set of preexisting objects (Flyweights) in order to get a reference to the object, while the immutability aspect of Value Objects allows clients to safely issue Commands on the Flyweights without worrying whether these Commands will have a side effect on the system.

So let's define the three main components of the pattern:

  • Flyweight: The Value Object to be interchanged within the system.

  • Flyweight Factory: An abstraction that is responsible for retrieving a Flyweight in an efficient manner.

  • The Client.

Assuming that we're working on a Shipping system that is cluttered with numerous Zone objects being assigned to different Shipping Routes. To apply a Flyweight Pattern refactoring, we first define the Zone as an immutable Value Object that will act as the Flyweight.

public class Zone : IEquatable<Zone>
//An immutable flyweight...This represents a domain value object
{
    public string ZipCode { get; }

    public IReadOnlyCollection<string> AvailableCouriers { get; private set; }

    public Zone (string zipCode) 
    {
        ZipCode = zipCode;
        AvailableCouriers = new List<string> ();
    }

    public Zone (string zipCode, IEnumerable<string> couriers) : this (zipCode) 
    {
        AvailableCouriers = new List<string> (couriers.Distinct ());
    }

    public Zone RegisterCourier (string courier) 
    {
        var newCouriers = new List<string> (AvailableCouriers) 
        {
            courier
        };

        return new Zone (ZipCode, newCouriers);
    }

    public bool Equals (Zone other) 
    {
        if (other == default (Zone))
            return false;

        if (ReferenceEquals (this, other))
            return true;

        if (!string.Equals (ZipCode, other.ZipCode, StringComparison.OrdinalIgnoreCase))
            return false;

        if (AvailableCouriers.Count != other.AvailableCouriers.Count)
            return false;

        if (!AvailableCouriers.All (x => other.AvailableCouriers.Contains (x)))
            return false;

        return true;
    }
}

We then introduce the ZoneFactory acting as the Flyweight Factory, a separate standalone Factory is justified in this case since the responsibility of managing the Flyweight collection doesn't belong to the Flyweight object. In this implementation, we are using a Dictionary to keep track of all the existing Zone Flyweights.

public class ZoneFactory
{
    private readonly IDictionary<string, Zone> _zones = new Dictionary<string, Zone>();
    private readonly object _lock = new object();

    public Zone GetZone(string zipCode)
    {
        lock (_lock)
        {
            if (_zones.TryGetValue(zipCode, out var zone))
            {
                return zone;
            }

            zone = new Zone(zipCode);
            _zones.Add(zipCode, zone);
            return zone;
        }
    }
}

Finally, the client is free to call the Flyweight Factory and issue Commands on the Flyweight.

public class ZoneClient
{
    static Random rnd = new Random();

    public IEnumerable<ShippingRoute> CreateShippingRoutes(IList<string> usZipCodes)
    {
        var routes = new List<ShippingRoute>();
        var zoneFactory = new ZoneFactory();

        for (var i = 0; i < 2000000; i++)
        {
            var destination = zoneFactory.GetZone(usZipCodes[rnd.Next(usZipCodes.Count)]);

            if(i % 100 == 0) //Issuing a command on the Value Object
                destination = destination.RegisterCourier("Fedex"); 

            routes.Add(new ShippingRoute(destination));
        }

        return routes;
    }
}

UPDATE #1:

It is important to note that Value Objects should not be confused with Value Types (.NET Structs, for example), actually I would prefer implementing Value Objects as immutable Classes rather than Structs, but after all, that's an implementation detail that has nothing to contribute to the conceptual meaning of a Value Object.

Top comments (0)