Saturday, June 26, 2021

Know your design tools — The Performing Half-baked object case

Among other important paradigms in software design, object orientation is a common set of design techniques these days. Hierarchies of classes, derivative class instantiations and virtual calls can be often found on software codebases. Hence, the relevance of the Performing Half-baked object case while designing object-oriented software.

The Performing Half-baked object case occurs where a virtual call is executed onto an unborn or uncreated or uninitialized class instance: the actual behavior can be unexpected, of course.

A couple of examples.

Consider the output of this Windows/Visual C++ code:

#include <iostream>

class AA
{
    public:
    AA()
    {
        std::cout << "AA()\n";
        f();
    }
    virtual void f() { std::cout << "AA.f\n"; }
};

class BB : public AA
{
    public:
    BB()
    {
        std::cout << "BB()\n";
        f();
    }
    virtual void f() { std::cout << "BB.f\n";}
};

void main()
{
	auto x = new BB;
}

For another –different– behavior, consider the output of this .NET/Visual C# code:

using static System.Console;

class AA
{
    public AA()
    {
        WriteLine("AA()");
        f();
    }
    public virtual void f() => WriteLine("AA.f");
}

class BB : AA
{
    public BB()
    {
        WriteLine("BB()");
        f();
    }
    public override void f() => WriteLine("BB.f");
}

class Program
{
    public static void Main()
    {
        var x = new BB();
    }
}

The Performing Half-baked object is a case that designers of object-oriented languages and runtimes have typically faced. Any designer using those languages and runtimes should be aware of this and other cases during the process of object creation.

Singleton in .NET

If I need a process-level global write-once/read-many state, a global variable, a Singleton, in .NET Runtime, then I keep in mind what I said at the post: Know your design tools — The Singleton case.

It is also relevant to keep in mind:

  1. the Visual C# language specification about Static constructors,

  2. the section on Static Constructors of the Visual C# Programming Guide,

  3. the description of what BeforeFieldInit does, and

  4. the category (Performance) and the 'When to suppress warnings' section of the CA1810 Rule.

The observations on that old post have been useful for me over the years. Specially while designing stateful software services, where the use of locking, global state, global variables, or Singletons do not hinder throughput. On the other hand, in the context of stateless software services processing a high number of concurrent requests, whose scalability is very important, then –to be clear–, I do not prefer any explicit use of locking, any global state, any global variables or any Singleton.

Here is a demonstrative version of a .NET/Visual C#-based Singleton derived from the sample code of such old post and a couple of MSTest test cases as evidence of its correctness, included post-conditions for same class instance and for same global value:

#region SUT
/// <summary>
///  https://docs.microsoft.com/en-us/archive/blogs/marcod/know-your-design-tools-the-singleton-case
///  https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/classes#static-constructors
///  https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/static-constructors
///  https://docs.microsoft.com/en-us/dotnet/api/system.reflection.typeattributes?view=net-5.0
///  https://docs.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca1810
/// </summary>
public sealed class TimepointSingleton
{
    #region Nested static class with access to private outer instance constructor.
    public static class Timepoint
    {
        private static readonly TimepointSingleton instance;

        static Timepoint()
        {
            instance = new TimepointSingleton();
        }

        public static TimepointSingleton Instance { get => instance; }
    }
    #endregion

    #region Outer class for instance-level members of Singleton.
    private DateTime point;

    private TimepointSingleton()
    {
        point = DateTime.Now;
        WriteLine("Timepoint acquired");

        //This simulates resource contention to better
        //show up the multithreading problem when no
        //proper thread synchronization is in place.
        Thread.Sleep(4000);
    }

    public DateTime GlobalTimepoint { get => point; }

    public override string ToString() => $"{GlobalTimepoint:s}";
    #endregion
}
#endregion

Test cases (MSTest) as evidence of the expected and actual behavior:

[TestClass]
public class SingletonSpec
{
    [TestMethod]
    public void two_reads()
    {
        //Arrange
        TimepointSingleton a = null;
        TimepointSingleton b = null;

        //Act
        a = TimepointSingleton.Timepoint.Instance;
        b = TimepointSingleton.Timepoint.Instance;

        //Assert
        Assert.IsNotNull(a);
        Assert.IsTrue(AreTheseTheExactSameObject(a, b));
        Assert.AreEqual($"{a}", $"{b}");

        static bool AreTheseTheExactSameObject(object a, object b) => object.ReferenceEquals(a, b);
    }

    [TestMethod]
    public void concurrent_reads()
    {
        //Arrange
        int read_count = 15;
        using var sync = new ManualResetEvent(false);
        var reads = new ConcurrentBag<string>();
        var instances = new ConcurrentBag<TimepointSingleton>();
        var threads = Enumerable.Range(0, read_count).Aggregate(new List<Thread>(), (whole, next) => { whole.Add(new Thread(operation)); return whole; });

        //Act
        threads.ForEach(t => t.Start());
        sync.Set();
        threads.ForEach(t => t.Join());

        //Assert
        Assert.AreEqual(read_count, reads.Count);
        Assert.IsTrue(reads.All(read => string.CompareOrdinal(read, $"{TimepointSingleton.Timepoint.Instance.GlobalTimepoint:s}") == 0));
        Assert.AreEqual(read_count, instances.Count);
        Assert.IsTrue(instances.All(instance => object.ReferenceEquals(instance, TimepointSingleton.Timepoint.Instance)));

        void operation()
        {
            WriteLine($"[{Thread.CurrentThread.ManagedThreadId}] {DateTime.Now:s} Started");
            sync.WaitOne();
            var global_state = TimepointSingleton.Timepoint.Instance;
            reads.Add($"{global_state}");
            instances.Add(global_state);
            WriteLine($"[{Thread.CurrentThread.ManagedThreadId}] {DateTime.Now:s} Instance value: {global_state} hash: {global_state.GetHashCode()}");
        }
    }
}