Repository pattern

Jeg har længe søgt efter en relativ generisk måde hvorpå jeg kan strukturere mit repositoryinterface og jeg er efterhånden kommet frem til et repository interface, som dækker de fleste af mine behov for CRUD og listeudlæsninger.

void Load(FO, id); 
void Save(FO); 
void Delete(id); 
IEnumerable<FO> GetList(count, page, out total, criteria);

Load modtager et instantieret forretningsobjekt (her betegnet FO, men skal have den konkrete type eller et interface) og fylder objektet med data fra repository med det angivne id. Hvis data ikke findes rejses en kørselsfejl, for at undgå at objektet skal implementere en sporing af, om data er indlæst eller ej. Alternativt kan Load returnere en boolsk falsk.

Save tager data i FO og oprette eller opdatere data i repository. Id i FO opdateres til at modsvare data i repository, hvis der er tale om en oprettelse i repository.

Delete sletter data for FO i repository og nulstiller FO's id, således dette kan gemmes igen og dermed foranledige en oprettelse, hvis dette skulle findes nødvendigt.

GetList modtager nogle parametre som hjælper med paginering af udtrækket. Count er det antal elementer der skal udtrækkes og page er den side i det total udtræk (jf. de øvrige parametre i criteria-objektet), hvorfra elementerne skal udtrækkes. Total er det antal elementer der total set er i udtrækket. Criteria indeholder et (variabelt) antal parametre, som er relevante for det aktuelle FO's kontekst. Dette kunne f.eks. være søgetekst eller hvorvidt de udtrukne FO skal have en bestemt status. Det er helt op til kravene til FO og systemet.

I praksis

Et eksempel på hvordan dette kan tage sig ud i praksis kunne se ud som vist i det følgende.

Præmisserne her er, at data gemmes i en SQL Server og ORM er LinqToSql. Der tages udgangspunkt i et personregister, hvor hver person har registreret nogle oplysninger omkring sig, der giver lidt muligheder for at udforske metoden omkring søgning og paginering. Forretningsobjektet spejler i dette eksempel tabellen person databasen, så definitionen af databasen er undladt her.

Først defineres FO:

public class Person
{
   public int Id { get; set; }
   public string Navn { get; set; }
   public string Adresse { get; set; }
   public string Postnr { get; set; }
   public string Bynavn { get; set; }
   public string Email { get; set; }
   public string Telefonnr { get; set; }
}

Kriterieobjektet kunne se således ud:

public class PersonSearchCriteria
{
   public string searchText { get; set; }
}

Dernæst defineres repositoryobjektet:

public class PersonRepository
{
   public void Load(Person person, int id)
   {
      using(var db = new personDataContext())
      {
         var dbPerson = db.person.SingleOrDefault(p => p.Id == id);
         if(dbPerson != null)
            throw new Exception("Person ikke fundet");

         person.Id = dbPerson.Id;
         person.Navn = dbPerson.Navn;
         person.Adresse = dbPerson.Adresse;
         person.Postnr = dbPerson.Postnr;
         person.Bynavn = dbPerson.Bynavn;
         person.Email = dbPerson.Email;
         person.Telefonnr = dbPerson.Telefonnr;
      }
   }

   public void Save(Person person)
   {
      using(var db = new personDataContext())
      {
         var dbPerson = 
                 db.person.SingleeOrDefault(p => p.Id == person.Id);
         if(dbPerson == null)
         {
            dbPerson = new person();
            db.person.InsertOnSubmit(dbPerson);
         }
         dbPerson.Navn = person.Navn;
         dbPerson.Adresse = person.Adresse;
         dbPerson.Postnr = person.Postnr;
         dbPerson.Bynavn = person.Bynavn;
         dbPerson.Email = person.Email;
         dbPerson.Telefonnr = person.Telefonnr;

         db.SubmitChanges();

         person.Id = dbPerson.Id;
      }
   }

   public void Delete(int it)
   {
      using(var db = new personDataContext())
      {
         var dbPerson = db.person.SingleOrDefault(p => p.Id == id);
         if(dbPerson != null)
         {
            db.person.DeleteOnSubmit(dbPerson);
            db.SubmitChanges();
         }
      }
   }

   public IEnumerable<Person> GetList(
         int count, 
         int page, 
         out int total, 
         PersonSearchCriteria criteria
      )
   {
      using(var db = new personDataContext())
      {
         var list = db.person.AsQueryable();

         if(!String.IsNullOrWhiteSpace(criteria.searchText))
         {
            var txt = criteria.searchText;
            list = db.person.Where(p => 
                     p.Navn.Contains(txt)
                     || p.Adresse.Contains(txt)
                     || p.Postnr.Contains(txt)
                     || p.Bynavn.contains(txt)
                     || p.Email.Contains(txt)
                     || p.Telefonnr.Contains(txt));
         }

         total = list.Count();

         if(count > 0)
         {
            if(page > 1)
               list = list.Skip((page - 1) * count);

            list = list.Take(count);
         }

         return 
            from p in list
            select new Person {
               Id = dbPerson.Id,
               Navn = dbPerson.Navn,
               Adresse = dbPerson.Adresse,
               Postnr = dbPerson.Postnr,
               Bynavn = dbPerson.Bynavn,
               Email = dbPerson.Email,
               Telefonnr = dbPerson.Telefonnr
            };
      }
   }
}

Med dette kan man udtrække en liste af alle personer i databasen eller en liste af personer som møder visse kriterier, her et ord som indgår i navn, adresse, postnr, by, email eller telefonnr. Kriterierne kan udvides efter behov og selve kaldet til databasen bliver aldrig mere komplekst end der er behov for, fordi kriterierne undlades, hvis der ikke er specificeret nogen søgetekst. På samme måde undlades paginering, hvis count, dvs. det ønskede antal personer, er mindre end 1 (reglen er implicit eller pr. konvention om man vil).

Brugen af repository kunne se således ud og ligge som en static metode på Personobjektet eller i en factoryklasse til Person.

Factoryklassen kunne se såleds ud:

public class PersonFactory
{
   private readonly PersonRepository _repository;

   public PersonFactory(PersonRepository repository)
   {
      _repository = repository;
   }

   public IEnumerable<Person> GetAll(
         int count, 
         int page, 
         out int total
      )
   {
      return _repository.GetList(count, page, total, null);
   }

   public IEnumerable<Person> Search(
         int count, 
         int page, 
         out int total, 
         string searchText
      )
   {
      var criteria = new PersonSearchCriteria { 
            searchText = searchText 
         };
      return _repository.GetList(count, page, total, criteria);
   }
}

Som det kan ses, implementerer factoryklassen et mere specialiseret interface der ligger tættere på forretningskravene og som er mere ligetil at benytte. Dette sker uden at repository-interfacet bliver forplumret af flere metoder.

Hvis man ønsker at implementere flere kriterier med tiden, skal disse blot lægges i PersonSearchCriteria-objektet og derefter skal GetList-metoden ændres så de nye kriterier håndteres. Dermed ændres der ikke på repositoryinterfacet og man sparer derfor, at skulle rette i evt. andre klienter som er afhængige af respositoryklassen.

Se også

En artikel om et generisk repository, som kan gøre hele arbejdet med repositories noget enklere.

Comment