12 August 2008 Dependency Injection, DI, HowTo, Interfaces, TDD, UnitTests Robert Muehsig

In einem HowTo ging es darum, was Interface eigentlich sind und wo man sowas einsetzen könnte. In vielen OOP Büchern wird immer wieder hervorgehoben, warum es so wichtig ist, "auf eine Schnittstelle zu programmieren" und nicht "auf eine konkrete Klasse". Bis vor kurzem war es mir selber noch nicht ganz klar, wozu dieser Aufriss mit den ganzen Schnittstellen, bis ich mit "Test Driven Development" angefangen habe.

Häufige Argumentation für Interfaces:

Wenn man sich etwas mit den Interfaces beschäftigt, dann kommt man immer wieder zum Datenbankbeispiel. "Stellen wir uns doch mal vor, die Datenquelle wechselt!"

Wie könnte ein Interface an dieser Stelle helfen?

An dieser Stelle können Interface helfen, indem sie wie eine Art "Vertrag" wirken:

interface IDataProvider
{
	List<string> GetData();
}

class OracleDataProvider : IDataProvider
{
	List<string> GetData()
	{
	// Oracle Zeugs
	}
}

class SqlDataProvider : IDataProvider
{
	List<string> GetData()
	{
	//MS Sql Zeugs
	}
}

Clients die diesen Code nutzen möchten brauchen, ähnlich wie beim ersten HowTo um Schnittstellen, nur noch das Interface nutzen:

class ClientCode
{
	public IDataProvider provider;
}

Dadurch kann man theoretisch die Datenbank nun wechseln so oft man möchte - unser ClientCode benötigt keine Änderung.

Allerdings...

ist dieses Beispiel meiner Meinung nach sehr "dürftig". Zwar kann man es daran sehr gut erklären, aber bei wie vielen Applikationen wird sowas überhaupt mit betrachtet? Bei vielen Projekten steht fest, welche DB genommen werden soll/genommen wird - die Datenbank wird sich sicherlich nicht ändern (und wenn doch, wird dies halt ein riesiger Change Request ;) ).

Was viele nicht betrachten ist, dass durch den Einsatz von Interfaces auch die Testbarkeit einer Applikation steigert.

Beispielanwendung:

image

Unser "Data" gibt Daten an den Service, dieser verarbeitet diese und reicht die weiter (3-Tier). Wie hier zu sehen ist, gibt es nur die konkrete Klasse "PersonService" (unser Service) und "Person" (unser Model) - alles andere sind Schnittstellen:

public class PersonService : IPersonService
    {
        private IAuthenticationService authentication;
        private IPersonDataProvider personProvider;

        public PersonService(IAuthenticationService authSrv, IPersonDataProvider provider)
        {
            authentication = authSrv;
            personProvider = provider;
        }

        public List<Person> GetPersons()
        {
            if (authentication.IsAuthenticated())
            {
                return personProvider.GetPersons();
            }
            
            return null;
        }

        public void AddPerson(Person p)
        {
            if (authentication.IsAuthenticated())
            {
                personProvider.AddPerson(p);
            }
        }

    }
Unser Service kennt nur die Interface vom AuthenticationServie und vom DataProvider - diese werden im Konstruktor mit übergeben.
Anmerkung: Diese Form der Übergabe von den "Abhängigkeiten" ist dem Feld der "Dependency Injection" zuzuordnen und werde ich in einem anderen HowTo genauer definieren.

Unser Service kann die Daten nur ausgeben, wenn wir authentifiziert sind.

Szenario:

Wenn wir nun ein größeres Projekt angehen und der Authentifizierungsmechanismus nicht so einfach ist und auch Fremdsysteme mit einbezieht, dann wäre es ja nett, wenn man während der Entwicklung eine Art "Fake" aufsetzen kann. Durch die Interface ist es sehr einfach, sowas zu machen:

image

In meinem UnitTest Projekt will ich meinen Service testen - und nur diesen(!). dafür habe ich mit ein "TestPersonDataProvider" erstellt, der "IPersonDataProvider" implementiert und jeweils ein AuthenticationService, welcher mir sagt, dass ich authentifiziert bin, oder nicht.

Mein Testcode:

    [TestClass]
    public class PersonServiceUnitTest
    {
        [TestMethod]
        public void PersonService_ReturnPersons_Work_With_Authentication()
        {
            TestPersonDataProvider provider = new TestPersonDataProvider();
            PersonService srv = new PersonService(new TrueTestAuthenticationService(), provider);
            Assert.AreEqual(provider.InMemoryPersonCollection.Count, srv.GetPersons().Count);
        }

        [TestMethod]
        public void PersonService_Should_Return_Null_Without_Authentication()
        {
            TestPersonDataProvider provider = new TestPersonDataProvider();
            PersonService srv = new PersonService(new FalseTestAuthenticationService(), provider);
            Assert.IsNull(srv.GetPersons());
        }
    }

In meinem Testcode kann ich nun einfach die jeweiligen "Fakes" übergeben - ohne den Clientcode ändern zu müssen.

Fazit:

Durch den Einsatz von Interfaces kann man sehr erweiterbare Systeme machen - ein Beispiel ist z.B. das MVC Framework von Microsoft. Der Nebeneffekt ist natürlich auch, dass man die Datenquelle wechseln kann oder dass man einfache Fakes in den Tests, aber auch im Produktionscode verwenden kann (jedenfalls bevor auf "Release" gedrückt wird ;)).

Schnittstellen erlauben erweiterbare und testbare Software!

[ Download Democode ]


Written by Robert Muehsig

Software Developer - from Saxony, Germany - working on primedocs.io. Microsoft MVP & Web Geek.
Other Projects: KnowYourStack.com | ExpensiveMeeting | EinKofferVollerReisen.de