Entity Framework a Model-View-ViewModel


V poslední době jsem se snažil něco se dozvědět o správném (spíše co nejlepším) použití Entity Frameworku v aplikaci, kde zároveň používám vzor Model-View-ViewModel a TDD (Test-Driven Development). Zajímalo mě to hlavně z pohledu snadného testování, dobrého oddělení zodpovědnosti jednotlivých úrovní, i jednotlivých tříd atd.

V tomto článku jsem se pokusil shrnout své poznatky a to, jak to dělám já.

1) POCO Entities – náhrada automaticky vygenerovaných entit

Když se aplikace vyvíjí pomocí TDD, tak vzniká model nezávislý na databázi (nebo jiném zdroji dat) – vzniká model závislý na požadavcích (definovaných v testech).

Například nějaká nová funkce vyžaduje, abychom znali datum narození u objektu Person. Takže je nutné tuto vlastnost rychle přidat, ale rychlým přidáním rozhodně není myšleno – přidat sloupec do databáze, aktualizace Entity Data Modelu (EDMX) a tím vytvoření vlastnosti. Rychlým přidáním je myšleno, že v testu tuto vlastnost použiju a abych mohl test zbuildovat, tak pomocí funkce Generate property stub (více například ZDE) vytvořím požadovanou vlastnost.

Z toho je doufám zcela jasné, že takto se s automaticky vygenerovanými entitami pracovat nedá. Proto EF od verze 4 podporuje POCO entity. POCO znamená Plain Old CLR Objects, takže čistá třída bez automaticky vygenerovaného balastu.

Takže použití POCO entit je základ. Jak používat POCO v tomto článku nebudu vysvětlovat, doporučuju články na ADO.NET Blog – například Úvod, POCO 1, POCO 2 a POCO 3.

Ukázka POCO Entity:

public class Person
{
    public string FirstName { get; set; }

    public string LastName { get; set; }

    public DateTime BirthDate { get; set; }

    public Address Address { get; set; }
}

 

2) Vzory Unit of Work a Repository

Vysvětlovat tyto vzory by bylo minimálně na další článek, takže se do toho pouštět nebudu a raději odkážu na vysvětlení od někoho jiného – Patterns of Enterprise Application Architecture od Martina Fowlera (jako kniha nebo v jeho online katalogu – Unit of Work, Repository), MSDN a MSDN Magazine.

Ve zkratce jde o to, že pro práci s daty existují dobře známé a rozšířené vzory – Unit of Work a Repository. Unit of Work si v souvislosti s EF můžeme zjednodušeně představit jako ObjectContext a jeho jednotlivé Repository jako Class" href="http://msdn.microsoft.com/en-us/library/dd412719.aspx">ObjectSet.

Možná někoho napadne, proč bychom se tím měli zabývat, když EF už těmto vzorům odpovídá. Důvody jsou dva:

  1. ObjectContext a ObjectSet jsou pevně svázané s EF, takže se tím aplikace stává úzce svázanou s EF, což není ideální (naopak, je to špatně!).
  2. Složité testování při závislosti na těchto objektech.

Proto je lepší, definovat si rozhraní pro Unit of Work a pro jednotlivé Repository. Je jedno, jestli univerzální generické IRepository<T> nebo konkrétní IPersonsRepository atd.

Když potom ve ViewModelu programujeme ukládání dat, načítání dat atd., programujeme proti těmto rozhraním a psaní testů je úplně jednoduché a přímočaré a aplikace je na této úrovni stále nezávislá na Entity Frameworku!

Úplně nejjednodušší příklady těchto rozhraní:

public interface IUnitOfWork
{
    IPersonsRepository PersonsRepository { get; }

    void Commit();
}

public interface IPersonsRepository
{
    IEnumerable<Person> Persons { get; }

    void InsertPerson(Person person);

    void DeletePerson(Person person);
}

 

3) Konkrétní implementace rozhraní pro použití Entity Frameworku

Až do této chvíle je vše nezávislé na Entity Frameworku (nebo jiné technologii ukládání dat). Je tedy teoreticky možné se ještě v tuto chvíli rozhodnout, že se data budou ukládat do XML nebo úplně někam jinam… Ale článek se týká Entity Frameworku a tak je v tuto chvíli nutné vytvořit konkrétní implementaci rozhraní IUnitOfWork a IPersonsRepository.

Jak už bylo řečeno výše, tak Unit of Work odpovídá ObjectContextu a Repository ObjectSetu, toho nyní využijeme, takže výsledek by mohl vypadat nějak takto:

public class EntityFrameworkUnitOfWork : IUnitOfWork
{
    private readonly MyObjectContext objectContext;

    private IPersonsRepository personsRepository;

    public EntityFrameworkUnitOfWork(MyObjectContext objectContext)
    {
        this.objectContext = objectContext;
    }

    public IPersonsRepository PersonsRepository
    {
        get
        {
            if (this.personsRepository == null)
            {
                this.personsRepository = 
                    new PersonsRepository(this.objectContext.Persons);
            }

            return this.personsRepository;
        }
    }

    public void Commit()
    {
        this.objectContext.SaveChanges();
    }
}

public class PersonsRepository : IPersonsRepository
{
    private IObjectSet<Person> persons;

    public PersonsRepository(IObjectSet<Person> objectSet)
    {
        this.persons = objectSet;
    }

    public IEnumerable<Person> Persons
    {
        get { return this.persons.ToArray(); }
    }

    public void InsertPerson(Person person)
    {
        this.persons.AddObject(person);
    }

    public void DeletePerson(Person person)
    {
        this.persons.DeleteObject(person);
    }
}

Až tyto třídy jsou závislé na EF, takže pokud bychom se rozhodli používat místo EF něco jiného, stačí jinak implementovat tato rozhraní a to je vše!

4) ViewModel

Ve ViewModelu se bude pracovat s těmito rozhraními (jak už bylo řečeno výše), takže testování je snadné a přímočaré a případné změny na úrovni perzistence dat se ViewModelu vůbec nedotýkají.

S objekty ViewModelu, jako například s PersonViewModel se pracuje úplně stejně, jako obvykle. Takže například, když chceme někde zobrazovat věk osoby, nemusíme nijak zasahovat do modelu, ani nemusíme mít žádné výpočty věku ve View, ale tuto vlastnost vytvoříme právě v PersonViewModel, který například může mít referenci na původní objekt Person a z něj získá datum narození…

Závěr

Takto použitý Entity Framework, který je izolován jen na nejnižší úrovni aplikace a nikam nezasahuje, je přesně to, co jsem chtěl. O tom, že se to snadno testuje už jsem mluvil. Takže původní požadavky jsou splněné, ale kdyby měl někdo nějaké další tipy nebo zajímavé odkazy k této problematice, budu rád.

, , , , ,

Komentáře jsou uzavřeny.