Maio de 2019

Volume 34 – Número 5

[O programador]

Codificação Naked: Coleções Naked

Por Ted Neward | Maio de 2019

Ted NewardBem-vindos de volta, NOFers. Na última vez, eu falei do tipo de domínio Speaker (palestrante) com algumas propriedades, bem como algumas anotações e convenções sobre tais propriedades que fornecem dicas (ou, para ser mais honesto, orientações) para a interface do usuário sobre como validar ou apresentar essas propriedades para o usuário. Porém, algo que não discuti é como um determinado objeto de domínio pode fazer referência a mais de um item. Por exemplo, os palestrantes normalmente têm várias palestras que podem fazer ou podem ser peritos em um ou mais tópicos. O NOF refere-se a eles como “coleções” e há poucas regras sobre o funcionamento deles que sejam um pouco diferentes da conversa anterior.

Vamos ver como dar aos palestrantes algumas palestras e tópicos?

Conceitos da codificação Naked

Para começar, abandone todas as matrizes. O NOF não usa matrizes para propriedades de coleção e, em vez disso, conta totalmente com objetos da coleção (IEnumerable<T>-derived) para armazenar zero para muitos de algum outro tipo. O manual do NOF recomenda enfaticamente que essas coleções sejam fortemente tipadas (usando genéricos) e o NOF não permite várias associações de tipos de valor (como cadeias de caracteres, tipos enumerados etc.) porque acredita que, se o tipo é suficientemente “importante” para garantir a associação, ele deve ser um domínio do tipo completo.

Assim, por exemplo, se eu quiser capturar a noção de um tópico (como "C#," "Java" ou "Sistemas Distribuídos") no sistema de conferência, onde outras abordagens de programação podem permitir que você se contente com uma simples "lista de cadeias de caracteres" como tipo de propriedade, o NOF insiste que esse tópico seja um tipo de objeto de domínio completo (que é uma classe pública com propriedades), com suas próprias regras de domínio. É razoável que a lista de tópicos possa ser um conjunto fixo; no entanto, para isso, propagarei no banco de dados um conjunto completo de tópicos que minha conferência deseja considerar.

Da mesma maneira, embora uma palestra possa ser apenas um título, na verdade ela é uma série de coisas: um título, uma descrição, um tópico (ao qual ela pertence ou refere-se) e é fornecida por um ou mais palestrantes. Sem dúvida, ainda tenho um pouco de modelagem de domínio pela frente.

Coleções Naked

Em muitos aspectos, a maneira mais fácil de começar é com as próprias classes de domínio Talk e Topic, sem conexões entre elas (ou Speakers). Por agora, grande parte do que eu escrevo aqui sobre cada um deles será bem simples e direto, como mostra a Figura 1.

Figura 1 - Classes de domínio para palestra e tópico

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>();
    }
  }

Até agora, isso é bem direto ao ponto. (Obviamente, existem outras coisas que poderiam e/ou deveriam ser adicionadas a cada uma dessas duas classes, mas isso já explica tudo muito bem.) Um novo atributo usado, [Bounded], é uma indicação para o NOF de que a lista completa (e imutável) das instâncias pode e deve ser mantida na memória do cliente e apresentada ao usuário como uma lista suspensa com opções a escolher. Do mesmo modo, a lista completa de tópicos deve ser estabelecida no banco de dados, o que é mais fácil se feito na classe DbInitializer do projeto "SeedData", no método Seed (como discutido nas colunas anteriores desta série), mostrado na Figura 2.

Figura 2 - Criando uma lista de tópicos

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();
  // ...
}

Isso fornecerá uma lista (pequena, mas útil) de tópicos para trabalhar. A propósito, se você estiver executando o jogo inicial e escrevendo o código manualmente, lembre-se de adicionar o TalkRepository ao menu principal, adicionando-o ao método MainMenus no NakedObjectsRunSettings.cs do projeto Server. Além disso, verifique se os dois tipos de repositório também estão listados no método Services, no mesmo arquivo.

Fundamentalmente, uma palestra é dada por um palestrante e é sobre um determinado tópico. Por enquanto, para simplificar, vou ignorar o cenário mais complicado, quando uma palestra é dada por dois palestrantes ou se uma palestra apresentará vários tópicos. Portanto, como primeira etapa, vamos adicionar palestras ao palestrante:

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

Se você compilar e executar o projeto, verá "Talks" (palestras) exibidas como uma coleção (tabela) na interface do usuário, mas ela estará vazia. Obviamente, eu poderia adicionar algumas palestras ao SeedData; mas, em geral, os palestrantes precisam estar aptos a adicionar palestras ao próprio perfil.

Ações naked

Isso pode ser feito adicionando uma ação à classe Speaker (palestrante): Um método que aparecerá “magicamente” como um item selecionável no menu “Actions” quando um objeto Speaker estiver na exibição. Assim como as propriedades, as ações são descobertas por meio da mágica da reflexão; portanto, tudo o que precisa acontecer é criar um método público na classe Speaker:

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

Agora, quando compilado e executado, após trazer um Palestrante, o menu "Actions" é exibido e, dentro dele, aparece "SayHello". Atualmente, ela não tem função; seria bom, como ponto de partida, pelo menos colocar uma mensagem para o usuário. No mundo do NOF, isso é feito por meio de um serviço, um objeto cujo objetivo é fornecer algumas funcionalidades adicionais que não pertençam a um objeto de domínio específico. No caso de “mensagens para o usuário” gerais, é fornecido por um serviço genérico, definido pelo próprio NOF na interface IDomainObjectContainer. No entanto, preciso de uma instância de uma dessas opções para fazer qualquer coisa, e o NOF usa injeção de dependência para fornecer uma sob demanda: Declare uma propriedade na classe Speaker do tipo IDomainObjectContainer e o NOF garantirá que cada instância tenha uma:

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

O objeto Container tem um método "InformUser" usado para transmitir mensagens gerais para o usuário; portanto, usá-lo pela ação SayHello é muito simples:

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

Eu comecei com um desejo de permitir que o usuário adicionasse uma palestra ao repertório de um determinado palestrante. Especificamente, eu preciso capturar o título da palestra, o abstrato (ou descrição, pois "abstrato" é uma palavra reservada no C#) e o tópico ao qual pertence a palestra. Chamando esse método de "EnterNewTalk", tenho a implementação a seguir:

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);
}

Várias coisas estão acontecendo aqui; então, vamos esclarecer. Primeiro, uso o IDomainObjectContainer para criar uma instância transitória (não persistentes) da palestra. Isso é necessário porque o NOF precisa estar apto a injetar "hooks" em cada objeto de domínio para que sua mágica funcione. (É por isso que todas as propriedades devem ser virtuais; para que o NOF possa gerenciar a sincronização da interface do usuário com o objeto, por exemplo.) Em seguida, as propriedades da palestra são definidas e o Container é usado novamente para Persistir a palestra. Se isso não for feito, a palestra não será um objeto persistente e não será armazenada quando eu adicioná-la à lista de palestras do palestrante.

É razoável perguntar, no entanto, como o usuário especificou essas informações para o próprio método EnterNewTalk. Mais uma vez, as maravilhas da reflexão estão em ação: O NOF extraiu os nomes e tipos de parâmetro dos parâmetros do método e construiu um diálogo para capturar esses itens, incluindo o próprio tópico. Lembra-se ao tópico foi anotado como "Bounded"? Isso instruiu o NOF a criar a lista de tópicos nesse diálogo para ser uma lista suspensa, tornando muito mais fácil selecionar um tópico na lista. (Neste ponto, fica fácil deduzir que, como adicionei tópicos ao sistema, eles serão todos adicionados a essa lista suspensa, sem necessidade de qualquer trabalho adicional.)

Agora, é sensato sugerir que a criação de palestras seja algo com suporte de TalkRepository, não na própria classe Speaker. Como você pode ver na Figura 3, é uma refatoração fácil de fazer.

Figura 3 - Suporte para a criação de palestra com 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;
    }
  }

Além disso, ao fazer isso, o menu "Talks" será automaticamente adornado com um novo item de menu, "CreateTalk", que irá, novamente, por meio da mágica da reflexão, criar automaticamente um diálogo para inserir os dados necessários para criar uma palestra. Obviamente, os palestrantes podem simplesmente ser digitados; portanto, o NOF não torná esse campo “soltável”, ou seja, o NOF não esperará que um objeto Speaker seja arrastado e solto nesse campo. (Para ver isso em ação na interface NOF Gemini padrão, acione o aplicativo, selecione um Speaker e clique no botão "Swap Pane", o botão com duas setas na parte inferior da tela. O Speaker selecionado mudará para a direita da tela e a interface inicial será exibida, permitindo a seleção do item "Talks/Create Talk". Arraste o nome do palestrante para o campo Speaker, na caixa de diálogo Create Talk, e pronto, o Speaker estará selecionado.)

É absolutamente fundamental entender o que está acontecendo aqui: Agora, tenho dois caminhos diferentes da interface do usuário (criar uma palestra pelo menu de nível superior ou criar uma palestra por um determinado Speaker) que permitem dois cenários diferentes de navegação do usuário, com pouco esforço e nenhuma duplicação. O TalkRepository se preocupa com tudo relacionado a "CRUD" e "Talk" e o Speaker usa esse código, tudo enquanto mantém a interação do usuário inteiramente dentro do Speaker, se for a organização desejada pelo usuário.

Conclusão

Este não é um kit de ferramentas de interface do usuário do seu avô. Em apenas algumas linhas de código, tenho uma interface viável (e, considerando que os dados são armazenados e recuperados de um back-end SQL padrão, um sistema de armazenamento de dados) que pode, pelo menos por enquanto, ser usada diretamente pelos usuários. O mais importante é que nenhum desses é um formato ou linguagem proprietário, ou seja, é C# direta, SQL Server direto, e a própria interface do usuário é Angular. Há algumas coisas para discutir sobre a interface padrão do NOF, que abordarei na próxima parte, incluindo recursos de autenticação e autorização. Mas, por enquanto... feliz codificação!


Ted Neward é um consultor de politecnologia, palestrante e mentor de Seattle. Ele já escreveu uma enormidade de artigos, é autor e coautor de dezenas de livros e faz palestras no mundo inteiro. Entre em contato com ele em ted@tedneward.com ou leia seu blog em blogs.tedneward.com.

Agradecemos ao seguinte especialista técnico pela revisão deste artigo: Richard Pawson


Discuta esse artigo no fórum do MSDN Magazine