Eine Eigenschaft, die sowohl ein einzelnes Datum als auch einen Datumsbereich darstellen kann: Wie kann man das richtig modellieren?

8

Ich arbeite in einem System, das auf zwei Arten eine "Versandschätzung" darstellen kann:

  1. Ein bestimmtes Datum: Der Artikel wird garantiert zu diesem Datum versendet
  2. Ein Tagesintervall: Der Artikel wird ab heute "X bis Y" Tage versendet

Die Informationen zum Modell sind semantisch identisch, es handelt sich um "die Versandschätzung". Wenn ich die Informationen über den Versandvoranschlag vom System erhalte, kann ich feststellen, ob der Kostenvoranschlag vom ersten oder vom zweiten Formular ist.

Das aktuelle Modell hierfür ähnelt dem folgenden:

class EstimattedShippingDateDetails
{
    DateTime? EstimattedShippingDate {get; set;}
    Range? EstimattedShippingDayRange {get; set;}
}

Range ist eine einfache Klasse, um einen "Anfang -> Ende" von ganzen Zahlen zu umbrechen, ähnlich wie folgt:

struct Range
{
    int Start {get; set}
    int End {get; set}

    public override ToString()
    {
        return String.Format("{0} - {1}", Start, End);
    }
}

Dieser Ansatz gefällt mir nicht, da nur eine der Eigenschaften des Schätzmodells jemals ausgefüllt wird und ich bei einer von ihnen auf Null testen und davon ausgehen muss, dass die andere über die Daten verfügt.

Jede der Eigenschaften wird für den Benutzer anders angezeigt, jedoch an derselben Stelle auf der Benutzeroberfläche. Dabei wird eine benutzerdefinierte MVC DisplayTemplate verwendet, in der sich die aktuelle Schaltlogik befindet:

@Model EstimattedShippingDateDetails

@if (Model.EstimattedShippingDate.HasValue)
{
    Html.DisplayFor(m => Model.EstimattedShippingDate)
}
else
{
    Html.DisplayFor(m => Model.EstimattedShippingDayRange)
}

Wie könnte ich dies modellieren, um es repräsentativer für die tatsächliche Anforderung zu machen und gleichzeitig die Anzeigelogik in einer MVC-Anwendung einfach zu halten?

Ich habe darüber nachgedacht, eine Schnittstelle und zwei Implementierungen zu verwenden, eine für jeden "Schätztyp", aber ich kann mich nicht um eine gemeinsame Schnittstelle für beide kümmern. Wenn ich eine Schnittstelle ohne Mitglieder erstelle, kann ich nicht einheitlich auf die Daten zugreifen, und es ist meiner Meinung nach ein schlechtes Design. Ich wollte auch das Ansichtsmodell so einfach wie möglich halten. Mit diesem Ansatz würde ich jedoch einen "durch Konstruktion korrekten" Code erhalten, da es keine Nullables mehr geben müsste: Jede Implementierung hätte eine nicht nullable Eigenschaft, entweder a DateTimeoder a Range.

Ich habe auch in Betracht gezogen, nur eine einzige zu verwenden, Rangeund wenn Situation Nr. 1 eintritt, verwenden Sie einfach dieselbe DateTimefür beide Startund End, aber dies würde zu Komplikationen bei der Ausgabe der Werte an die Benutzeroberfläche führen, da ich dann feststellen müsste, ob es sich um eine statische Aufladung oder ein Intervall von handelt Vergleichen Sie die Werte und formatieren Sie den Bereich richtig, um ihn entweder als einzelnes Datum oder als formatiertes Intervall anzuzeigen.

Es scheint, dass ich ein Konzept brauche, das den Gewerkschaften von Typescript ähnelt: im Grunde eine Eigenschaft, die zwei Arten haben kann. Natürlich gibt es so etwas in C # nicht nativ (nur dynamicin der Nähe davon).

julealgon
quelle
Dies ist eher ein konzeptionelles "Wie kann ich ...?" Frage als eine Bitte um offene Kritik. Migrieren dieser Frage zu Programmierern.
200_erfolg
@ 200_success Entschuldigung. Ich dachte, dies wäre für die Codereview angemessen, aber Sie haben Recht, es passt hier besser. Danke für die Hilfe;)
Julealgon
Ist die Formatierung in Stein gemeißelt, oder können Sie beispielsweise das Intervall "Wird zwischen $ date1 und $ date2 versendet" formatieren?
Svick
@svick Leider ist es vorerst in Stein gemeißelt.
Julealgon

Antworten:

17

Verwenden Sie für alle Versandschätzungen einen Datumsbereich (dh zwei Daten).

Machen Sie für ein einzelnes Datum X und Y gleich.

Robert Harvey
quelle
Ich habe dies in meiner eigenen Frage erwähnt. Können Sie die Nachteile erläutern, die ich bei der Verwendung dieses Ansatzes angesprochen habe? Beachten Sie, dass das Intervallszenario ein festes Tagesintervall und kein vollständiges Datumsintervall ist.
Julealgon
2
Wenn Sie beide Felder immer ausfüllen, sollten Sie Ihre Einwände gegen Nullwerte beseitigen. Was meinst du mit "festes Tagesintervall und kein ganzes Tagesintervall"?
Robert Harvey
Die Daten, die ich vom Server erhalte, sind entweder ein DateTimeObjekt, das das erwartete Versanddatum darstellt, oder zwei ganzzahlige Werte mit den minimalen und maximalen Tagen ab heute, an denen der Artikel versendet wird. Wenn ich Datumsangaben standardisiere, muss ich diese Ganzzahlen in ordnungsgemäß erstellte DateTimes konvertieren, was die Komplexität erheblich erhöhen würde, da alles berücksichtigt werden muss, wie Client- / Server-Datumsangaben, UTC und Sommerzeit Unterschiede usw. Ich möchte an dieser Stelle vermeiden, diese Art von Komplexität zu erzeugen.
Julealgon
Guter Punkt für die Möglichkeit, niemals Nullwerte zu haben, das wäre ziemlich nett.
Julealgon
Was ist falsch daran, wie Sie es jetzt tun, indem Sie Ihren Schalter in der MVC-Vorlage verwenden?
Robert Harvey
2

Sie können dies mithilfe der Kapselung modellieren.

Lassen Sie die Klasse 2 Konstruktoren haben, einen für das einzelne Datum und einen für den Datumsbereich.

Bestimmen Sie in der ToString () -Methode den 'Status' der Klasse und generieren Sie die entsprechende formatierte Zeichenfolge.

Fügen Sie andere Methoden hinzu, die Ihren anderen Anforderungen entsprechen.

hocho
quelle
Das würde auf einem einfacheren System gut funktionieren, aber beachten Sie, dass ich dies mithilfe von MVC-Ansichten ordnungsgemäß anzeigen muss und dass der RangeTyp an anderen Stellen im System verwendet und in diesem Fall auf die gleiche Weise angezeigt werden kann. Aus diesem Grund muss ich die RangeAnzeige in einer DisplayTemplate zentralisieren, die wiederverwendet werden kann. Wenn ich die ToStringMethode verwende, um das zu kapseln, verliere ich viel Flexibilität, die Ansichten bieten.
Julealgon
Wie schlagen Sie vor, dass ich den "Zustand" des Objekts kenne? Eine Art lokale Aufzählungsvariable? Ein Boolescher? Ich nehme an, es würde auf unterschiedliche Werte gesetzt, je nachdem welcher Konstruktor richtig aufgerufen wurde. Damit dies funktioniert, müsste ich wahrscheinlich auch die Klasse unveränderlich machen, oder ich könnte Probleme bekommen, wenn jemand den anderen Wert usw. festlegt. Was mache ich, wenn man den Konstruktor für ein einzelnes Datum verwendet und versucht, auf den Bereich zuzugreifen Eigentum auch? Würde ich in diesem Fall eine Ausnahme auslösen oder einfach null zurückgeben? Sie sehen, die Benutzerfreundlichkeit ist immer noch nicht ideal.
Julealgon
Ja, im Wesentlichen wäre die Klasse unveränderlich, wenn die beiden Daten schreibgeschützt und in den Konstruktoren zugewiesen würden. Der Zustand könnte durch einen Bool festgestellt werden und / oder dasselbe Datum für die Darstellung des einzelnen Datums haben. Ihre anderen Mitgliedsfunktionen könnten den Status angemessen verwenden und Sie könnten den Status bei Bedarf zusätzlich verfügbar machen. Entschuldigung kann nicht genauer sein, da ich nicht alle Funktionen kenne, die Sie unterstützen müssen.
hocho
Was @hocho sagt, ist, dass alle Liefertermine als Bereiche modelliert werden können. Es ist nur so, dass einige Bereiche das gleiche Start- und Enddatum haben. Das scheint ziemlich vernünftig. Client-Code, der Bereiche verwendet, sollte darauf vorbereitet sein, mit einem Start- und Enddatum desselben Datums umzugehen, und in diesem Fall möglicherweise unterschiedliche Informationen (dh ein einzelnes Datum gegenüber dem Datumsbereich) in die E-Mail drucken.
Erik Eidt
Abstimmung nach unten für ..ToString () [um] die zu bestimmenstate . ToString()sollte nur den Zustand "melden". Der Status wird in Konstruktoren, Eigenschaftssetzern usw. bestimmt. Und ich sehe einfach nichts im OP, was darauf hindeutet, dass es einen "zusammenfassenden Zustand" gibt oder geben sollte
Radarbob
1

Dies ist ein ziemlich häufiges Problem. Nullables lösen beispielsweise das alltägliche Problem von endDates für Dinge, die noch nicht beendet sind.

Aber ich denke, Sie haben ein schlechtes Beispiel gewählt.

Mit Ihrem genauen Fall von zwei Daten scheint ein Datumsbereich, der morgens beginnt und abends endet, die perfekte Lösung zu sein. Oder vielleicht ein Startdatum und eine ganzzahlige Anzahl zusätzlicher Tage?

Betrachten wir jedoch einen schwierigeren Fall. Ich habe zwei Arten der Lieferung, Post und Abholung vom Geschäft. Diese sind viel unterschiedlicher, der Shop benötigt den Namen und die Adresse, die Post hat Kosten, möglicherweise eine Reihe von Lieferoptionen, Tracking-Codes usw.

Der Standardansatz besteht darin, nach den gemeinsamen Dingen zu suchen, die diese beiden "Lieferoptionen" ausmachen, und diese in eine Basisklasse einzuteilen. Die Unterklasse der beiden spezifischen Fälle mit den zusätzlichen Details, die sie haben / benötigen.

In fast allen Fällen haben Sie mindestens eine ID, einen Typ und eine Beschreibung, die beiden Typen gemeinsam sind. Damit:

public class DeliveryOption 
{
     Public string Id;
     Public typeEnum Type;
     public string Description;
}


Public class Collection : DeliveryOption
{
     Public string ShopName;
}

Public class Post : DeliveryOption
{
     Public DateTime EstDelivery;
}
Ewan
quelle
... ein Datumsbereich, der morgens beginnt und abends endet, scheint die perfekte Lösung zu sein. Nutzen Sie einfach die DataTime.DateUnterkunft und sorgen Sie sich überhaupt nicht um die Zeit.
Radarbob
1

"Start" und "Ende" sind nicht DateTime, sondern Offsets

Wie könnte ich dies modellieren, um es repräsentativer für die tatsächliche Anforderung zu machen und gleichzeitig die Anzeigelogik in einer MVC-Anwendung einfach zu halten?

"Start" und "Ende" sind Offsets zum EstimatedShipDate. Sie sind nicht sie DateTimeselbst . Dies beschreibt besser, was passiert, und reduziert die Komplexität erheblich.

Keine Notwendigkeit für eine interface. Keine Notwendigkeit für eine RangeKlasse. Machen Sie keine Komplexität, bis Sie sicher sind, dass Sie sie brauchen. Ich vermute sehr, dass eine einzelne Klasse mit einem Konstruktor mit 3 optionalen Parametern die Dinge viel einfacher macht.


Die Daten, die ich vom Server erhalte, sind entweder ein DateTime-Objekt, das das erwartete Versanddatum darstellt, oder zwei ganzzahlige Werte mit den minimalen und maximalen Tagen ab heute, an denen der Artikel versendet wird.

Verwenden Sie einen einzelnen Konstruktor , der alle 3 Werte über optionale Parameter übergibt. Dies liefert den gesamten benötigten Kontext. Die Einzelkonstruktorlogik kann dann alle Variationen auswerten, um den Anfangszustand korrekt einzustellen. Verwenden Sie bei Bedarf auch benannte Parameter im Konstruktoraufruf, um dies kristallklar zu machen.

public class ShipDate {
    public ShipDate (Datetime? shipDate = null, int earliestOffset = 0, latestOffset = 0) {
        EstShipDate = (DateTime) shipDate ?? DateTime.Now.Date;
        start = earliestOffset < 0 ? 0 : earliestOffset;
        end   = latestOffset < 0 ? 0 : latestOffset;
        // That's all, folks!
    }
}

Wenn ich Datumsangaben standardisiere, muss ich diese Ganzzahlen in ordnungsgemäß erstellte DateTimes konvertieren.

Tun Sie das nicht und die Dinge sind einfacher; Verwenden Sie stattdessen DateTime.AddDays () `.

public DateTime EstShipDate      {get; protected set;}
public DateTime EarliestShipDate { get { return EstShipDate.AddDays(start).Date; } }
public DateTime LatestShipDate   { get { return EstShipDate.AddDays(end).Date; } }

Dies ist möglicherweise flexibler, wenn sich Datums- und Versatzwerte ändern dürfen.


Dies würde die Komplexität erheblich erhöhen, da alles berücksichtigt werden müsste, wie Client- / Server-Daten, UTC, Sommerzeitunterschiede usw. Ich möchte an dieser Stelle vermeiden, diese Art von Komplexität zu erzeugen.

Lesen Sie hier mehr über den Umgang mit Zeitzonen.

DateTime.DateTimeKindFügen Sie vorerst nur einen (enum) Konstruktorparameter hinzu und behandeln Sie ihn später.


Aus einer der Antworten:

Mit Ihrem genauen Fall von zwei Daten scheint ein Datumsbereich, der morgens beginnt und abends endet, die perfekte Lösung zu sein. Oder vielleicht ein Startdatum und eine ganzzahlige Anzahl zusätzlicher Tage?

Verwenden Sie die DateTime.DateEigenschaft und ignorieren Sie die Zeit vollständig.

Radarbob
quelle
0

Was ist mit so etwas

class EstimattedShippingDateDetails
{
    DateTime EarliestShippingDate {get; set;}
    DateTime LatestShippingDate {get; set;}
    TimeSpan Range 
    {
        get 
        { 
            return LatestShippingDate - EarliestShippingDate; 
        } 
    }
}

Ein bestimmtes Datum: Der Artikel wird garantiert zu diesem Datum versendet

Beide EarliestShippingDateund LatestShippingDatesind auf das garantierte Versanddatum eingestellt.

Ein Tagesintervall: Der Artikel wird ab heute "X bis Y" Tage versendet

EarliestShippingDateist auf heute eingestellt und LatestShippingDateist auf eingestellttoday + (Y - X)

Gepunktet
quelle
0

Sie müssen entscheiden, was der Unterschied (falls vorhanden) zwischen einem einzelnen Versanddatum und einem Bereich ist, der aus einem Tag besteht. Wenn ein "einzelnes Versanddatum" nur eine Reihe von Tagen ist, die zufällig aus einem Tag bestehen, modellieren Sie alles als Start- und Enddatum. Wenn ein "einzelnes Versanddatum" und eine Reihe von Tagen mit demselben Start- und Enddatum unterschiedlich behandelt werden sollen, speichern Sie einen der beiden Fälle, entweder ein einzelnes Datum für ein einzelnes Versanddatum und zwei Daten für eine Reihe von Tagen .

gnasher729
quelle
0

Sie haben grundsätzlich 2 Möglichkeiten:

  • 2 Termine. Wenn das Objekt ein einzelnes Datum darstellt, setzen Sie entweder beide auf denselben Wert oder das zweite auf einen Nullwert.

  • 1 Datum und eine Zeitspanne. Das Datum stellt den Start dar und die Zeitspanne zeigt, wie viel in der Zukunft der Bereich sein kann. Setzen Sie den Bereich für ein einzelnes Datum auf 0.

Für den Versand hätte ich eher ein Datum als ein Datum, da Sie ohne Zweifel die Lieferung am Morgen / Abend modellieren möchten. Welche der beiden Optionen am besten geeignet ist, hängt davon ab, wie Sie die Anzeige berechnen möchten. Wenn Sie "zwischen x und y" anzeigen, ist die erste Option möglicherweise einfacher zu verwenden. Wenn Sie "bis zu x Tage von y" anzeigen, ist letztere einfacher zu verwenden.

Wenn Sie Nullwerte nicht mögen, ist die letztere Option besser, da die Berechnung des ursprünglichen Datums plus der Zeitspanne unabhängig davon erfolgen kann, ob die Zeitspanne einen Wert hat oder auf 0 gesetzt ist. Sie erhalten immer ein korrektes Ergebnis, ohne nach Null zu suchen.

gbjbaanb
quelle