Muster in der Praxis

Konvention vor Konfiguration

Jeremy Miller

Inhalt

Die Auswirkungen der Sprachinnovation
Sag’s nur einmal
Vernünftige Standards
Konvention vor Konfiguration
Nächste Schritte

Haben Sie schon einmal darüber nachgedacht, wie viel Zeit in Ihren Projekten für grundlegende Probleme verwendet wird und wie viel Zeit Sie dafür aufwenden, mit rein technischen Problemen zu kämpfen? Es könnte behauptet werden, dass das wichtigste Ziel aller neuen Softwaretechnologien und Verfahren darin besteht, den Abstand zwischen der Absicht des Entwicklers für seine Software und der Realisierung dieser Absicht in Code zu verringern. Die Branche hat kontinuierlich die Abstraktionsebene angehoben, um Entwicklern die Möglichkeit zu geben, mehr Zeit für das Entwickeln der Funktionalität aufzuwenden, und weniger dafür, grundlegende Infrastruktur zu schreiben, aber es gibt immer noch viel zu tun.

Denken Sie eine Minute lang darüber nach. Wenn Sie Ihren Unternehmensvertretern Ihren Code zeigen wollten (vorausgesetzt, dass diese tatsächlich bereit wären, den Code mit Ihnen zu lesen), wie viel davon würde sie interessieren? Diese Unternehmensvertreter würden sich wahrscheinlich nur um den Teil des Codes kümmern, der die Geschäftsfunktionalität des Systems ausdrückt. Dieser Code ist das Wesentliche des Systems. Andererseits hätten sie wahrscheinlich nicht das geringste Interesse an Codebestandteilen wie Typdeklarationen, Konfigurationseinstellungen, try/catch-Blöcken und generischen Einschränkungen. Dieser Code ist die Infrastruktur (bzw. das „Zeremoniell“), die Sie, der Entwickler, durchgehen müssen, um den Code zu liefern.

In früheren Ausgaben dieser Rubrik habe ich größtenteils grundlegende Entwurfskonzepte und -prinzipien untersucht, von denen die meisten seit recht langer Zeit zum Entwurfskanon gehören. In diesem Monat möchte ich einige der neueren Verfahren betrachten, die Sie übernehmen können, um den zeremoniellen Anteil in Ihrem Code zu verringern. Einige dieser Konzepte mögen neu für Sie sein, aber ich glaube, dass sie alle innerhalb einiger Jahre zum .NET-Standard gehören werden.

Die Auswirkungen der Sprachinnovation

Der erste Faktor, den ich betrachten möchte, ist die Auswahl der Programmiersprache und ihre Verwendung. Um die Auswirkungen einer Programmiersprache auf den zeremoniellen Anteil im Code aufzuzeigen, lassen Sie uns einen kleinen Exkurs in die Geschichte wagen.

Vor einigen Jahren habe ich ein großes System in Visual Basic 6.0 erstellt. Jede Methode sah dem ähnlich, was Sie in Abbildung 1 sehen. Jedes Bisschen dieses Codes war zeremoniell.

Abbildung 1 Die zeremonielle Grundlage

Sub FileOperations()
    On Error Goto ErrorHandler

    Dim A as AType
    Dim B as AnotherType

    ' Put some code here

    Set A = Nothing
    Set B = Nothing
    ErrorHandler:
        Err.Raise Err.Number, _
            "FileOperations" & vbNewLine & Err.Source, _
            Err.Description, _
            Err.HelpFile, _
            Err.HelpContext
Exit Sub

Ich verwendete für jede Methode eine beachtliche Menge an Standardcode, um ein Äquivalent einer Stapelüberwachung für leichteres Debuggen zu erstellen. Ich musste auch Variable innerhalb der Methode dereferenzieren (Set A = Nothing), um diese Objekte zu veröffentlichen. Ich hatte einen strengen Standard für Codeüberprüfungen, nur um sicherzustellen, dass die Fehlerbehandlung und die Objektbereinigung für jede Methode richtig codiert sind. Der eigentliche Code, das Wesentliche des Systems, ging irgendwo inmitten dieses ganzen Zeremoniells unter.

Spulen wir vor zur Gegenwart und zu modernen Programmiersprachen wie C# oder Visual Basic .NET. Heute hat die Garbage Collection den größten Teil der expliziten Speicherbereinigung beseitigt, die beim Programmieren in Visual Basic 6.0 erforderlich war. Stapelüberwachungen in Ausnahmen sind in Microsoft .NET Framework selbst integriert, sodass Sie dies nicht mehr selbst übernehmen müssen. Wenn Sie an den Standardcode denken, der mit dem Wechsel zu .NET Framework beseitigt wurde, kann gesagt werden, dass die .NET-kompatiblen Sprachen produktiver und besser lesbar sind als Visual Basic 6.0, was auf die Verringerung an zeremoniellem Code zurückzuführen ist.

.NET Framework bedeutete einen großen Sprung, aber die Entwicklung der Sprachen ist noch nicht abgeschlossen. Betrachten wir eine einfache, auf klassische Weise implementierte Eigenschaft in C#:

public class ClassWithProperty {
  // Higher Ceremony
  private string _name;
  public string Name {
    get { return _name; }
    set { _name = value; }
  }
}

Das Wesentliche dieses Codes ist einfach, dass die ClassWithProperty-Klasse eine Zeichenfolgeneigenschaft namens „Name“ enthält. Lassen Sie uns schnell zu C# 3.0 vorspulen und das Gleiche mit einer automatischen Eigenschaft durchspielen:

public class ClassWithProperty {
  // Lower Ceremony
  public string Name { get; set; }
}

Dieser Code hat genau die gleiche Absicht wie die klassische Stileigenschaft, aber er erfordert deutlich weniger überflüssigen Compilercode.

Im Allgemeinen haben Softwareentwickler jedoch keine absolute Kontrolle über Programmiersprachen. Obwohl ich davon überzeugt bin, dass wir die Innovationen neuer Programmiersprachen oder sogar alternative Sprachen nutzen sollten, ist es an der Zeit, über Entwurfsideen zu sprechen, die Sie heute mit den gängigen Sprachen C# und Visual Basic verwenden können.

Domänenzentrierte Überprüfung

.NET Framework macht es mit Tools wie den ASP.NET Validator-Steuerelementen beinahe trivial, deklarative Überprüfung auf Feldebene in der Benutzeroberfläche hinzuzufügen. In jedem Fall denke ich, dass es von Vorteil ist, Überprüfungslogik in den Domänenmodellklassen oder zumindest in der Nähe in Domänendiensten unterzubringen, wofür es folgende Gründe gibt:

  1. Überprüfungslogik ist ein Geschäftslogikaspekt, und deshalb würde ich es bevorzugen, dass Geschäftslogik in Geschäftslogikklassen enthalten ist.
  2. Das Einfügen der Überprüfungslogik in das Domänenmodell oder die Domänendienste getrennt von der Benutzeroberfläche verringert möglicherweise die Duplizität über Bildschirme hinweg und ermöglicht eben dieser Überprüfungslogik, in nicht mit der Benutzeroberfläche verbundenen Diensten ausgeführt zu werden (z. B. in Webdiensten), die von der Anwendung verfügbar gemacht werden (einmal mehr „Sag’s nur einmal“).
  3. Es ist viel einfacher, Komponenten- und Akzeptanztests in Bezug auf Überprüfungslogik im Modell zu schreiben, als eben diese Logik als implementierten Teil der Benutzeroberfläche zu testen.

Sag’s nur einmal

Was geschieht, wenn Sie mitten in einem Projekt herausfinden, dass etwas in der Definition eines einzelnen Datenfelds geändert werden muss? In viel zu vielen Fällen wird sich diese kleine Änderung in der Datenbank auf Ihre gesamte Anwendung auswirken, wenn Sie eine analoge Änderung an verschiedenen Teilen der mittleren Ebene, am Datenzugriffscode und sogar an der Benutzeroberflächenschicht vornehmen, um eine einzelne logische Änderung unterzubringen.

Bei einem früheren Arbeitgeber nannten wir diesen Effekt, der aus einer kleinen Datenfeldänderung entsteht, „Wurmloch-Antimuster“. Sie möchten davon wegkommen, dass eine kleine Änderung an einer Stelle durch alle Schichten hindurch transferiert werden muss. Sie können den Wurmlocheffekt durch Entfernen unnötiger Schichten verlangsamen. Das ist der erste Schritt. Der schwierigere, aber viel effektivere Schritt besteht darin, eine Möglichkeit zu finden, dies nur einmal sagen zu müssen.

Das Prinzip „Sag’s nur einmal“ bedeutet, dass es nur eine einzige maßgebliche Quelle für alle Fakten oder Richtlinien im System als Ganzes geben sollte. Nehmen wir das Beispiel des Erstellens einer Webanwendung mit der Funktionalität zum Erstellen, Lesen, Aktualisieren und Löschen. Dieser Systemtyp ist größtenteils mit Bearbeiten und Speichern von Datenfeldern beschäftigt. Diese Felder müssen in Bildschirmen bearbeitet, auf dem Server überprüft, in der Datenbank gespeichert und hoffentlich auch auf dem Client auf eine bessere Benutzerfunktionalität hin überprüft werden. Sie möchten dies aber im Code nur einmal als „Dies ist ein Pflichtfeld, und/oder dieses Feld darf nicht mehr als 50 Zeichen umfassen“ festlegen.

Es gibt einige verschiedene Ansätze, die Sie verwenden können. Sie können das Datenbankschema zur Hauptdefinition machen und den Code sowohl auf mittlerer Ebene als auch auf Benutzerebene aus dem Schema generieren. Sie können auch die Datenfelder in einer Art externem Metadatenspeicher, wie z. B. einer XML-Datei, definieren und anschließend die Codegenerierung zum Erstellen des Datenbankschemas, sämtlicher Objekte mittlerer Ebene und der Benutzeroberflächen-Bildschirme verwenden. Ich selbst bin kein Fan groß angelegter Codegenerierung. Deshalb hat mein Team einen anderen Ansatz gewählt.

Im Allgemeinen entwerfen wir zuerst die Domänenmodellklassen und betrachten die Überprüfungslogik als Verantwortung der Domänenmodell-Entitätsklassen. Für unkomplizierte Überprüfungsregeln, wie z. B. Regeln für Pflichtfelder und maximale Zeichenfolgenlängen, werden die Eigenschaften von uns mit Überprüfungsattributen ausgestattet, wie z. B. im Fall der in Abbildung 2 dargestellten Address-Klasse.

Abbildung 2 Verwenden von Überprüfungsattributen

public class Address : DomainEntity {
  [Required, MaximumStringLength(250)]
  public string Address1 { get; set; }

  [Required, MaximumStringLength(250)]
  public string City { get; set; }

  [Required]
  public string StateOrProvince { get; set; }

  [Required, MaximumStringLength(100)]
  public string Country { get; set; }

  [Required, MaximumStringLength(50)]
  public string PostalCode { get; set; }

  public string TimeZone { get; set; }
}

Das Verwenden von Attributen ist ein unkompliziertes und ziemlich häufig anzutreffendes Verfahren zum Festlegen von Überprüfungsregeln. Sie können auch sagen, dass, seitdem die Überprüfungsregeln deklarativ ausgedrückt und nicht mehr mit imperativem Code implementiert werden, Sie den Test zwischen dem Wesentlichen und dem Zeremoniell bestanden haben.

Sie müssen nun jedoch die Regeln für Pflichtfelder und maximale Zeichenfolgenlängen in die Datenbank replizieren. In meinem Team verwenden wir NHibernate für unseren Persistenzmechanismus. Eine der leistungsfähigen Funktionen von NHibernate ist die Generierung des DDL-Codes (data definition language) aus den NHibernate-Zuordnungen. Dieser kann anschließend zum Erstellen des Datenbankschemas und zu seinem Synchronisieren mit dem Domänenmodell verwendet werden (diese Strategie funktioniert offensichtlich am besten in neuen Projekten). Damit diese Strategie der Datenbankgenerierung aus dem Domänenmodell nützlich sein kann, mussten wir den NHibernate-Zuordnungen zusätzliche Informationen hinzufügen, um Nicht-null-Felder zu markieren und Zeichenfolgenlängen festzulegen.

Wir haben die neue Fluent NHibernate-Methode zum Definieren unserer Objektzuordnungen verwendet. In unserem Setupcode für Fluent NHibernate haben wir automatische Konventionen in der Zuordnung eingerichtet, indem wir Fluent NHibernate lehrten, wie die Attribute [Required] und [MaximumStringLength] in unseren Modellklassen mit dem in Abbildung 3 dargestellten Code zu verarbeiten sind.

Abbildung 3 Verarbeiten von Attributen in NHibernate

public class MyPersistenceModel : PersistenceModel {
  public MyPersistenceModel() {
    // If a property is marked with the [Required]
    // attribute, make the corresponding column in
    // the database "NOT NULL"
    Conventions.ForAttribute<RequiredAttribute>((att, prop) => {
      if (prop.ParentIsRequired) {
        prop.SetAttribute("not-null", "true");
      }
    });

    // Uses the value from the [MaximumStringLength]
    // attribute on a property to set the length of 
    // a string column in the database
    Conventions.ForAttribute<MaximumStringLengthAttribute>((att, prop) => {
      prop.SetAttribute("length", att.Length.ToString());
    });
  }
}

Diese Konventionen werden nun auf alle Zuordnungen im Projekt angewendet. Für die Address-Klasse teile ich Fluent NHibernate einfach mit, welche Eigenschaften beibehalten werden sollen:

public class AddressMap : DomainMap<Address> {
  public AddressMap() {
    Map(a => a.Address1);
    Map(a => a.City);
    Map(a => a.TimeZone);
    Map(a => a.StateOrProvince);
    Map(a => a.Country);
    Map(a => a.PostalCode);
  }
}

Nachdem ich Fluent NHibernate Überprüfungsattribute beigebracht habe, kann ich die DDL für die Address-Tabelle generieren (siehe Abbildung 4). Beachten Sie im SQL in Abbildung 4, dass die Zeichenfolgenlängen der Definition aus den [MaximumStringLength]-Attributen in der Address-Klasse entsprechen. Die Werte NULL und NOT NULL folgen ebenfalls aus den [Required]-Attributen in der Address-Klasse.

Abbildung 4 Generieren des DDL-Codes

CREATE TABLE [dbo].[Address](
  [id] [bigint] IDENTITY(1,1) NOT NULL,
  [StateOrProvince] [nvarchar](100) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
  [Country] [nvarchar](100) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
  [PostalCode] [nvarchar](50) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
  [TimeZone] [nvarchar](100) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
  [Address1] [nvarchar](250) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
  [Address2] [nvarchar](100) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
  [City] [nvarchar](250) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
PRIMARY KEY CLUSTERED 
(
  [id] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, 
ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
) ON [PRIMARY]

Wir haben in eine Infrastruktur investiert, die die Datenbankstruktur aus den Überprüfungsattributen in den Domain Model-Objekten ableitet, aber wir haben immer noch das Problem der clientseitigen Überprüfung und des clientseitigen Erscheinungsbilds. Wir möchten die unkomplizierten Eingabeüberüberprüfungen innerhalb des Browsers durchführen und für eine bessere Benutzerfunktionalität irgendwie die Pflichtfeldelemente markieren.

Mein Team gelangte zu der Lösung, die HTML-Darstellung von Eingabeelementen auf Überprüfungsattribute aufmerksam werden zu lassen. (Wir verwenden übrigens ASP.NET Model View Controller (MVC) Framework Beta1 mit dem Web Forms-Modul als unser Ansichtmodul.) In Abbildung 5 wird dargestellt, wie das Markup für eine Adressenansicht in unserem System aussehen könnte.

Abbildung 5 Markup einer Adressenansicht

<div class="formlayout_body">
  <p><%= this.TextBoxFor(m => m.Address1).Width(300)%></p>
  <p><%= this.TextBoxFor(m => m.Address2).Width(300) %></p>
  <p>
    <%= this.TextBoxFor(m => m.City).Width(140) %>
    <%= this.DropDownListFor(m => 
        m.StateOrProvince).FillWith(m => m.StateOrProvinceList) %>
  </p>
  <p><%= this.TextBoxFor(m => m.PostalCode).Width(80) %></p>        
  <p><%= this.DropDownListFor(m => 
         m.Country).FillWith(m => m.CountryList) %></p>
  <p><%= this.DropDownListFor(m => 
         m.TimeZone).FillWith(m => m.DovetailTimeZoneList) %></p>
</div>

TextBoxFor und DropDownListFor sind kleine HTML-Hilfen in der allgemeinen Basisansicht, die für alle Ansichten in unserer MVC-Architektur verwendet werden. Die Signatur von TextBoxFor ist hier dargestellt:

public static TextBoxExpression<TModel> TextBoxFor< TModel >(
  this IViewWithModel< TModel > viewPage, 
  Expression<Func< TModel, object>> expression)

  where TModel : class {
    return new TextBoxExpression< TModel >(
    viewPage.Model, expression);
  }

Eine Besonderheit dieses Codes besteht darin, dass das Eingabeargument ein Expression ist (Expression<Func<TModel, object>>, um genau zu sein). Beim Erstellen des eigentlichen HTML für das Textfeld tut die TextBoxExpression-Klasse Folgendes:

  1. Analysieren von Expression und Suchen des genauen PropertyInfo-Objekts für die gebundene Eigenschaft.
  2. Befragen von PropertyInfo hinsichtlich der Existenz von Überprüfungsattributen.
  3. Entsprechendes Rendern des HTML für das Textfeld.

Wir haben einfach allen HTML-Elementen, die an die mit dem [Required]-Attribut markierten Eigenschaften gebunden sind, eine Klasse namens „required“ hinzugefügt. Sobald wir bei einer gebundenen Eigenschaft ein [MaximumStringAttribute] fanden, legten wir entsprechend das maxlength-Attribut des HTML-Textfelds fest, sodass es dem Attribut entsprach, und begrenzten die Benutzereingabe auf zulässige Längen. Das resultierende HTML sieht folgendermaßen aus:

<p><label>Address:</label>
<input type="text" name="Address1" value="" 
       maxlength="250" style="width: 300px;" 
       class="required textinput" /></p>

Das Erscheinungsbild von Pflichtfeldern ist recht einfach zu steuern, indem das Erscheinungsbild der erforderlichen CSS-Klasse bearbeitet wird (wir änderten die Farbe von Pflichtfeldern auf dem Bildschirm in ein helles Blau). Die eigentliche clientseitige Überprüfung führten wir mit dem jQuery Validation-Plug-In durch, das praktischerweise einfach die Existenz der required-Klasse in Eingabeelementen prüft. Die maximale Länge eines Textelements wird durch das Festlegen des maxlength-Attributs in Eingabeelementen durchgesetzt.

Dies war keineswegs eine vollständige Implementierung. Das Erstellen der eigentlichen Implementierung im Laufe der Zeit war nicht besonders schwierig. Der schwierige Teil war das Erörtern von Möglichkeiten, wie man Metadaten und Code vermeiden kann, die sich in mehreren Schichten wiederholen. Ich bin sicher, dass viele Teams die Art und Weise, in der mein Team die Datenbank aus dem Objektmodell generiert hat, nicht mögen werden, aber das ist in Ordnung, denn mein wirkliches Ziel ist, Ihnen einfach einige Ideen zu vermitteln, wie Sie das Prinzipal „Sag’s nur einmal“ verwenden können, um Ihre eigenen Entwicklungsbemühungen zu optimieren.

Vernünftige Standards

Im vorherigen Abschnitt habe ich ein sehr kleines Beispiel (über eine AddressMap-Klasse) für das Ausdrücken objektrelationaler Zuordnung (object relation mapping, ORM) mit Fluent NHibernate vorgestellt. Hier ist ein etwas komplizierteres Beispiel, das einen Verweis von einer Site-Klasse zu Adress-Objekten ausdrückt:

public SiteMap() {
  // Map the simple properties
  // The Site object has an Address property called PrimaryAddress
  // The code below sets up the mapping for the reference between
  // Site and the Address class
  References(s => s.PrimaryAddress).Cascade.All();
  References(s => s.BillToAddress).Cascade.All();
  References(s => s.ShipToAddress).Cascade.All();
}

Wenn Sie ORM-Tools konfigurieren, müssen Sie im Allgemeinen Folgendes tun:

  1. Angeben des Tabellennamens, dem eine Entity-Klasse zugeordnet wird.
  2. Angeben des Primärschlüsselfelds einer Entity-Klasse und meist auch das Festlegen einer Strategie zum Zuweisen der Primärschlüsselwerte. (Ist es eine automatische Zahl-/Datenbanksequenz? Oder weist das System die Primärschlüsselwerte zu? Oder verwenden Sie GUIDs für den Primärschlüssel?)
  3. Zuordnen von Objekteigenschaften oder Feldern zu Tabellenspalten.
  4. Beim Erstellen einer Zu-eins-Beziehung von einem Objekt zu einem anderen (wie z. B. bei der Zuordnung von Site zu Address) müssen Sie die Fremdschlüsselspalte angeben, die vom ORM-Tool verwendet werden kann, um übergeordnete und untergeordnete Datensätze zu verknüpfen.

Im Allgemeinen ist diese Arbeit sehr eintönig. Sie müssen das Zeremoniell befolgen, damit ORM Objektpersistenz bietet. Glücklicherweise können Sie einen Teil der Eintönigkeit durch Einbetten vernünftiger Standards (Sensible Defaults) in unserem Code beseitigen.

Sie haben vielleicht bemerkt, dass ich in den Zuordnungsbeispielen keine Tabellennamen, keine Primärschlüsselstrategie und keine Fremdschlüsselfeldnamen angegeben habe. In der Fluent NHibernate-Zuordnungssuperklasse meines Teams legen wir einige Standards für unsere Zuordnung fest:

public abstract class DomainMap<T> : ClassMap<T>, 
  IDomainMap where T : DomainEntity {

  protected DomainMap() {
    // For every DomainEntity class, use the Id property
    // as the Primary Key / Object Identifier
    // and use an Identity column in SQL Server,
    // or an Oracle Sequence
    UseIdentityForKey(x => x.Id, "id");
    WithTable(typeof(T).Name);
  }
}

Eigensinnige Software

Sie haben vielleicht bemerkt, dass ich die Annahme der Konventionen als „einschränkend“ beschrieben habe. Ein Teil der Philosophie, die hinter „Konvention vor Konfiguration“ steckt, besteht darin, „eigensinnige Software“ zu entwickeln, die künstliche Einschränkungen für den Entwurf erstellt.

Ein eigensinniges Framework beeinflusst die Arbeit der Entwickler insofern, als sie bestimmte Dinge auf eine bestimmte Weise erledigen müssen, was fast zur Beseitigung der Flexibilität führt. Befürworter eigensinniger Software glauben, dass diese Einschränkungen Entwicklungsprozesse effizienter gestalten können, indem Entwicklern Entscheidungen abgenommen werden und die Konsistenz gefördert wird.

Ein von meinem Team verwendeter Ansatz besteht darin, alle Domänenmodellklassen vollständig durch eine einzige lange Eigenschaft namens „Id“ zu identifizieren:

public virtual long Id { get; set; }

Es ist eine einfache Regel, aber sie hatte einige tiefgreifende Auswirkungen auf den Entwurf. Da alle Entitätsklassen auf die gleiche Weise identifiziert werden, konnte eine einzelne Repositoryklasse verwendet werden, statt spezialisierte Repositoryklassen für jede Entität oberster Ebene zu schreiben. Ebenso ist die URL-Handhabung in einer Webanwendung über Entitätsklassen hinweg konsistent, ohne dass Sie besondere Routingregeln für jede Entität registrieren müssen.

Dieser Ansatz verringert die Infrastrukturkosten für das Hinzufügen eines neuen Entitätstyps. Der Nachteil dieses Ansatzes besteht darin, dass es sehr schwierig wäre, einen natürlichen Schlüssel oder sogar einen zusammengesetzten Schlüssel unterzubringen oder eine GUID für einen Objektbezeichner zu verwenden. Das ist für mein Team kein Problem, aber es könnte leicht ein anderes Team von der Verwendung unseres Ansatzes abhalten.

Wie können Sie diese Ansätze nun durchsetzen? Der erste Schritt ist einfach die allgemeine Verständigung innerhalb eines Teams über diese Ansätze. Informierte Entwickler können diese Aspekte des eigensinnigen Entwurfs effektiver zu ihrem Vorteil einsetzen. Das Prinzip „Konvention vor Konfiguration“ kann aber auch beinahe eine Katastrophe sein, wenn die Entwickler nicht mit vorhandenen Konventionen vertraut sind oder Konventionen unklar sind.

Möglicherweise ziehen auch Sie die Verwendung eines statischen Codeanalysetools als Teil Ihrer kontinuierlichen Integration in Betracht, damit Ihre Projektkonventionen automatisch durchgesetzt werden.

In diesem Code legen wir eine Richtlinie fest, dass alle Klassen, die DomainEntity als Unterklasse haben, von der Id-Eigenschaft identifiziert werden, die über die Identitätsstrategie zugewiesen wird. Es wird angenommen, dass der Tabellenname identisch mit dem Namen der Klasse ist. Wir können nun diese Auswahl immer klassenspezifisch außer Kraft setzen, aber wir haben dies selten tun müssen (eine Klasse namens „User“ musste einer Tabelle namens „Users“ zugeordnet werden, nur um einen Konflikt mit einem reservierten Wort in SQL Server zu vermeiden). Auf die gleiche Art und Weise geht Fluent NHibernate von einem Fremdschlüsselnamen aus, basierend auf dem Eigenschaftennamen, der auf eine andere Klasse verweist.

Natürlich erspart dies nicht viele Zeilen Codes pro Zuordnungsklasse, aber es verbessert die Lesbarkeit der veränderlichen Zuordnungsteile, indem der mehr oder weniger überflüssige Code in der Zuordnung verringert wird.

Konvention vor Konfiguration

Softwareentwickler suchten einen Weg, die Produktivität zu erhöhen und Systeme dynamischer zu gestalten, indem sie das Verhalten aus imperativem Code in deklarative XML-Konfiguration verlegten. Viele Entwickler fühlten, dass die Verbreitung von XML-Konfiguration zu weit ging und zu einer schädlichen Praxis wurde. Die Strategie „Standards vor expliziter Konfiguration“ wird auch „Konvention vor Konfiguration“ genannt.

„Konvention vor Konfiguration“ ist eine Entwurfsphilosophie und ein Verfahren, bei dem, statt expliziten Code zu verlangen, Standards angewendet werden, die sich aus der Struktur des Codes ergeben. Die Idee ist, die Entwicklung zu vereinfachen, indem den Entwicklern die Möglichkeit geboten wird, sich nur um die unkonventionellen Teile der Anwendung und der Architektur kümmern zu müssen.

Momentan spielen viele Entwickler eifrig mit ASP.NET MVC Framework und experimentieren mit unterschiedlichen Arten seiner Verwendung. Im MVC-Modell der Webentwicklung gibt es einige Quellen für sich wiederholenden Code, was eine großartige Gelegenheit sein kann, das Prinzip „Konvention vor Konfiguration“ anzuwenden.

Hier sind die fünf Schritte im grundlegenden Fluss einer einzelnen Anforderung im MVC-Modell:

  1. Empfangen einer URL vom Client. Das Routingsubsystem analysiert die URL und bestimmt den Namen des Controllers, der diese URL verarbeitet.
  2. Erstellen oder Suchen des richtigen Controllerobjekts aus dem Controllernamen, der durch das Routingsubsystem bestimmt wird.
  3. Aufrufen der richtigen Controllermethode.
  4. Auswählen der richtigen Ansicht und Marshallen der Modelldaten, die von der Controllermethode zu dieser Ansicht generiert werden.
  5. Rendern der Ansicht.

Standardmäßig gibt es beim Erstellen von Webseiten mit ASP.NET MVC Framework ein gewisses Maß an sich wiederholendem Zeremoniell, das Sie durch Annehmen einiger einschränkender Konventionen verringern können.

Die erste Aufgabe besteht darin, eine eingehende URL mit der Website mit der richtigen Controllerklasse zu verbinden. Die Routing-Bibliothek in MVC Framework kann eine URL befragen und den Namen des Controllers bestimmen. MVC Framework wird dann das registrierte IControllerFactory-Objekt nach dem Controllerobjekt fragen, das dem Controller mit dem Controllernamen entspricht, der aus der eingehenden URL bestimmt wird.

Viele Teams delegieren die Controllerkonstruktion an ein IOC-Tool (inversion of control, Steuerumkehrung). In meinem Team verwenden wir das Open-Source-StructureMap-Tool zum Auflösen von Controllerinstanzen nach dem Namen:

public class StructureMapControllerFactory 
  : IControllerFactory {

  public IController CreateController(
    RequestContext requestContext, string controllerName) {

    // Requests the named Controller from the 
    // StructureMap container
    return ObjectFactory.GetNamedInstance<IController>(
      controllerName.ToLowerInvariant());
  }
}

Den Controller anzufordern, ist recht einfach, aber zuerst müssen Sie alle Controllerklassen mit dem Namen im IOC-Container registrieren. Warten Sie! Fügt dieses Verfahren der Architektur nicht noch mehr Zeremoniell hinzu? Vor einem oder zwei Jahren wäre ich mit expliziter IOC-Konfiguration der Controllerklassen vorausgeeilt:

public static class ExplicitRegistration {
  public static void BootstrapContainer() {
    ObjectFactory.Initialize(x => {
      x.ForRequestedType<IController>().AddInstances(y => {
        y.OfConcreteType<AddressController>().WithName("address");
        y.OfConcreteType<ContactController>().WithName("contact");

        // and so on for every possible type of Controller
      });
    });
  }
}

Dieser Code verkörpert reine Eintönigkeit und Zeremoniell, die nur existieren, damit das IOC-Tool versorgt werden kann. Wenn Sie den Registrierungscode näher betrachten, werden Sie merken, dass er einem konsistenten Muster folgt. AddressController ist als Adresse und ContactController als Kontakt registriert. Statt jeden Controller explizit zu konfigurieren, könnten Sie einfach eine Konvention für das automatische Bestimmen des Routingnamens jeder Controllerklasse erstellen.

Glücklicherweise gibt es in StructureMap direkte Unterstützung für konventionsbasierte Registrierung. Deshalb können Sie ein neues ControllerConvention erstellen, das automatisch jeden konkreten Typ von IControllers registriert:

public class ControllerConvention : TypeRules, ITypeScanner {
  public void Process(Type type, PluginGraph graph) {
    if (CanBeCast(typeof (IController), type)) {
      string name = type.Name.Replace("Controller", "").ToLower();
      graph.AddType(typeof(IController), type, name);
    }
  }
}

Als Nächstes benötigen Sie Code, der den StructureMap-Container mit der neuen Konvention startet (siehe Abbildung 6). Sobald das neue ControllerConvention implementiert und Teil des IOC-Container-Bootstrapping ist, werden alle neuen Controller, die Sie der Anwendung hinzufügen, der IOC-Registrierung automatisch ohne explizite Konfiguration seitens des Entwicklers hinzugefügt. Es wird keine Fehler mehr geben, weil ein Entwickler vergaß, neue Konfigurationselemente für neue Bildschirme hinzuzufügen.

Abbildung 6 Neue Konventionen für StructureMap

/// <summary>
/// This code would be in the same assembly as 
/// the controller classes and would be executed
/// in the Application_Start() method of your
/// Web application
/// </summary>
public static class SampleBootstrapper {
  public static void BootstrapContainer() {
    ObjectFactory.Initialize(x => {
      // Directs StructureMap to perform auto registration
      // on all the Types in this assembly
      // with the ControllerConvention
      x.Scan(scanner => {
        scanner.TheCallingAssembly();
        scanner.With<ControllerConvention>();
        scanner.WithDefaultConventions();
      });
    });
  }
}

Am Rande möchte ich erklären, dass diese Strategie der automatischen Registrierung in allen IOC-Containern möglich ist, die mir für .NET Framework bekannt sind, solange der IOC-Container eine programmgesteuerte Registrierungs-API verfügbar macht.

Nächste Schritte

Letztendlich geht es nur um das Verringern des Abstands und der Diskrepanz zwischen Ihrer Absicht und dem Code, mit dem diese Absicht umgesetzt wird. Viele der von mir in diesem Artikel gezeigten Verfahren beziehen sich wirklich darauf, den Code es „einfach herausfinden“ zu lassen, und zwar durch Verwenden von Benennungskonventionen statt expliziten Codes oder durch Möglichkeiten zur Vermeidung von Informationsduplizierung im System. Ich zeigte auch einige reflektierende Verfahren zum Wiederverwenden von Informationen, die in Attributen verborgen sind, damit der mechanische Aufwand verringert werden kann.

Alle diese Entwurfsideen können das sich wiederholende Zeremoniell bei der Entwicklung verringern, aber sie haben einen Preis. Kritiker von „Konvention vor Konfiguration“ beschweren sich über die „magische“ Seite dieser Vorgehensweise. Das Einbetten von Ansätzen in Ihren Code oder Ihr Framework wird von der potentiellen Wiederverwendung in neuen Szenarios, wo diese Ansätze nicht so günstig sind, abhalten.

Es gab viele andere Themen, die ich dieses Mal nicht besprechen konnte und auf die ich möglicherweise in einem späteren Artikel zurückkommen werde. Ich möchte definitiv untersuchen, wie sprachorientierte Programmierung, alternative Sprachen wie F#, IronRuby und IronPython sowie interne domänenspezifische Sprachverwendung den Softwareentwurfsprozess beeinflussen.

Senden Sie Fragen und Kommentare (in englischer Sprache) an mmpatt@microsoft.com.

Jeremy Miller ist ein Microsoft MVP für C# und ist der Entwickler des Open-Source-Tools StructureMap für Abhängigkeitsinjektion mit .NET und des bevorstehenden Tools StoryTeller für FitNesse-Tests in .NET. Besuchen Sie seinen Blog The Shade Tree Developer, der Teil der CodeBetter-Website ist.