mark_h mark_h - 1 month ago 6
C# Question

Correct Use of Dependency Injection and YAGNI confusion in C#

I am aware that you should depend on abstractions not concrete implementations but I am also aware of the YAGNI principle. I sometimes find myself struggling to reconcile both of these.

Consider the following classes;

public class Foo
{
public void DoFoo()
{
}
//private foo stuff
}

public class Bar
{
private readonly Foo _foo;

public Bar()
{
_foo = new Foo();
}
}


"Bar" is the class I am interested in; obviously there is a problem, Bar is instantiating an instance of Foo, so let me refactor;

public class Bar
{
private readonly Foo _foo;

public Bar(Foo foo)
{
_foo = foo;
}
}


Great, but Bar's constructor still depends on Foo, a concrete implementation. I haven't gained anything (have I?). To fix this I need to make foo an abstraction and this is where my problem begins.

Every example I ever find always (understandably) demonstrates constructor injection using abstractions. I'm all for programming defensively but lets presume I have no need for any other implementations except Foo (test doubles don't count). To create an "IFoo" interface or a "FooBase" abstract class surely violates the YAGNI principle? I would be making something for a possible future scenario and I can always do that later e.g.

public abstract class Foo
{
public abstract void DoFoo();

//private foo stuff
}

public class Foo1:Foo
{
public override void DoFoo()
{
}
}


This doesn't break Bar and I could even do this for an interface provided I dropped the "I" convention (which I grow ever more sceptical of) e.g.

public interface Foo
{
void DoFoo();
}

public abstract class FooBase:Foo
{
public abstract void DoFoo();

//private foo stuff
}

public class Foo1:FooBase
{
public override void DoFoo()
{
}
}


What is wrong with injecting a concrete implementation since I can refactor this to an abstraction at a later stage (provided I give the abstraction the same name as the concrete implementation)?

Note: I am aware of the arguments for the "I" interface naming convention and this is not the point of my question. I am also aware that making Foo an abstract class will break the code wherever I was previously instantiating it, but presume I am using DI extensively and so I would only need to change the DI container registration, something I would probably have to do anyway if I were to introduce a new implementation of Foo.

Answer

but Bar's constructor still depends on Foo, a concrete implementation. I haven't gained anything (have I?).

What you gained here is that when the dependency Foo itself gets any dependencies of its own, or requires a different lifestyle, you can make this change without having to do sweeping changes throughout all consumers of Foo.

I have no need for any other implementations except Foo (test doubles don't count)

You can't just ignore unit testing in this. As Roy Osherove explained a long time ago, your test suite is another (equally important) consumer of your application with its own requirements. If adding the abstraction simplifies testing, you shouldn't need another reason for creating it.

To create an "IFoo" interface or a "FooBase" abstract class surely violates the YAGNI principle?

You won't violate YAGNI if you create this abstraction for testing. In that case YNI (You need it). By not creating the abstraction you you are optimizing locally within your production code. This is a local optimum instead of a global optimum, since this optimization doesn't take all the other (equally important) code that needs to be maintained (i.e. your test code) into consideration.

What is wrong with injecting a concrete implementation since I can refactor this to an abstraction

There isn't anything wrong per see to inject a concrete instance, although -as said- creating an abstraction could simplify testing. If it doesn't simplify testing and letting the consumer take a hard dependency on the implementation could be fine. But do note that depending on a concrete type can have its downsides. For instance, it becomes harder to replace it with a different instance (such as an interceptor or decorator) without having to make changes to the consumer(s). If this is not a problem, you might as well use the concrete type.