Search code examples
c#oopconstructorinterfaceproperties

Preventing uninitialized properties when using interfaces / logic outside of the constructor


  • Up to now I typically executed my code logic in the constructor of my classes, which made sure that properties of the instance were defined and could not be accessed in an invalid state.
  • Recently I tried to improve my code quality and started using interfaces. However, as constructors are not part of the interface, it is now possible to create instances without code logic being executed (as the code logic is not contained in the constructor, but e.g. in an interface method called Handle).

In addition to my textual description, I want to include this reduced code example to make my issue more clear:

// my old coding style: no interface, logic in constructor
class MyClass1
{
    public string SomePropertyDerivedFromInput { get; set; }
    public MyClass1(string input)
    {
        SomePropertyDerivedFromInput = input + " derived";
    }
}

// my new coding style: using an interface, logic in instance method
internal interface IMyInterface
{
    void Handle(string input);
}

class MyClass2 : IMyInterface
{
    public string SomePropertyDerivedFromInput { get; set; }

    public void Handle(string input)
    {
        SomePropertyDerivedFromInput = input + " derived";
    }
}

// without interfaces I am required to pass my input to the constructor,
// so SomePropertyDerivedFromInput is always in a valid state
var myClass1 = new MyClass1("hello");
Console.WriteLine(myClass1.SomePropertyDerivedFromInput ?? "null");

// with interfaces however it is possible that SomePropertyDerivedFromInput
// isn't yet in a valid state when it's accessed, because the instance
// can be created without the Handle() method being called
var myClass2 = new MyClass2();
Console.WriteLine(myClass2.SomePropertyDerivedFromInput ?? "null");

The output of this is

hello derived
null

Boom, my new code with interfaces is more error prone when using it (2nd line in the output: null) than my previous code without interfaces.

So, when using interfaces, how do I ensure that the properties of the class implementing the interface are always in a valid state or at least can't be accidently used before they are in a valid state (meaning before Handle method is called)? What's the best practice for such cases? Introducing null checks and throwing exceptions? Can't believe that would be the best practice solution, making my code more and more complex.


Solution

  • The two examples are not equivalent. In the first case (without the interface), the MyClass1 constructor performs proper initialization, making sure that invariants hold. That's fine.

    You can view a constructor as a function that transforms the input arguments to a properly instantiated object. In this particular example, it's a function that takes a string as input and produces a valid MyClass1 object as output.

    In the second example, you now propose a Handle method that takes a string as input, but doesn't return anything as output. That is not equivalent to the first example.

    I don't mean that as a criticism of the question. Rather, I'm trying to explain why you're having trouble with encapsulation in the second example. You want to achieve parity, but the designs are not equivalent. This goes some distance towards explaining why you're encountering problems.

    Specifically in this case, if the goal is to produce a valid object from a string, an interface that models that might look like this:

    public interface IMyObjectFactory
    {
        IMyObject Create(string input);
    }
    

    Before I continue, I want to point out that too many IFooFactory objects are a design smell in its own right, but that's the best I can do with the example code suggested in the OP.

    Often, a better design that avoids too many factories is preferable, and possible, but I can't suggest anything based on the OP, since you don't describe what you actually want to do with the object.

    The bottom line, however, is that it's a really good idea to think about object contracts (pre- and post-conditions and invariants), and interfaces don't stop you from doing that. Initialization is, however, not part of interfaces, so must be regarded as implementation details.

    This doesn't mean that you can't define good encapsulation and make sure that objects are always in a valid state. What it does mean, on the other hand, is that different implementations of the same interface may have different ways of fulfilling the contract. Interfaces describe the contract that all implementations have in common, which leaves constructors as a good place to put implementation details.