Project – Building a search machine – Behavioral Patterns-1

Let’s start with a simple, classic example to demonstrate how the Template Method pattern works.Context: Depending on the collection, we want to use a different search algorithm. We want to use a binary search for sorted collections, but we want to use a linear search for unsorted collections.Let’s start with the consumer, a REST endpoint in the Program.cs file that returns plain/text results:

var builder = WebApplication.CreateBuilder(args);
builder.Services
    .AddSingleton<SearchMachine>(x
        => new LinearSearchMachine(1, 10, 5, 2, 123, 333, 4))
    .AddSingleton<SearchMachine>(x
        => new BinarySearchMachine(1, 2, 3, 4, 5, 6, 7, 8, 9, 10))
;
var app = builder.Build();
app.MapGet(“/”, (IEnumerable<SearchMachine> searchMachines) =>
{
    var sb = new StringBuilder();
    var elementsToFind = new int[] { 1, 10, 11 };
    foreach (var searchMachine in searchMachines)
    {
        var typeName = searchMachine.GetType().Name;
        var heading = $”Current search machine is {typeName}”;
        sb.AppendLine(“”.PadRight(heading.Length, ‘=’));
        sb.AppendLine(heading);
        foreach (var value in elementsToFind)
        {
            var index = searchMachine.IndexOf(value);
            var wasFound = index.HasValue;
            if (wasFound)
            {
                sb.AppendLine($”The element ‘{value}’ was found at index {index!.Value}.”);
            }
            else
            {
                sb.AppendLine($”The element ‘{value}’ was not found.”);
            }
        }
    }
    return sb.ToString();
});
app.Run();

As highlighted in the first few lines, we configure LinearSearchMachine and BinarySearchMachine as two SearchMachine implementations. We initialize each instance using a different sequence of numbers.Afterward, we inject all registered SearchMachine services into the endpoint (highlighted in the code block). That handler iterates all SearchMachine instances and tries to find all elements of the elementsToFind array before outputting the text/plain results.Next, let’s explore the SearchMachine class:

namespace TemplateMethod;
public abstract class SearchMachine
{
    protected int[] Values { get; }
    protected SearchMachine(params int[] values)
    {
        Values = values ??
throw new ArgumentNullException(nameof(values));
    }
    public int?
IndexOf(int value)
    {
        if (Values.Length == 0) { return null; }
        var result = Find(value);
        return result;
    }
    protected abstract int?
Find(int value);
}

The SearchMachine class represents AbstractClass. It exposes the IndexOf template method, which uses the required hook represented by the abstract Find method (see highlighted code). The hook is required because each subclass must implement that method, thereby making that method a required extension point (or hook).Next, we explore our first implementation of ConcreteClass, the LinearSearchMachine class:

namespace TemplateMethod;
public class LinearSearchMachine : SearchMachine
{
    public LinearSearchMachine(params int[] values)
        : base(values) { }
    protected override int?
Find(int value)
    {
        for (var i = 0; i < Values.Length; i++)
        {
            if (Values[i] == value) { return i; }
        }
        return null;
    }
}

The LinearSearchMachine class is a ConcreteClass representing the linear search implementation used by SearchMachine. It contributes a part of the IndexOf algorithm through its Find method.Finally, we move on to the BinarySearchMachine class:

namespace TemplateMethod;
public class BinarySearchMachine : SearchMachine
{
    public BinarySearchMachine(params int[] values)
        : base(values.OrderBy(v => v).ToArray()) { }
    protected override int?
Find(int value)
    {
        var index = Array.BinarySearch(Values, value);
        return index < 0 ?
null : index;
    }
}

The BinarySearchMachine class is a ConcreteClass representing the binary search implementation of SearchMachine. As you may have noticed, we skipped the binary search algorithm’s implementation by delegating it to the built-in Array.BinarySearch method. Thanks to the .NET team!

The binary search algorithm requires an ordered collection to work; hence the sorting done in the constructor when passing the values to the base class (OrderBy). That may not be the most performant way of ensuring the array is sorted (precondition/guard), but it is a very fast to write and readable way to write it. Moreover, in our case, performance is not an issue.

If you must optimize such an algorithm to work with a large data set, you can leverage parallelism (multithreading) to help out. In any case, run a proper benchmark to ensure you optimize the right thing and assess your real gains. Look at BenchmarkDotNet (https://adpg.link/C5E9) if you are looking at benchmarking .NET code.

Leave a Reply

Your email address will not be published. Required fields are marked *



          Copyright © 2015-2024 | About | Terms of Service | Privacy Policy