1
\$\begingroup\$

The goal is to create a Singleton and pass it a parameter that is required for the construction and initialization of the class, then preventing any changes to be made to the passed parameter (just like a readonly field being set by an argument passed to a constructor).

For instance:

  • Sockets
  • Hosts
  • Databases
  • Repositories
  • (Any instance that requires at least one argument in order to construct)

I am having a tough time coming to terms with this design, and I am quite certain that there is a pitfall or a loose-end to this implementation of a Singleton combined with a Builder Pattern, to mimic readonly fields set by constructor arguments.


Example Implementation

In this example, I am trying to get a Singleton of Host, where I would like the enum EnvironmentTypes to be treated like a readonly field usually found in classes that have parameters passed into the constructor.

EnvironmentTypes Enum

public enum EnvironmentTypes
{
    Production,
    Staging,
    Development
}

IHost Interface

public interface IHost
{
    string Name { get; set; }
}

Host Class

public sealed class Host : IHost
{
    #region Singleton
    private static readonly Lazy<Host> _instance = new Lazy<Host>(() => new Host());
    public static Host Instance { get { return _instance.Value; } }
    #endregion

    private static bool _isInstantiated;
    private static EnvironmentTypes _environment;
    private string _name;

    internal static EnvironmentTypes Environment
    {
        get { return _environment; }
        internal set
        {
            if (_isInstantiated) throw new InvalidOperationException(nameof(_environment) +" cannot be set once an instance is created."); 

            _environment = value;
        }
    }

    public string Name
    {
        get { return _name; }
        set { _name = value; }
    }

    static Host()
    {
        _isInstantiated = false;
        _environment = EnvironmentTypes.Production;
    }

    private Host()
    {
        _isInstantiated = true;
        _name = "My Server";
    }
}

HostBuilder Class

public sealed class HostBuilder
{
    private readonly EnvironmentTypes _environment;
    private string _name;

    public HostBuilder(EnvironmentTypes environment)
    {
        _environment = environment;
    }

    public HostBuilder SetName(string name)
    {
        _name = name;
        return this;
    }

    public IHost Build()
    {
        Host.Environment = _environment;
        Host host = Host.Instance;
        host.Name = _name;

        return host;
    }
}

Implementation

class Foo
{   
    void UsingTheBuilder()
    {
        // probably over-kill
        HostBuilder builder = new HostBuilder(EnvironmentTypes.Development)
            .SetName("Bingo");

        IHost host = builder.Build();

        //host.Environment is not available, great!
        host.Name = "Renamed Server"; // works as expected.
    }

    void ManualConfiguration()
    {
        Host.Environment = EnvironmentTypes.Development;
        Host host = Host.Instance;
        host.Name = "Bingo";

        Host.Environment = EnvironmentTypes.Staging; // throws! Hoped to prevent
                                                     // the developer from doing this.
    }
}

Random Notes: It would be great if I could restrict access from getting to the static properties of Host, so that I can totally avoid anyone trying to set the Host.Environment static property and throwing an exception -- note how the HostBuilder shields that from happening as it is a readonly field.

\$\endgroup\$
  • \$\begingroup\$ Is there any reason why Environment can't be an instance property and also accessed via the Instance? \$\endgroup\$ – t3chb0t May 24 '17 at 17:55
  • \$\begingroup\$ Oh, I think I get it now. You want to be able to set it once before the singleton is initialized and prevent subsequent changes. \$\endgroup\$ – t3chb0t May 24 '17 at 17:58
  • \$\begingroup\$ @t3chb0t - Right, exactly. I am trying to make it feel like a readonly parameter would. At least from the developer's point of view. \$\endgroup\$ – Svek May 24 '17 at 17:59
  • 1
    \$\begingroup\$ @t3chb0t - I updated the question to illustrate that it is meant as a somewhat generic question towards Singletons that need readonly-like parameters. \$\endgroup\$ – Svek May 24 '17 at 18:19
  • 1
    \$\begingroup\$ Unless I'm missing something, your builder's Build method can only be called once because the second call will throw an exception when Environment is set again. Is that intentional? \$\endgroup\$ – 404 May 24 '17 at 18:21
3
\$\begingroup\$

Disclaimer: I am biased towards singletons. I think it is an anti-pattern, that has no place in modern C#.

First, here is a great article on how singletons become a disaster when you try to unit test a code, that heavily relies on them. Your case is even more complex, because you also have to initialize additional parameters. And you can't change those. So you can't test Host with different "environments" unless you try to bypass your own exception with reflection.

I would just register non-static Host class as singleton inside IoC container, and be done with it. It will solve all your problems:

  1. Parameters of Host are no longer exposed.
  2. Container guaranties, that there is going to be a single instance of Host.
  3. Host is exposed as service (IHost) and not as implementation (Host).
  4. You can mock IHost in unit tests.
  5. You can easily unit-test Host implementation with whatever parameters you want, because now it has public constructor and can be re-created as often as it is required by your tests.
  6. Classes that depend on IHost will now require it as dependency, instead of secretly accessing it via global static property.
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.