Frage

Angenommen, Sie haben eine Anwendung, die in drei Ebenen unterteilt ist:GUI, Geschäftslogik und Datenzugriff.In Ihrer Geschäftslogikschicht haben Sie Ihre Geschäftsobjekte beschrieben:Getter, Setter, Accessoren usw.Du hast die Idee.Die Schnittstelle zur Geschäftslogikschicht garantiert eine sichere Nutzung der Geschäftslogik, sodass alle von Ihnen aufgerufenen Methoden und Zugriffsfunktionen die Eingabe validieren.

Dies ist ideal, wenn Sie zum ersten Mal den UI-Code schreiben, da Sie über eine klar definierte Schnittstelle verfügen, der Sie vertrauen können.

Aber hier kommt der knifflige Teil: Wenn Sie mit dem Schreiben der Datenzugriffsschicht beginnen, entspricht die Schnittstelle zur Geschäftslogik nicht Ihren Anforderungen.Sie benötigen mehr Accessoren und Getter, um Felder festzulegen, die ausgeblendet sind/verborgen wurden.Jetzt sind Sie gezwungen, die Schnittstelle Ihrer Geschäftslogik zu untergraben;Jetzt ist es möglich, Felder aus der UI-Ebene festzulegen, für die die UI-Ebene keine Geschäftseinstellung hat.

Aufgrund der für die Datenzugriffsschicht erforderlichen Änderungen ist die Schnittstelle zur Geschäftslogik so weit erodiert, dass es sogar möglich ist, die Geschäftslogik mit ungültigen Daten festzulegen.Somit gewährleistet die Schnittstelle keine sichere Nutzung mehr.

Ich hoffe, ich habe das Problem klar genug erklärt.Wie können Sie eine Erosion der Schnittstelle verhindern, das Verbergen und Kapseln von Informationen aufrechterhalten und dennoch den unterschiedlichen Schnittstellenanforderungen verschiedener Schichten gerecht werden?

War es hilfreich?

Lösung

Wenn ich die Frage richtig verstehe, haben Sie ein Domänenmodell erstellt und möchten einen objektrelationalen Mapper schreiben, um eine Zuordnung zwischen Datensätzen in Ihrer Datenbank und Ihren Domänenobjekten vorzunehmen.Sie befürchten jedoch, dass Ihr Domänenmodell mit dem „Klempner“-Code verunreinigt wird, der zum Lesen und Schreiben in die Felder Ihres Objekts erforderlich wäre.

Wenn wir einen Schritt zurückgehen, haben Sie im Wesentlichen zwei Möglichkeiten, wo Sie Ihren Datenzuordnungscode platzieren möchten – innerhalb der Domänenklasse selbst oder in einer externen Zuordnungsklasse.Die erste Option wird oft als Active Record-Muster bezeichnet und hat den Vorteil, dass jedes Objekt weiß, wie es sich selbst persistieren kann, und ausreichend Zugriff auf seine interne Struktur hat, um die Zuordnung durchzuführen, ohne dass nicht geschäftsbezogene Felder verfügbar gemacht werden müssen.

Z.B

public class User
{
    private string name;
    private AccountStatus status;

    private User()
    {
    }

    public string Name
    {
        get { return name; }
        set { name = value; }
    }

    public AccountStatus Status
    {
        get { return status; }
    }

    public void Activate()
    {
        status = AccountStatus.Active;
    }

    public void Suspend()
    {
        status = AccountStatus.Suspended;
    }

    public static User GetById(int id)
    {
        User fetchedUser = new User();

        // Lots of database and error-checking code
        // omitted for clarity
        // ...

        fetchedUser.name = (string) reader["Name"];
        fetchedUser.status = (int)reader["statusCode"] == 0 ? AccountStatus.Suspended : AccountStatus.Active;

        return fetchedUser;
    }

    public static void Save(User user)
    {
        // Code to save User's internal structure to database
        // ...
    }
}

In diesem Beispiel haben wir ein Objekt, das einen Benutzer mit einem Namen und einem AccountStatus darstellt.Wir möchten nicht zulassen, dass der Status direkt festgelegt wird, vielleicht weil wir überprüfen möchten, ob es sich bei der Änderung um einen gültigen Statusübergang handelt, sodass wir keinen Setter haben.Glücklicherweise hat der Zuordnungscode in den statischen Methoden GetById und Save vollen Zugriff auf die Namens- und Statusfelder des Objekts.

Die zweite Möglichkeit besteht darin, eine zweite Klasse zu haben, die für das Mapping verantwortlich ist.Dies hat den Vorteil, dass die verschiedenen Belange der Geschäftslogik und der Persistenz voneinander getrennt werden, wodurch Ihr Design testbarer und flexibler wird.Die Herausforderung bei dieser Methode besteht darin, wie die Namens- und Statusfelder der externen Klasse zugänglich gemacht werden.Einige Optionen sind:1.Verwenden Sie die Reflexion (die keine Bedenken über das Tiefpunkt in die privaten Teile Ihres Objekts enthält) 2.Stellen Sie speziell benannte öffentliche Setter bereit (z. B.Präfix sie mit dem Wort "privat") und hoffen, dass niemand sie versehentlich verwendet 3.Wenn Ihre Sprache dies unterstützt, machen Sie die Setter intern, gewähren Sie Ihrem Data Mapper-Modul jedoch Zugriff.Z.B.Verwenden Sie das InternalsVisibleToAttribute ab .NET 2.0 oder Friend-Funktionen in C++

Für weitere Informationen würde ich Martin Fowlers klassisches Buch „Patterns of Enterprise Architecture“ empfehlen.

Bevor Sie jedoch Ihre eigenen Mapper schreiben, empfehlen wir Ihnen dringend, die Verwendung eines ORM-Tools (Object Relational Mapper) eines Drittanbieters wie nHibernate oder Entity Framework von Microsoft in Betracht zu ziehen.Ich habe an vier verschiedenen Projekten gearbeitet, bei denen wir aus verschiedenen Gründen unseren eigenen Mapper geschrieben haben und es sehr leicht ist, viel Zeit mit der Wartung und Erweiterung des Mappers zu verschwenden, anstatt Code zu schreiben, der dem Endbenutzer einen Mehrwert bietet.Ich habe nHibernate bisher bei einem Projekt verwendet und obwohl die Lernkurve anfangs recht steil ist, zahlt sich die Investition, die Sie zu Beginn getätigt haben, erheblich aus.

Andere Tipps

Dies ist ein klassisches Problem – die Trennung Ihres Domänenmodells von Ihrem Datenbankmodell.Es gibt mehrere Möglichkeiten, es anzugreifen. Meiner Meinung nach hängt es wirklich von der Größe Ihres Projekts ab.Sie könnten das Repository-Muster verwenden, wie andere gesagt haben.Wenn Sie .net oder Java verwenden, können Sie Folgendes verwenden NHibernate oder Überwintern.

Was ich tue, ist zu verwenden Testgetriebene Entwicklung Also schreibe ich zuerst meine UI- und Modellebene und die Datenschicht wird simuliert, sodass die Benutzeroberfläche und das Modell um domänenspezifische Objekte herum aufgebaut sind. Später ordne ich diese Objekte der von mir verwendeten Technologie der Datenschicht zu.Es ist eine sehr schlechte Idee, das Design Ihrer App von der Datenbank bestimmen zu lassen, zuerst die App zu schreiben und später über die Daten nachzudenken.

PS: Der Titel der Frage ist etwas irreführend

@Eis^^Hitze:

Was meinen Sie damit, dass die Datenschicht die Geschäftslogikschicht nicht kennen sollte?Wie würden Sie ein Geschäftsobjekt mit Daten füllen?

Die Benutzeroberfläche fragt die ServiceClass in der Geschäftsebene nach einem Dienst, nämlich dem Abrufen einer Liste von Objekten, gefiltert nach einem Objekt mit den erforderlichen Parameterdaten.
Anschließend erstellt die ServiceClass eine Instanz einer der Repository-Klassen in der Datenschicht und ruft GetList(ParameterType-Filter) auf.
Dann greift die Datenschicht auf die Datenbank zu, ruft die Daten ab und ordnet sie dem allgemeinen Format zu, das in der „Domänen“-Assembly definiert ist.
Der BL hat mit diesen Daten keine Arbeit mehr zu tun und gibt sie daher an die Benutzeroberfläche aus.

Dann möchte die Benutzeroberfläche Element X bearbeiten.Es sendet das Element (oder Geschäftsobjekt) an den Dienst in der Business-Ebene.Die Business-Schicht validiert das Objekt und sendet es zur Speicherung an die Datenschicht, wenn es in Ordnung ist.

Die Benutzeroberfläche kennt den Dienst in der Geschäftsschicht, die wiederum über die Datenschicht Bescheid weiß.

Die Benutzeroberfläche ist für die Zuordnung der Benutzerdateneingaben zu und von den Objekten verantwortlich, und die Datenschicht ist für die Zuordnung der Daten in der Datenbank zu und von den Objekten verantwortlich.Die Business-Stufe bleibt rein geschäftlich.:) :)

Dies könnte eine Lösung sein, da die Schnittstelle dadurch nicht beeinträchtigt würde.Ich denke, Sie könnten eine Klasse wie diese haben:

public class BusinessObjectRecord : BusinessObject
{
}

Ich erstelle immer eine separate Assembly, die Folgendes enthält:

  • Viele kleine Schnittstellen (denken Sie an ICreateRepository, IReadRepository, IReadListRepsitory).die Liste geht weiter und die meisten von ihnen stützen sich stark auf Generika)
  • Viele konkrete Schnittstellen, wie ein IPersonRepository, das von IReadRepository erbt, verstehen Sie schon.
    Alles, was Sie nicht nur mit den kleineren Schnittstellen beschreiben können, fügen Sie in die konkrete Schnittstelle ein.
    Solange Sie das IPersonRepository zum Deklarieren Ihres Objekts verwenden, erhalten Sie eine saubere, konsistente Schnittstelle, mit der Sie arbeiten können.Aber der Clou ist, dass Sie auch eine Klasse erstellen können, die z.ein ICreateRepository in seinem Konstruktor, so dass es sehr einfach ist, mit dem Code einige wirklich abgefahrene Sachen zu machen.Auch hier gibt es Schnittstellen für die Services im Business-Tier.
  • Zum Schluss füge ich alle Domänenobjekte in die zusätzliche Assembly ein, um die Codebasis selbst etwas übersichtlicher und lockerer zu gestalten.Diese Objekte haben keine Logik, sie sind lediglich eine übliche Methode zur Beschreibung der Daten für alle 3+ Ebenen.

Übrigens.Warum sollten Sie Methoden in der Geschäftslogikschicht definieren, um der Datenschicht Rechnung zu tragen?
Die Datenschicht sollte keinen Grund haben, überhaupt zu wissen, dass es eine Geschäftsschicht gibt.

Was meinen Sie damit, dass die Datenschicht die Geschäftslogikschicht nicht kennen sollte?Wie würden Sie ein Geschäftsobjekt mit Daten füllen?

Ich mache das oft:

namespace Data
{
    public class BusinessObjectDataManager
    {
         public void SaveObject(BusinessObject object)
         {
                // Exec stored procedure
         {
    }
}

Das Problem besteht also darin, dass die Business-Schicht der Datenschicht mehr Funktionalität zur Verfügung stellen muss und das Hinzufügen dieser Funktionalität bedeutet, dass der UI-Schicht zu viel Funktionalität zur Verfügung gestellt wird?Wenn ich Ihr Problem richtig verstehe, hört es sich so an, als ob Sie versuchen, mit einer einzigen Schnittstelle zu viel zufriedenzustellen, und das führt nur dazu, dass es unübersichtlich wird.Warum nicht zwei Schnittstellen zur Business-Schicht haben?Eine davon wäre eine einfache, sichere Schnittstelle für die UI-Ebene.Die andere wäre eine untergeordnete Schnittstelle für die Datenschicht.

Sie können diesen Zwei-Schnittstellen-Ansatz auch auf alle Objekte anwenden, die sowohl an die Benutzeroberfläche als auch an die Datenebene übergeben werden müssen.

public class BusinessLayer : ISimpleBusiness
{}

public class Some3LayerObject : ISimpleSome3LayerObject
{}

Möglicherweise möchten Sie Ihre Schnittstellen in zwei Typen aufteilen, nämlich:

  • Schnittstellen anzeigen – das sind Schnittstellen, die Ihre Interaktionen mit Ihrer Benutzeroberfläche spezifizieren, und
  • Datenschnittstellen – das sind Schnittstellen, die es Ihnen ermöglichen, Interaktionen mit Ihren Daten festzulegen

Es ist möglich, beide Schnittstellensätze zu erben und zu implementieren, sodass:

public class BusinessObject : IView, IData

Auf diese Weise müssen Sie in Ihrer Datenschicht nur die Schnittstellenimplementierung von IData sehen, während Sie in Ihrer Benutzeroberfläche nur die Schnittstellenimplementierung von IView sehen müssen.

Eine andere Strategie, die Sie möglicherweise verwenden möchten, besteht darin, Ihre Objekte in der UI- oder Datenebene so zusammenzustellen, dass sie lediglich von diesen Ebenen genutzt werden, z. B.

public class BusinessObject : DomainObject

public class ViewManager<T> where T : DomainObject

public class DataManager<T> where T : DomainObject

Dies wiederum ermöglicht es Ihrem Geschäftsobjekt, weder die UI-/Ansichtsschicht noch die Datenschicht zu kennen.

Ich werde meine Angewohnheit, gegen den Strom zu schwimmen, fortsetzen und sagen, dass Sie sich fragen sollten, warum Sie all diese schrecklich komplexen Objektebenen erstellen.

Ich denke, viele Entwickler betrachten die Datenbank als eine einfache Persistenzschicht für ihre Objekte und kümmern sich nur um die CRUD-Operationen, die diese Objekte benötigen.Es wird zu viel Aufwand in die „Impedanzinkongruenz“ zwischen Objekt- und Beziehungsmodellen gesteckt.Hier ist eine Idee:aufhören zu versuchen.

Schreiben Sie gespeicherte Prozeduren, um Ihre Daten zu kapseln.Verwenden Sie Ergebnismengen, DataSet, DataTable, SqlCommand (oder das entsprechende Java/PHP/was auch immer) nach Bedarf aus dem Code, um mit der Datenbank zu interagieren.Sie brauchen diese Objekte nicht.Ein hervorragendes Beispiel ist das Einbetten einer SqlDataSource in eine .ASPX-Seite.

Sie sollten nicht versuchen, Ihre Daten vor irgendjemandem zu verbergen.Entwickler müssen genau verstehen, wie und wann sie mit dem physischen Datenspeicher interagieren.

Objektrelationale Mapper sind der Teufel.Hören Sie auf, sie zu benutzen.

Die Entwicklung von Unternehmensanwendungen ist oft eine Übung zur Bewältigung der Komplexität.Sie müssen die Dinge so einfach wie möglich halten, sonst haben Sie ein absolut nicht wartbares System.Wenn Sie bereit sind, eine gewisse Kopplung zuzulassen (die ohnehin jeder Anwendung innewohnt), können Sie sowohl Ihre Geschäftslogikschicht als auch Ihre Datenzugriffsschicht abschaffen (sie durch gespeicherte Prozeduren ersetzen) und benötigen keine davon Schnittstellen.

Lizenziert unter: CC-BY-SA mit Zuschreibung
Nicht verbunden mit StackOverflow
scroll top