Uma propriedade que pode representar uma única data e um intervalo de datas: como modelar isso adequadamente?

8

Eu trabalho em um sistema que pode representar uma "estimativa de remessa" de duas maneiras:

  1. Uma data específica: é garantido que o item seja enviado nessa data
  2. Um intervalo de dias: o item será enviado "X a Y" dias a partir de hoje

As informações no modelo são semanticamente iguais, é "a estimativa de remessa". Quando obtenho as informações sobre a estimativa de remessa no sistema, posso saber se a estimativa é do primeiro ou do segundo formulário.

O modelo atual para isso é semelhante ao seguinte:

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

Range é uma classe simples para agrupar um "começo -> fim" de números inteiros, mais ou menos assim:

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

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

Não gosto dessa abordagem, porque apenas uma das propriedades no modelo de estimativa será preenchida e preciso testar o valor nulo em uma delas e assumir que a outra possui os dados.

Cada uma das propriedades é mostrada de maneira diferente para o usuário, mas no mesmo local da interface do usuário, usando um MVC DisplayTemplate personalizado, onde a lógica de comutação atual reside:

@Model EstimattedShippingDateDetails

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

Como modelar isso para torná-lo mais representativo do requisito real, mantendo a lógica de exibição simples em um aplicativo MVC?

Pensei em usar uma interface e duas implementações, uma para cada "tipo" de estimativa, mas não consigo entender a interface comum para ambas. Se eu criar uma interface sem membros, não consigo acessar os dados de maneira unificada e é um IMHO de design ruim. Eu também queria manter o viewmodel o mais simples possível. Eu obteria um código "correto por construção" com essa abordagem, já que não seria mais necessário anular: cada implementação teria uma propriedade não anulável, a DateTimeou a Range.

Também considerei usar apenas um Rangee, quando a situação nº 1 acontecer, use o mesmo DateTimepara ambos Starte End, mas isso adicionaria complicações sobre como emitir os valores para a interface do usuário, pois eu precisaria detectar se é um estático ou intervalo por comparando os valores e formate corretamente o intervalo para ser exibido como uma única data ou um intervalo formatado.

Parece que o que eu preciso é de um conceito semelhante aos sindicatos do Typescript: basicamente uma propriedade que pode ser de dois tipos. Naturalmente, não existe tal coisa em C # ( dynamicseria apenas próximo disso).

julealgon
fonte
Este é mais um conceito "Como posso ...?" pergunta do que um pedido de crítica aberta. Migrando esta pergunta para programadores.
200_success
@ 200_success Desculpe por isso. Eu pensei que isso seria apropriado para codereview, mas você está correto, ele se encaixa melhor aqui. Obrigado pela ajuda;)
julealgon
A formatação está definida, ou você poderia, por exemplo, intervalo de formato como "será enviado entre $ date1 e $ date2"?
svick
@svick Infelizmente, está gravado em pedra por enquanto.
julealgon

Respostas:

17

Use um período (ou seja, duas datas) para todas as estimativas de envio.

Para uma única data, faça X e Y iguais.

Robert Harvey
fonte
Mencionei isso na minha própria pergunta. Você pode elaborar os inconvenientes que levantei ao usar essa abordagem? Observe que o cenário de intervalo é um intervalo fixo de dia e também não um intervalo de data completo.
10136 julealgon
2
Preencher os dois campos sempre deve eliminar suas objeções a valores nulos. O que você quer dizer com "intervalo de dia fixo e não um intervalo de dia inteiro?"
Robert Harvey
Os dados que eu recebo do servidor são um DateTimeobjeto, representando a data esperada de envio ou dois valores inteiros com os dias mínimo e máximo de hoje em que o item será enviado. Se eu padronizar as datas, terei que converter esses números inteiros em DateTimes adequadamente construídos, o que aumentaria bastante a complexidade devido a tudo o que seria necessário levar em conta, como datas cliente versus servidor, UTC, horário de verão diferenças etc. Gostaria de evitar criar esse tipo de complexidade neste momento.
julealgon
Bom ponto sobre a possibilidade de nunca ter valores nulos, seria muito bom.
julealgon
O que há de errado com a maneira como você está fazendo isso agora, usando sua opção no modelo MVC?
22416 Robert Harvey
2

Você pode modelar isso usando o encapsulamento.

Deixe a classe ter dois construtores, um para a data única e outro para o período.

No método ToString (), determine o 'estado' da classe e gere a sequência formatada apropriada.

Adicione outros métodos conforme apropriado para suas outras necessidades.

hocho
fonte
Isso funcionaria bem em um sistema mais simples, mas observe que eu preciso exibi-lo adequadamente usando as visualizações MVC e que o Rangetipo pode ser usado em outros locais do sistema e, nesse caso, ser exibido da mesma maneira. É por isso que preciso centralizar a Rangeexibição em um DisplayTemplate que possa ser reutilizado. Se eu usar o ToStringmétodo para encapsular isso, estou perdendo muita flexibilidade que as visualizações fornecem.
julealgon
Além disso, como você propõe que eu conheça o "estado" do objeto? Uma variável de enumeração local? Um booleano? Suponho que seria definido com valores diferentes, dependendo de qual construtor foi chamado, certo? Para fazer esse trabalho, eu provavelmente também precisaria tornar a classe imutável, ou posso ter problemas se alguém definir o outro valor, etc. O que faço se alguém usar o construtor de data única e tentar acessar o intervalo propriedade também? Eu lançaria uma exceção nesse caso ou retornaria nulo? Você vê, a usabilidade disso ainda não é o ideal.
julealgon
Sim, essencialmente a classe seria imutável, com as duas datas sendo somente leitura e atribuídas nos construtores. O estado pode ser determinado por um booleano e / ou com a mesma data para representar a data única. Suas outras funções de membro podem usar o estado adequadamente e você também pode expô-lo, se necessário. Desculpe, não pode ser mais específico, porque não conheço toda a funcionalidade que você precisa oferecer suporte.
Hocho 10/03/16
O que a @hocho está dizendo é que todas as datas de entrega podem ser modeladas como intervalos, apenas alguns intervalos têm as mesmas datas de início e de término. Isso parece bastante sensato. O código do cliente usando intervalos deve estar preparado para lidar com uma data de início e término da mesma data e, nesse caso, talvez imprima informações diferentes (por exemplo, data única versus intervalo de datas) no email.
Erik Eidt
voto negativostate para ..ToString () [to] determinar o . ToString()deve apenas "relatar" o estado. O estado é determinado em construtores, configuradores de propriedades e assim por diante. E, eu não estou vendo nada no OP que sugere que é ou deveria ser um "estado resumo"
radarbob
1

Esse é um problema bastante comum; os Nullables, por exemplo, resolvem o problema comum de endDates para coisas que não terminaram.

Mas acho que você escolheu um mau exemplo.

Com o seu caso exato de duas datas, um período que começa pela manhã e termina à noite parece ser a solução perfeita. Ou talvez uma data de início e um número inteiro de dias extras, pode ser?

No entanto, vamos considerar um caso mais difícil. Tenho dois tipos de entrega, postagem e retirada na loja. Estes são obvs muito mais diferentes, a loja precisa do nome e endereço, o correio tem um custo, pode ter várias opções de entrega, códigos de rastreamento etc.

A abordagem padrão é procurar as coisas comuns que fazem essas duas 'opções de entrega' e colocá-las em uma classe base. A subclasse dos dois casos específicos com os detalhes extras que eles têm / precisam.

Em quase todos os casos, você terá pelo menos um ID, um Tipo e uma Descrição comuns aos dois tipos. Assim:

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
fonte
... um período que começa de manhã e termina à noite parece ser a solução perfeita. Basta usar a DataTime.Datepropriedade e não se preocupe com o tempo.
Radarbob 13/03/19
1

"start" e "end" não são DateTime, são compensações

Como modelar isso para torná-lo mais representativo do requisito real, mantendo a lógica de exibição simples em um aplicativo MVC?

"start" e "end" são deslocamentos para o EstimatedShipDate. Eles não são DateTimeeles mesmos . Isso descreve melhor o que está acontecendo e reduzirá drasticamente a complexidade.

Não há necessidade de um interface. Não há necessidade de uma Rangeaula. Não faça complexidade até ter certeza de que precisa. Eu suspeito fortemente que uma única classe com um construtor de 3 parâmetros opcionais manterá as coisas muito mais simples.


Os dados que recebo do servidor são um objeto DateTime, representando a data esperada de entrega ou dois valores inteiros com os dias mínimo e máximo de hoje em que o item será enviado.

Use um único construtor passando todos os 3 valores por meio de parâmetros opcionais. Isso fornece todo o contexto necessário. A lógica do construtor único pode avaliar todas as variações para definir o estado inicial corretamente. Também use parâmetros nomeados na chamada do construtor, se desejar, para torná-lo claro.

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!
    }
}

Se eu padronizar as datas, terei que converter esses números inteiros em DateTimes adequadamente construídos,

Não. Não faça isso e as coisas são mais simples; em vez disso, use 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; } }

Isso é potencialmente mais flexível se datas e valores de deslocamento puderem mudar.


o que aumentaria um pouco a complexidade devido a tudo o que seria necessário levar em consideração, como datas de cliente versus servidor, UTC, diferenças de horário de verão etc. Gostaria de evitar criar esse tipo de complexidade neste momento.

Leia sobre como lidar com fusos horários aqui.

Por enquanto, inclua um DateTime.DateTimeKindparâmetro do construtor (enum) e lide com ele mais tarde.


De uma das respostas:

Com o seu caso exato de duas datas, um período que começa pela manhã e termina à noite parece ser a solução perfeita. Ou talvez uma data de início e um número inteiro de dias extras, pode ser?

Use a DateTime.Datepropriedade e ignore totalmente o tempo.

radarbob
fonte
0

Que tal algo como

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

Uma data específica: é garantido que o item seja enviado nessa data

Ambos EarliestShippingDatee LatestShippingDateestão definidos para a data de envio garantida.

Um intervalo de dias: o item será enviado "X a Y" dias a partir de hoje

EarliestShippingDateestá definido para hoje e LatestShippingDateestá definido paratoday + (Y - X)

Visto
fonte
0

Você precisa decidir qual é a diferença (se houver) entre uma única data de envio e um intervalo que consiste em um dia. Se uma "única data de envio" for apenas um intervalo de dias que consiste em um dia, modele tudo como data de início e data de término. Se uma "data única de remessa" e um intervalo de dias com as mesmas datas de início e término forem tratados de maneira diferente, armazene um dos dois casos, uma única data para uma única data de remessa e duas datas para um período de dias .

gnasher729
fonte
0

Você tem basicamente 2 opções:

  • 2 datas. Se o objeto representar uma única data, defina-os para o mesmo valor ou defina o segundo como um valor nulo.

  • 1 data e um período de tempo. A data representa o início e o período mostra quanto no futuro o intervalo pode ser. Defina o intervalo como 0 para uma única data.

Para o transporte, eu teria uma data e hora em vez de uma data, pois você certamente desejará modelar a entrega da manhã / noite. Qual das 2 opções é a melhor depende de como você deseja calcular a exibição. Se você estiver exibindo "entre x e y", a primeira opção poderá ser mais fácil de usar; se você estiver exibindo "até x dias a partir de y", a segunda será mais fácil de usar.

Se você não gostar de valores nulos, a última opção é melhor, pois o cálculo da data original e do período pode ser feito independentemente de o período ter um valor ou estar definido como 0. Você sempre obterá um resultado correto sem verificar nulo.

gbjbaanb
fonte