Mai 2019

Band 34, Nummer 5

[Programmiererpraxis]

Naked-Programmierung: Naked-Sammlungen

Von Ted Neward| Mai 2019

Ted NewardWillkommen zurück, Freunde des NOF. Im letzten Artikel habe ich den Speaker-Domänentyp um eine Reihe von Eigenschaften erweitert, zusammen mit einer Reihe von Anmerkungen und Konventionen zu diesen Eigenschaften, die Hinweise (oder, um ehrlich zu sein, Anweisungen) für die Benutzeroberfläche enthalten, wie diese Eigenschaften überprüft oder dem Benutzer präsentiert werden können. Eine Sache, die ich nicht erläutert habe, ist jedoch, wie ein bestimmtes Domänenobjekt Verweise auf mehrere Objekte haben kann. Beispielsweise haben Referenten oft mehrere Vorträge, die sie halten können, oder sie können sich kompetent zu einem Thema oder mehreren Themen äußern. NOF bezeichnet diese als „Sammlungen“, und es gibt einige Regeln für ihre Funktionsweise, die sich etwas von den vorherigen Erläuterungen unterscheiden.

Sehen wir uns also an, wie wir den Referenten einige Vorträge und Themen zuweisen können.

Konzepte von Naked

In Anlehnung an Dante sollte zunächst Folgendes gesagt werden: Lasst, die Ihr hier eintretet, alle Arrays fahren! NOF verwendet keine Arrays für Sammlungseigenschaften, sondern verlässt sich ausschließlich auf Sammlungsobjekte (IEnumerable<T>-derived), um Null-zu-Viele-Beziehungen eines anderen Typs zu speichern. Der NOF-Leitfaden empfiehlt dringend, diese Sammlungen stark zu typisieren (unter Verwendung von Generics). NOF erlaubt keine mehrfachen Zuordnungen von Werttypen (wie Zeichenfolgen, aufgezählte Typen usw.), da NOF der Ansicht ist, dass es sich um einen vollwertigen Domänentyp handeln sollte, wenn der Typ „wichtig“ genug ist, um die Zuordnung zu gewährleisten.

Wenn ich also zum Beispiel den Begriff eines Themas (etwa „C#“, „Java“ oder „Distributed Systems“) im Konferenzsystem erfassen möchte, besteht NOF darauf, dass „Topic“ ein vollständiger Domänenobjekttyp (d.h. eine öffentliche Klasse mit Eigenschaften) ist, komplett mit eigenen Domänenregeln, wo andere Programmieransätze es Ihnen ggf. ermöglichen, eine einfache „Liste von Zeichenfolgen“ als Eigenschaftstyp zu verwenden. Es ist jedoch sinnvoll, dass die Liste der Themen ein fester Satz ist, also werde ich das Seeding der Datenbank mit dem vollständigen Satz von Themen durchführen, die meine Konferenz berücksichtigen möchte.

Auch wenn ein Vortrag (Talk) nur ein Titel sein könnte, ist er in Wirklichkeit eine Reihe von Dingen: ein Titel, eine Beschreibung, ein Thema (zu dem er gehört oder auf das er verweist). Und er wird von einem (oder mehreren) Referenten (Speakers) gehalten. Offensichtlich habe ich bereits den Ansatz einer Domänenmodellierung im Sinn.

Naked-Sammlungen

In vielerlei Hinsicht ist der einfachste Weg, mit den Domänenklassen für Vortrag (Talk) und Thema (Topic) selbst zu beginnen, ohne Verbindungen zwischen ihnen oder Referenten (Speakers). Inzwischen sollte vieles von dem, was ich hier für jede dieser Klassen schreibe, ziemlich trivial und unkompliziert sein, wie Abbildung 1 zeigt.

Abbildung 1: Domänenklassen für „Talk“ und „Topic“

public class Talk
  {
    [NakedObjectsIgnore]
    public virtual int Id { get; set; }
    [Title]
    [StringLength(100, MinimumLength = 1,
       ErrorMessage = "Talks must have an abstract")]
    public virtual string Title { get; set; }
    [StringLength(400, MinimumLength = 1,
       ErrorMessage = "Talks must have an abstract")]
    public virtual string Abstract { get; set; }
  }
  public class TalkRepository
  {
    public IDomainObjectContainer Container { set; protected get; }
    public IQueryable<Talk> AllTopics()
    {
      return Container.Instances<Talk>();
    }
  }
  [Bounded]
  public class Topic
  {
    [NakedObjectsIgnore]
    public virtual int Id { get; set; }
    [Title]
    [StringLength(100, MinimumLength = 1,
       ErrorMessage = "Topics must have a name")]
    public virtual string Name { get; set; }
    [StringLength(400, MinimumLength = 1,
       ErrorMessage = "Topics must have a description")]
    public virtual string Description { get; set; }
  }
  public class TopicRepository
  {
    public IDomainObjectContainer Container { set; protected get; }
    public IQueryable<Topic> AllTopics()
    {
      return Container.Instances<Topic>();
    }
  }

Bisher ist das ziemlich einfach. (Es gibt offensichtlich andere Dinge, die zu jeder dieser beiden Klassen hinzugefügt werden könnten und/oder sollten, aber dies bringt es recht gut auf den Punkt.) Das einzige neue verwendete Attribut, [Bounded], ist ein Hinweis an NOF, dass die vollständige (und unveränderliche) Liste der Instanzen auf dem Client im Arbeitsspeicher gespeichert sein darf und auch sollte und dem Benutzer als Dropdownliste mit Auswahlmöglichkeiten angezeigt wird. Dementsprechend muss dann die vollständige Liste der Themen in der Datenbank eingerichtet werden. Am einfachsten geschieht dies in der Klasse DbInitializer aus dem SeedData-Projekt in der Seed-Methode (wie in vorherigen Kolumnen dieser Serie erläutert). Abbildung 2 zeigt dies.

Abbildung 2: Erstellen einer Liste mit Themen

protected override void Seed(ConferenceDbContext context)
{
  this.Context = context;
  Context.Topics.Add(new Topic() { Name = "C#",
    Description = "A classical O-O language on the CLR" });
  Context.Topics.Add(new Topic() { Name = "VB",
    Description = "A classical O-O language on the CLR" });
  Context.Topics.Add(new Topic() { Name = "F#",
    Description = "An O-O/functional hybrid language on the CLR" });
  Context.Topics.Add(new Topic() { Name = "ECMAScript",
    Description = "A dynamic language for browsers and servers" });
  Context.SaveChanges();
  // ...
}

Dies stellt eine (eher kleine, aber nützliche) Liste von Themen bereit, die als Arbeitsgrundlage dienen. Übrigens: Wenn Sie dies als Heimspiel spielen und den Code von Hand schreiben, denken Sie daran, das TalkRepository zum Hauptmenü hinzuzufügen, indem Sie es der MainMenus-Methode in der Datei „NakedObjectsRunSettings.cs“ im Server-Projekt hinzufügen. Stellen Sie außerdem sicher, dass die beiden Repositorytypen auch in der Services-Methode in derselben Datei aufgeführt werden.

Grundsätzlich wird ein Vortrag (Talk) von einem Referenten (Speaker) gehalten und bezieht sich auf ein bestimmtes Thema (Topic). Ich werde das kompliziertere Szenario ignorieren, wenn ein Vortrag von zwei Referenten gehalten wird oder mehrere Themen umfasst, um das Beispiel vorerst einfach zu halten. Fügen wir also als ersten Schritt dem Referenten Vorträge hinzu:

private ICollection<Talk> _talks = new List<Talk>();
public virtual ICollection<Talk> Talks
{
  get { return _talks; }
  set { _talks = value; }
}

Wenn Sie das Projekt erstellen und ausführen, wird „Talks“ als Sammlung (Tabelle) in der Benutzeroberfläche angezeigt, ist aber leer. Ich könnte natürlich einige Vorträge in SeedData hinzufügen, aber im Allgemeinen müssen die Referenten in der Lage sein, ihren Profilen Vorträge hinzuzufügen.

Naked-Aktionen

Dies kann durch Hinzufügen einer Aktion zur Speaker-Klasse erfolgen: Eine Methode, die „wie von Zauberhand“ als auswählbares Element im Menü „Actions“ (Aktionen) erscheint, wenn sich ein Speaker-Objekt in der Anzeige befindet. Wie Eigenschaften werden auch Aktionen über die Magie der Reflektion ermittelt, sodass nur eine öffentliche Methode für die Speaker-Klasse erstellt werden muss:

public class Speaker
{
  // ...
  public void SayHello()
  {
  }
}

Wenn das Projekt jetzt erstellt und ausgeführt wird, wird nach der Anzeige eines Referenten das Menü „Actions“ mit der darin enthaltenen Angabe „SayHello“ angezeigt. Zurzeit geschieht nichts. Es wäre schön, als Ausgangspunkt zumindest eine Meldung an den Benutzer zurückzugeben. Im NOF-Universum geschieht dies durch die Nutzung eines Diensts: eines Objekts, dessen Zweck es ist, einige zusätzliche Funktionen bereitzustellen, die nicht zu einem bestimmten Domänenobjekt gehören. Im allgemeinen Fall „Meldungen zurück an den Benutzer geben“ wird diese Möglichkeit durch einen generischen Dienst bereitgestellt, der von NOF selbst in der IDomainObjectContainer-Schnittstelle definiert wird. Ich brauche eine jener Instanzen, um Vorgänge ausführen zu können, aber NOF verwendet Abhängigkeitsinjektion, um bei Bedarf eine Instanz bereitzustellen: Deklarieren Sie eine Eigenschaft vom Typ IDomainObjectContainer für die Speaker-Klasse, und NOF stellt sicher, dass jede Instanz über eine solche verfügt:

public class Speaker
{
  public TalkRepository TalkRepository { set; protected get; }
  public IDomainObjectContainer Container { set; protected get; }

Das Container-Objekt hat eine InformUser-Methode, mit der allgemeine Meldungen an den Benutzer zurückgegeben werden, sodass die Verwendung aus der SayHello-Aktion ganz einfach ist:

public class Speaker
{
  // ...
  public void SayHello()
  {
    Container.InformUser("Hello!");
  }
}

Aber am Anfang stand der Wunsch, dem Benutzer zu erlauben, dem Repertoire eines bestimmten Referenten einen Vortrag hinzuzufügen. Insbesondere muss ich den Titel des Vortrags (Talk), die Zusammenfassung (englisch „Abstract“) oder vielmehr Beschreibung, da „abstract“ ein reserviertes Wort in C# ist, und das Thema (Talk) erfassen, zu dem dieser Vortrag gehört. Wenn ich diese Methode „EnterNewTalk“ nenne, verfüge ich über die folgende Implementierung:

public void EnterNewTalk(string title, string description, Topic topic)
{
  var talk = Container.NewTransientInstance<Talk>();
  talk.Title = title;
  talk.Abstract = description;
  talk.Speaker = this;
  Container.Persist<Talk>(ref talk);
  _talks.Add(talk);
}

Hier passieren mehrere Dinge, die ich erläutern möchte. Zunächst verwende ich IDomainObjectContainer, um eine kurzlebige (nicht persistiert gespeicherte) Instanz von „Talk“ zu erstellen. Dies ist notwendig, da NOF in der Lage sein muss, „Hooks“ in jedes Domänenobjekt zu injizieren, um seine Magie zu entfalten. (Deshalb müssen alle Eigenschaften virtuell sein, damit NOF z.B. die Synchronisierung zwischen der Benutzeroberfläche und Objekten verwalten kann.) Dann werden die Eigenschaften des Vortrags festgelegt, und der Container wird erneut verwendet, um den Vortrag persistent zu speichern. Wenn dies nicht geschieht, ist der Vortrag kein persistent gespeichertes Objekt und wird nicht gespeichert, wenn ich den Vortrag der Liste der Vorträge des Referenten hinzufüge.

Es ist jedoch eine berechtigte Frage, wie der Benutzer diese Informationen an die EnterNewTalk-Methode selbst weitergegeben hat. Auch hier sind die Wunder von Reflektion am Werk: NOF hat die Parameternamen und Typen aus den Methodenparametern entnommen und ein Dialogfeld erstellt, um diese Elemente zu erfassen, einschließlich des Themas selbst. Erinnern Sie sich daran, dass „Topic“ mit der Anmerkung „Bounded“ versehen wurde? Dadurch wurde NOF angewiesen, die Liste der Themen in diesem Dialogfeld als Dropdownliste zu erstellen, wodurch es unglaublich einfach wird, ein Thema aus der Liste auszuwählen. (Es sollte an diesem Punkt leicht zu erkennen sein: Wenn ich dem System Themen hinzufüge, werden sie alle ohne weiteren Aufwand in diese Dropdownliste aufgenommen.)

Es wäre sinnvoll, wenn das Erstellen von Vorträgen von TalkRepository unterstützt würde und nicht für die Speaker-Klasse selbst. Wie Sie in Abbildung 3 sehen können, ist dies ein einfaches Refactoring.

Abbildung 3: Unterstützen der Vortragserstellung mit TalkRepository

public class Speaker
  {
    public TalkRepository TalkRepository { set; protected get; }
    public IDomainObjectContainer Container { set; protected get; }
    public void EnterNewTalk(string title, string description, Topic topic)
    {
      var talk = TalkRepository.CreateTalk(this, title, description, topic);
      _talks.Add(talk);
    }
  }
  public class TalkRepository
  {
    public IDomainObjectContainer Container { set; protected get; }
    public Talk CreateTalk(Speaker speaker, string title, string description,
                Topic topic)
    {
      var talk = Container.NewTransientInstance<Talk>();
      talk.Title = title;
      talk.Abstract = description;
      talk.Speaker = speaker;
      Container.Persist<Talk>(ref talk);
      return talk;
    }
  }

Darüber hinaus wird das Menü „Talks“ dadurch automatisch um einen neuen Menüpunkt „CreateTalk“ ergänzt, der (wiederum durch die Magie von Reflektion) automatisch ein Dialogfeld zur Eingabe der für die Erstellung eines Vortrags erforderlichen Daten erstellt. Referenten können natürlich nicht nur eingegeben werden, sodass NOF dieses Feld zu einem „dropfähigen“ Feld macht, was bedeutet, dass NOF erwartet, dass ein Speaker-Objekt mithilfe von Drag & Drop in diesem Feld platziert wird. (Um dies in der NOF Gemini-Standardschnittstelle in Aktion zu erleben, starten Sie die App, wählen Sie einen Referenten aus, und klicken Sie dann auf die Schaltfläche „Swap Pane“ (Bereich tauschen): die Schaltfläche mit zwei Pfeilen am unteren Rand der Anzeige. Der ausgewählte Referent wird auf die rechte Seite des Bildschirms verschoben, und die Startseite wird eingeblendet, sodass Sie das Element „Talks/Create Talks“ (Vorträge/ Vortrag erstellen) auswählen können. Ziehen Sie den Namen des Referenten in das Feld „Speaker“ im Dialogfeld „Create Talk“, und schon ist der Referent ausgewählt.)

Es ist von entscheidender Bedeutung zu verstehen, was hier passiert: Ich habe jetzt zwei verschiedene Benutzeroberflächenpfade (Erstellen eines Vortrags aus dem Hauptmenü oder Erstellen eines Vortrags aus einem bestimmten Referenten), die zwei verschiedene Benutzernavigationsszenarien mit geringem Aufwand und ohne doppelten Code ermöglichen. TalkRepository kümmert sich um alle Dinge, die mit „CRUD-Vorgängen“ und „Talk“ zusammenhängen, und „Speaker“ verwendet diesen Code. Dabei bleiben die Benutzerinteraktion vollständig innerhalb von Speaker, wenn der Benutzer dies so wünscht.

Zusammenfassung

Dies ist nicht das UI-Toolkit Ihres Großvaters. In nur wenigen Codezeilen habe ich eine funktionsfähige Schnittstelle (und wenn man bedenkt, dass ein SQL-Standard-Back-End zum Speichern und Abrufen von Daten verwendet wird, ein Datenspeichersystem) erstellt, die zumindest im Moment direkt von Benutzern verwendet werden kann. Noch wichtiger ist, dass nichts davon in einem proprietären Format oder einer proprietären Sprache vorliegt: das ist reines C#, reiner SQL Server-Code, und die Benutzeroberfläche selbst ist Angular. Es gibt noch einige weitere erwähnenswerte Dinge zur Standardoberfläche von NOF, auf die ich in der nächsten Kolumne eingehen werde, z.B. Authentifizierungs- und Autorisierungsfunktionen. Bis dahin: Viel Spaß beim Programmieren!


Ted Neward ist ein in Seattle ansässiger Berater, Referent und Mentor für zahlreiche Technologien. Er hat unzählige Artikel geschrieben und als Autor und Mitautor ein Dutzend Bücher verfasst. Er hält weltweit Vorträge. Sie erreichen ihn unter ted@tedneward.com, oder lesen Sie seinen Blog unter blogs.tedneward.com.

Unser Dank gilt dem folgenden technischen Experten für die Durchsicht dieses Artikels: Richard Pawson


Diesen Artikel im MSDN Magazine-Forum diskutieren