Repository / Service Design Pattern

82640
Jamal

Ich verwende das Repository / Service-Entwurfsmuster in meinen Projekten und habe etwas gefunden, das möglicherweise etwas redundant ist. Schreibe ich unnötigen Code?

In diesem Sinne ist hier meine Struktur:

public class Competition
{
    public int Id { get; set; }
    [Required] public string UserId { get; set; }
    public string UserName { get; set; }
    public DateTime DateCreated { get; set; }
    public DateTime? DateModified { get; set; }
    [Required] public string Name { get; set; }
}

Dies ist eine Beispiel-POCO-Klasse, aber wie Sie sehen werden, verwende ich, Genericsdamit es alles sein kann:

internal interface IRepository<TEntity> : IDisposable where TEntity : class
{
    IList<TEntity> GetAll();
    TEntity Get(int id);
    void Save(TEntity model);
    void Delete(int id);
}

Mein interfaceimplementiert IDisposableund ermöglicht, dass jedes Klassenobjekt wie folgt festgelegt wird TEntity:

internal class CompetitionRepository : IRepository<Competition>
{
    private readonly string userId;
    private readonly CompetitionProvider provider;

    public CompetitionRepository(string userId) : this(userId, "DefaultConnection")
    {
    }

    public CompetitionRepository(string userId, string connectionString)
    {
        this.userId = userId;
        this.provider = new CompetitionProvider(connectionString);
    }

    public IList<Competition> GetAll()
    {
        return provider.Get(this.userId);
    }

    public Competition Get(int id)
    {
        return GetAll().Where(model => model.Id == id).SingleOrDefault();
    }

    public void Save(Competition model)
    {
        provider.Save(model);
    }

    public void Delete(int id)
    {
        provider.Delete(id);
    }

    public void Dispose()
    {
        provider.Dispose();
    }
}

Mein Repository, das implementiert IRepository, spezifiziert Competitionals TEntity. Dieses Repository fragt meine Datenbank und somit den Provider ab.

In diesem Repository mache ich nur das Notwendigste, um meine Datenbank abzufragen:

public class CompetitionService : IRepository<Competition>
{

    private readonly string userId;
    private readonly IRepository<Competition> repository;

    public CompetitionService(string userId) : this(userId, "DefaultConnection")
    {
    }

    public CompetitionService(string userId, string connectionString)
    {
        this.userId = userId;
        this.repository = new CompetitionRepository(this.userId, connectionString);
    }

    public IList<Competition> GetAll()
    {
        return this.repository.GetAll();
    }

    public Competition Get(int id)
    {
        return this.repository.Get(id);
    }

    public void Save(Competition model)
    {
        if (model.Id > 0)
        {
            model.UserId = userId;
            model.DateCreated = DateTime.UtcNow;
        } else
        {
            model.DateModified = DateTime.UtcNow;
        }

        this.repository.Save(model);
    }

    public void Delete(int id)
    {
        this.repository.Delete(id);
    }

    public void Dispose()
    {
        this.repository.Dispose();
    }
}

Das ist jetzt mein Dienst. Sie können beispielsweise sehen, dass die SaveMethode Werte zuweist, abhängig davon, ob wir erstellen oder aktualisieren. Eine ServiceKlasse kann alles enthalten, z. B. Uploads durchführen oder was sich nicht auf die Datenquelle bezieht.

Sie Repositorykönnen nur mit der Datenquelle interagieren und es ist kein anderer Code vorhanden. Dadurch kann das Repository durch ein anderes ersetzt werden, das eine andere Datenquelle abfragt.

Mein Problem ist, dass mir aufgefallen ist, dass der Dienst zwar einen Code enthält, der die Werte ändert und korrigiert, bevor sie dann über das Repository gespeichert werden, die Struktur jedoch dem Repository sehr ähnlich ist, dh:

GetAll()

Get()

Save()

Delete()

Ich habe es auch umgesetzt IRepository<Competition>. Der Dienst hat möglicherweise andere öffentliche Methoden, aber es spielt keine Rolle, da ich ihn in meinen Controllern so nennen würde:

//
// POST: /Competitions/Create
[HttpPost]
public ActionResult Create(Competition model)
{
    try
    {
        using (var service = new CompetitionService(User.Identity.GetUserId()))
        {
            service.Save(model);
        }

        return RedirectToAction("Index");
    }
    catch
    {
        return View(model);
    }
}
  1. Denkst du, dass es eine gute Praxis ist oder glaubst du, dass ich es ein bisschen zu viel töte?
  2. Denken Sie, ich sollte IRepositoryden Dienst implementieren ?
  3. Meinst du, ich sollte mich im Extremfall überhaupt mit einer Dienstleistung beschäftigen?

(NB: Ich zögere es, den Dienst zu entfernen, da manchmal sehr viel Code eingegeben werden muss, der meine Controller nicht verstopft.)

Antworten
42
Warum Sie das UnitOfWork-Muster nicht verwenden, wie es im Martin-Blog http://martinfowler.com/eaaCatalog/unitOfWork.html beschrieben ist, entfernen Sie einfach die Methoden save und delete aus der Repository-Schnittstelle und erledigen Sie diese Aufgabe beim Festschreiben der Arbeitseinheit . Bassam Alugili vor 6 Jahren 0
Schnittstellen sollten IDisposable nicht "implementieren", sondern nur die konkrete Implementierung, ansonsten handelt es sich um eine * undichte Abstraktion *. Schein-Repos sind verfügbar? Mathieu Guindon vor 6 Jahren 1

3 Antworten auf die Frage

23
Henk Mollema

Du bist auf dem richtigen Weg. Die Trennung von Geschäftslogik (Services) und Datenzugriffslogik (Repositorys) ist eine gute Sache, ich empfehle das dringend. Ich habe ein generisches Repository erstellt, das die IRepository<TEntity>Schnittstelle implementiert . Es ist etwas anders als bei Ihnen, aber Sie haben den Punkt (ich habe eine BaseEntityEinschränkung, die Sie auch dafür verwenden classkönnen):

public class EfRepository<TEntity> : IRepository<TEntity>
    where TEntity : BaseEntity, new()
{
    private readonly IDbContext _context;
    private IDbSet<TEntity> _entities;

    public EfRepository(IDbContext context)
    {
        _context = context;
    }

    private IDbSet<TEntity> Entities
    {
        get { return _entities ?? (_entities = _context.Set<TEntity>()); }
    }

    public TEntity GetById(object id)
    {
        return Entities.Find(id);
    }

    public void Insert(TEntity entity)
    {
        Entities.Add(entity);
        _context.SaveChanges();
    }

    public void Update(TEntity entity)
    {
        _context.SaveChanges();
    }

    public void Delete(int id)
    {
        // Create a new instance of an entity (BaseEntity) and add the id.
        var entity = new TEntity
                     {
                         ID = id
                     };

        // Attach the entity to the context and call the delete method.
        Entities.Attach(entity);
        Delete(entity);
    }

    public void Delete(TEntity entity)
    {
        Entities.Remove(entity);
        _context.SaveChanges();
    }

    public IList<TEntity> Table
    {
        get { return Entities.ToList(); }
    }
}

Dadurch wird die Menge an doppeltem Code bei Verwendung bestimmter Repositorys erheblich reduziert. Ich benutze das in meinen Diensten wie:

public class ProductService : IProductService
{
    private readonly IRepository<Product> _productRepository;

    public ProductService(IRepository<Product> productRepository)
    {
        _productRepository = productRepository;
    }

    public bool AddProduct(Product product)
    {
        // Validate etc.
        _productRepository.Insert(product);

        // return validation result.
        return true;
    }
}

Wie Sie vielleicht bemerkt haben, verwende ich ein IoC-Framework, um die Abhängigkeiten einzufügen.

Ja, es scheint, als würden alle Antworten Abhängigkeitsinjektion verwenden. Ich hatte nicht richtig darüber nachgedacht, ich werde das auch anpassen. Prost vor 6 Jahren 0
What is the IoC framework you are using? Also, how do you go about configuring the EfRepository injection? Using AutoFac, I've done something like: builder.RegisterAssemblyTypes(typeof(ProjectRepository).Assembly).Where(t => to.Name.EndsWith("Repository")).AsImplementedInterfaces()... Who would that transfer? mickyjtwin vor 5 Jahren 0
@mickyjtwin I'm using [Simple Injector](https://simpleinjector.codeplex.com/) which allows me to do: `container.RegisterOpenGeneric(typeof (IRepository<>), typeof (EfRepository<>));`. Henk Mollema vor 5 Jahren 0
@HenkMollema, alles was du erklärt hast scheint Sinn zu machen, aber ich habe ein kleines Problem. Mein Repository implementiert IDisposable. Der Vorteil meiner Lösung ist, dass Repositorys einfache DB-Vorgänge ausführen und ich entscheide, wann die DB-Verbindung geschlossen werden soll. Problem ist, wenn ich Repository im Service verwenden möchte. Ich möchte IRepository am Ende jeder Servicemethode bereitstellen. Ich brauche einen Weg, um den Typ des IRepositorys zu speichern, nicht eine konkrete Instanz. Ich habe darüber nachgedacht, Code-Überlegungen zu verwenden, aber ich habe Angst vor der Leistung, und es wird mir unangenehm erscheinen. Kennen Sie ein Designmuster oder ein Werkzeug, um mein Ziel zu erreichen? pt12lol vor 5 Jahren 0
http://pastebin.com/Quhp8Mz7 - hier ist ein Lösungsbeispiel, an das ich gedacht habe. pt12lol vor 5 Jahren 0
Die Erstellung und Lebensdauer von Schnittstellenimplementierungen (DbRepository)zum Beispiel) sollte von einem [IoC-Container] (http://en.wikipedia.org/wiki/Inversion_of_control) verwaltet werden. Das Aufrüsten von Klassen, wie Sie es im Konstruktor tun, ist ein schlechtes Design und führt zu einer engen Kopplung mit Infrastrukturcode und Frameworks. Was versuchen Sie durch Speichern des Betontyps zu erreichen? Henk Mollema vor 5 Jahren 0
Ich möchte nur mehrere using-Anweisungen hinzufügen. Soweit ich weiß, wird der Gegenstand unbrauchbar und sollte erneut installiert werden. pt12lol vor 5 Jahren 0
Ich fand die andere Frage und Antwort, die ich meinte! Schauen Sie [hier] (http://stackoverflow.com/questions/12360271/what-happens-to-using-statement-when-i-move-to-dependency-injection). Sag mir bitte, was denkst du über diese akzeptierte Antwort? pt12lol vor 5 Jahren 0
@ pt12lol Ich denke es wird funktionieren. Dies ist jedoch nicht der Weg, um Fragen zu stellen. Bitte erstellen Sie eine neue Frage. Henk Mollema vor 5 Jahren 0
Warum ist der Dienst das Repository "implementieren"? Aus meiner Sicht sollte der Service das Verhalten der Repositorys verwenden und nicht definieren hanzolo vor 5 Jahren 0
@Hanzolo Wie implementiert der Service das Repository? Es wird nur in den Konstruktor eingefügt und in den Servicemethoden verwendet. Henk Mollema vor 5 Jahren 0
@HenkMollema - Ich muss über den Code der Op gesprochen haben, aber dann habe ich einfach einen tieferen Blick darauf geworfen, was es tut. Sie sind die Implementierung meiner bevorzugten Layering-Technik hanzolo vor 5 Jahren 0
@HenkMollema Wie teilen Sie Ihre Repository- und Service-Schnittstellen und konkrete Implementierungen projektübergreifend auf? dav_i vor 5 Jahren 0
@HenkMollema - mit Ihrer Lösung oben. Verwenden Sie EF Pocos in Ihrer Serviceschicht? Stokedout vor 4 Jahren 0
@Stokedout im Grunde ja. Obwohl die Entitäten nicht vollständig an EF gebunden sind (oder zumindest sollten). Möglicherweise möchten Sie bestimmte EF-Entitäten (DTOs) erstellen und diese ihren Domänenmodellen oder POCOs zuordnen. Henk Mollema vor 4 Jahren 0
@dav_i Entschuldigung für meine späte Reaktion. Sie können nebeneinander sein. Die beste Vorgehensweise besteht jedoch darin, Implementierungs-Agnostics-Schnittstellen in einer bestimmten 'Core'-Schicht zu haben und deren Implementierung in einer separaten Assembly auszuführen, die zur Laufzeit eingefügt wird. Henk Mollema vor 4 Jahren 0
13
Kristof

Ich träume manchmal davon, dass ich ein Gott bin, aber meine reale Situation könnte nicht weiter davon entfernt sein.

Ich bin jedoch bereit, meine Gedanken zu Ihrer Situation mitzuteilen.

Zunächst einmal würde ich Ihre Service-Schicht niemals loswerden. Ich bin absolut dafür, dass Ihre Controller so leicht wie möglich sind!

Ich bin nicht sicher, was Sie hinter dieser Repository-Schicht verwenden, aber so wie es aussieht, tun Ihre Respository-Klassen nicht viel, außer die Anrufe durchzuleiten. Vielleicht ein allgemeines Respository erstellen, das die meisten Entitäten behandelt, und wenn Sie etwas spezifischeres benötigen, erstellen Sie ein neues Respository.

Sie könnten auch in Erwägung ziehen, Ihren Dienst Providerdirekt von Ihrem Dienst zu verwenden, und diesen Anbieter als Repository behandeln.

Was IRepositorydie Serviceebene angeht, muss ich sagen, dass ich dagegen bin. Wenn Sie Ihren Service als Repository einrichten, ist Ihr Controller auf diese CRUD-ähnlichen Methoden beschränkt. Wenn Sie CRUD-ähnliche Bildschirme haben, kann dies schon frühzeitig klappen, aber ich bin kein großer Fan von Services auf diese Weise.

Meiner Meinung nach sollte der Dienst für jede logische Aktion, die Sie mit dieser Entität ausführen möchten, eine Methode bereitstellen.

Saveund Editkann ein Teil davon sein, aber so wäre AddNewAnswerToQuestion, RaiseFlagToQuestion, AddTagToQuestion(zum Beispiel). Ich fürchte, wenn Sie sich auf CRUD-ähnliche Methoden beschränken, müssen Sie entweder eine Menge ifStrukturen in Ihrer SaveMethode erstellen (wenn ein Tag hinzugefügt wurde, wenn ein Flag gesetzt wurde) oder Ihr Controller am Ende mehr und mehr Arbeit erledigt ( Aufrufmethode Save, dann Aufrufmethode Mail, da bei einem zusätzlichen Tag eine E-Mail gesendet werden muss).

Wenn Sie dies etwas extremer machen, machen Sie nicht mehr einen Dienst pro Entität, sondern einen Dienst pro Aktion .

An diesem Punkt hören Sie mit dem Aufrufen der Dienste auf, beginnen mit dem Aufrufen der Befehle und verwenden CQS (Command-Query Separation), aber diese Entscheidung liegt bei Ihnen :)

Kurz gesagt, ich würde ein generisches Repository erstellen oder ganz entfernen und meine Services expliziter machen, indem ich entweder mehr Methoden mache oder Befehle anstelle von Services erzeuge .

Netter Kommentar, du hast alles gesagt, woran ich dachte :) Prost vor 6 Jahren 0
11
Aleksei Chepovoi

Zunächst einmal: Warum implementieren Sie Ihr Repo nicht als generisch? Ich meine die Implementierung einer generischen Schnittstelle. In Ihrer Lösung müssen Sie IRepository<Entity>für jedes neue POCO eine Implementierung erstellen . Dadurch wird Ihr Code aufgebläht, und Sie werden keine generische Schnittstelle verwenden.

Hier ist das Beispiel einer generischen Repo-Implementierung:

public class Repository<T> : IRepository<T> where T : class, new()
{
    private IYourContext _context;

    public Repository(IYourContext context)
    {
        this._context = context;
    }

    public IQueryable<T> All
    {
        get { return _context.Set<T>(); }
    }

    public T Find(int id)
    {
        return _context.Set<T>().Find(id);
    }

    public void Delete(T entity)
    {
        if (_context.Entry(entity).State == EntityState.Detached)
            _context.Set<T>().Attach(entity);
        _context.Set<T>().Remove(entity);
    }

    public void Insert(T entity)
    {
        _context.Set<T>().Add(entity);
    }

    public void Update(T entity)
    {
        _context.Set<T>().Attach(entity);
        _context.Entry(entity).State = EntityState.Modified;
    }
}

Sie können unterschiedliche Methoden in Ihrer Schnittstelle und Implementierung verwenden. Wenn dies nicht der Fall ist, können Sie Ihr Repo wie folgt verwenden:

IRepository<YourPOCO> repo = new Repository<YourPOCO>();

Zweitens: Es gibt ein Muster Unit Of Work, das häufig mit dem generischen Repository-Muster implementiert wird. Es ist ein Wrapper um Repositorys, der eine Möglichkeit bietet, einen Kontext zwischen allen zu teilen.

Hier ist das Beispiel:

public interface IUnitOfWork : IDisposable
{
    IRepository<Product> ProductRepository { get; }
    // other repositories                

    void SaveChanges();
    void RollbackChanges();
}

Und die Umsetzung:

public class UnitOfWork : IUnitOfWork
{
    private IRepository<Product> _productRepository;
    private IYourContext _context;
    private bool _disposed = false;

    public UnitOfWork(IYourContext context)
    {
        this._context = context;
    }

    // Lazy Loading pattern is also often used here
    public IRepository<Product> ProductRepository
    {
        get
        {
            if (this._productRepository == null)
                this._productRepository = new Repository<Product>(_context);
            return _productRepository;
        }
    }

    public void SaveChanges()
    {
        _context.SaveChanges();
    }

    // implement other functionality
}

Wenn Sie in einer Arbeitseinheit Änderungen in verschiedenen Repositorys vorgenommen haben, müssen Sie die SaveChanges()Methode der UnitOfWorkKlasse nur einmal aufrufen . Es wird also eine einzelne Transaktion sein.

Vielleicht habe ich auch bemerkt, dass ich Schnittstellen überall benutze, selbst in der Implementierung des DB-Kontexts. Dies wird Ihnen helfen, Ihre Logik in der Zukunft bei Bedarf zu testen. Und verwenden Dependency Injection!

Für den Kontext:

public interface IYourContext : IDisposable
{
    DbSet<T> Set<T>() where T : class;
    DbEntityEntry Entry(object entry);
    int SaveChanges();
}

public class YourContext : DbContext, IYourContext
{
    public NorthwindContext(IConnectionFactory connectionFactory) : base(connectionFactory.ConnectionString) { }

    public DbSet<Product> Products { get; set; }
    // other DbSets ..
}

Und eine Verbindungsfabrik (ohne sie erhalten Sie eine Ausnahme im DI-Container):

public interface IConnectionFactory
{
    string ConnectionString { get; }
}

public class ConnectionFactory : IConnectionFactory
{
    public string ConnectionString
    {
        get { return "YourConStringName"; }
    }
}

Zusammenfassend: Implementieren Sie das generische Repository und die Arbeitseinheit als DAL (Data Access Layer). Anschließend können Sie beliebige Service-Layer mit Ihrer BL (Business Logic) darüber aufbauen.

Tolle Antwort, danke, ich habe dies als die Antwort zusammen mit den anderen markiert. Ich werde einen Blick auf das UnitOfWork-Muster werfen, da ich das vorher nicht verwendet habe. Prost vor 6 Jahren 0
Ich habe gelesen, dass unitofwork von EF erledigt wird? hanzolo vor 5 Jahren 4
@Hanzolo ja, und das Repository-Muster auch. Überprüfen Sie dieses https://softwareengineering.stackexchange.com/a/220126/198700 vcRobe vor einem Jahr 0