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.
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.