Usando Linq para mapear perfil no Facebook com minhas informações de usuário

StackOverflow https://stackoverflow.com/questions/603650

  •  03-07-2019
  •  | 
  •  

Pergunta

Depois de ler um livro sobre LINQ Estou pensando em re-escrever uma classe mapeador que eu escrevi em c # para uso LINQ. Eu estou querendo saber se alguém pode me dar uma mão. Nota:. É um pouco confuso, mas o objeto de usuário é o usuário local e usuário (em minúsculas) é o objeto gerado a partir do Facebook XSD

Original Mapper

public class FacebookMapper : IMapper
{
    public IEnumerable<User> MapFrom(IEnumerable<User> users)
    {
      var facebookUsers = GetFacebookUsers(users);
      return MergeUsers(users, facebookUsers);
    }

    public Facebook.user[] GetFacebookUsers(IEnumerable<User> users)
    {
      var uids = (from u in users
        where u.FacebookUid != null
        select u.FacebookUid.Value).ToList();

      // return facebook users for uids using WCF
    }

    public IEnumerable<User> MergeUsers(IEnumerable<User> users, Facebook.user[] facebookUsers)
    {
      foreach(var u in users)
      {
        var fbUser = facebookUsers.FirstOrDefault(f => f.uid == u.FacebookUid);
        if (fbUser != null)
          u.FacebookAvatar = fbUser.pic_sqare;
      }
      return users;
    }
}

As minhas duas primeiras tentativas bater paredes

Tentativa 1

public IEnumerable<User> MapFrom(IEnumerable<User> users)
{
  // didn't have a way to check if u.FacebookUid == null
  return from u in users
    join f in GetFacebookUsers(users) on u.FacebookUid equals f.uid
    select AppendAvatar(u, f);
}

public void AppendAvatar(User u, Facebook.user f)
{
  if (f == null)
    return u;
  u.FacebookAvatar = f.pic_square;
  return u;
}

Tentativa 2

public IEnumerable<User> MapFrom(IEnumerable<User> users)
{
  // had to get the user from the facebook service for each single user,
  // would rather use a single http request.
  return from u in users
    let f = GetFacebookUser(user.FacebookUid)
    select AppendAvatar(u, f);
}
Foi útil?

Solução

Ok, não é claro exatamente o que IMapper tem nele, mas eu sugiro algumas coisas, alguns dos quais podem não ser devido viável para outras restrições. Eu escrevi isso muito bem como eu pensava sobre isso - eu acho que ajuda a ver a linha de pensamento em ação, como que vai torná-lo mais fácil para você fazer a mesma coisa na próxima vez. (Supondo que você gosta de minhas soluções, é claro:)

LINQ é inerentemente funcional em grande estilo. Isso significa que, idealmente, as consultas não devem ter efeitos colaterais. Por exemplo, eu esperaria um método com uma assinatura de:

public IEnumerable<User> MapFrom(IEnumerable<User> users)

para retornar uma nova seqüência de objetos de usuário com informações adicionais, ao invés de mutação os usuários existentes. A única informação que você está anexando atualmente é o avatar, então eu adicionar um método em User ao longo das linhas de:

public User WithAvatar(Image avatar)
{
    // Whatever you need to create a clone of this user
    User clone = new User(this.Name, this.Age, etc);
    clone.FacebookAvatar = avatar;
    return clone;
}

Você pode até querer fazer User totalmente imutável - existem várias estratégias em torno de que, como o padrão do construtor. Pergunte-me se você quiser mais detalhes. Enfim, a coisa principal é que nós criamos um novo usuário que é uma cópia do antigo, mas com o avatar especificado.

Primeira tentativa: junção interna

Agora, de volta ao seu mapeador ... você atualmente tem três público métodos, mas o meu palpite é que apenas os primeiros a pessoa precisa ser público, e que a resto da API na verdade não precisa expor os usuários do Facebook. Parece que o seu método GetFacebookUsers é basicamente tudo bem, embora eu provavelmente iria alinhar a consulta em termos de espaço em branco.

Assim, dada uma seqüência de usuários locais e uma coleção de usuários do Facebook, ficamos fazendo o bit mapeamento real. A reta "juntar-se" cláusula é problemática, porque não vai render os usuários locais que não têm um usuário correspondente Facebook. Em vez disso, precisamos de alguma maneira de tratar um usuário não-Facebook como se fossem um usuário do Facebook sem um avatar. Essencialmente, este é o padrão nulo objeto.

Podemos fazer isso por surgir com um usuário do Facebook que tem um uid nula (assumindo que o modelo de objeto permite que):

// Adjust for however the user should actually be constructed.
private static readonly FacebookUser NullFacebookUser = new FacebookUser(null);

No entanto, nós realmente quer um seqüência desses usuários, porque é isso que os usos Enumerable.Concat:

private static readonly IEnumerable<FacebookUser> NullFacebookUsers =
    Enumerable.Repeat(new FacebookUser(null), 1);

Agora podemos simplesmente "adicionar" esta entrada manequim para o nosso real, e fazer participar de um interior normal. Note-se que esta assume que a pesquisa de usuários do Facebook vai sempre encontrar um usuário para qualquer "real" Facebook UID. Se isso não for o caso, tínhamos necessidade de rever isso e não usar uma junção interna.

Nós incluímos o usuário "nulo" no final, em seguida, fazer a junção e projeto usando WithAvatar:

public IEnumerable<User> MapFrom(IEnumerable<User> users)
{
    var facebookUsers = GetFacebookUsers(users).Concat(NullFacebookUsers);
    return from user in users
           join facebookUser in facebookUsers on
                user.FacebookUid equals facebookUser.uid
           select user.WithAvatar(facebookUser.Avatar);
}

Assim, a classe cheia seria:

public sealed class FacebookMapper : IMapper
{
    private static readonly IEnumerable<FacebookUser> NullFacebookUsers =
        Enumerable.Repeat(new FacebookUser(null), 1);

    public IEnumerable<User> MapFrom(IEnumerable<User> users)
    {
        var facebookUsers = GetFacebookUsers(users).Concat(NullFacebookUsers);
        return from user in users
               join facebookUser in facebookUsers on
                    user.FacebookUid equals facebookUser.uid
               select user.WithAvatar(facebookUser.pic_square);
    }

    private Facebook.user[] GetFacebookUsers(IEnumerable<User> users)
    {
        var uids = (from u in users
                    where u.FacebookUid != null
                    select u.FacebookUid.Value).ToList();

        // return facebook users for uids using WCF
    }
}

Alguns pontos aqui:

  • Como observado anteriormente, a junção interna torna-se problemático se um usuário Facebook UID pode não ser obtida como um usuário válido.
  • Da mesma forma temos problemas se tivermos duplicados usuários do Facebook - cada usuário local acabaria saindo duas vezes
  • !
  • Isto substitui (remove) o avatar para usuários não-Facebook.

A segunda abordagem: grupo juntar-se

Vamos ver se podemos resolver esses pontos. Eu vou assumir que, se nós temos vários usuários obtida do Facebook para um único Facebook UID, então não importa qual deles nós agarramos o avatar de -. Que deve ser o mesmo

O que precisamos é um grupo juntar-se, de modo que para cada usuário local temos uma seqüência de correspondência de usuários do Facebook. Vamos então usar DefaultIfEmpty para tornar a vida mais fácil.

Podemos manter WithAvatar como era antes - mas desta vez nós só vamos chamá-lo se temos um usuário do Facebook para pegar o avatar de. Um grupo juntar-se em expressões # consulta C é representado por join ... into. Esta consulta é razoavelmente longo, mas não é muito assustador, honesto!

public IEnumerable<User> MapFrom(IEnumerable<User> users)
{
    var facebookUsers = GetFacebookUsers(users);
    return from user in users
           join facebookUser in facebookUsers on
                user.FacebookUid equals facebookUser.uid
                into matchingUsers
           let firstMatch = matchingUsers.DefaultIfEmpty().First()
           select firstMatch == null ? user : user.WithAvatar(firstMatch.pic_square);
}

Aqui está a expressão de consulta novamente, mas com os comentários:

// "Source" sequence is just our local users
from user in users
// Perform a group join - the "matchingUsers" range variable will
// now be a sequence of FacebookUsers with the right UID. This could be empty.
join facebookUser in facebookUsers on
     user.FacebookUid equals facebookUser.uid
     into matchingUsers
// Convert an empty sequence into a single null entry, and then take the first
// element - i.e. the first matching FacebookUser or null
let firstMatch = matchingUsers.DefaultIfEmpty().First()
// If we've not got a match, return the original user.
// Otherwise return a new copy with the appropriate avatar
select firstMatch == null ? user : user.WithAvatar(firstMatch.pic_square);

A solução não-LINQ

Outra opção é apenas para usar LINQ muito ligeiramente. Por exemplo:

public IEnumerable<User> MapFrom(IEnumerable<User> users)
{
    var facebookUsers = GetFacebookUsers(users);
    var uidDictionary = facebookUsers.ToDictionary(fb => fb.uid);

    foreach (var user in users)
    {
        FacebookUser fb;
        if (uidDictionary.TryGetValue(user.FacebookUid, out fb)
        {
            yield return user.WithAvatar(fb.pic_square);
        }
        else
        {
            yield return user;
        }
    }
}

Este utiliza um bloco de iterator, em vez de uma expressão de consulta LINQ. ToDictionary irá lançar uma exceção se ele recebe a mesma tecla duas vezes - uma opção para contornar este é mudar GetFacebookUsers para ter certeza que só olha para IDs distintos:

    private Facebook.user[] GetFacebookUsers(IEnumerable<User> users)
    {
        var uids = (from u in users
                    where u.FacebookUid != null
                    select u.FacebookUid.Value).Distinct().ToList();

        // return facebook users for uids using WCF
    }

Isso pressupõe o serviço web funciona adequadamente, é claro - mas se isso não acontecer, você provavelmente vai querer lançar uma exceção de qualquer maneira:)

Conclusão

Faça a sua escolha dos três. O grupo juntar-se é provavelmente mais difícil de entender, mas se comporta melhor. A solução bloco iterador é possivelmente o mais simples, e deve comportar-se bem com a modificação GetFacebookUsers.

Fazendo User imutável seria quase certamente um passo positivo embora.

Um bom subproduto de todas estas soluções é que os usuários saem na mesma ordem em que entrou. Isso não pode muito bem ser importante para você, mas pode ser uma propriedade agradável.

Espero que isso ajude - tem sido uma questão interessante:)

EDIT:? É mutação o caminho a percorrer

Depois de ter visto em seus comentários que o tipo de usuário local é realmente um tipo de entidade a partir da estrutura de entidade, ele pode não ser apropriado fazer este curso de ação. Tornando-se imutável é praticamente fora de questão, e eu suspeito que a maioria dos usos do tipo irá esperar mutação.

Se for esse o caso, pode valer a pena mudar sua interface para fazer isso mais claro. Em vez de retornar um IEnumerable<User> (o que implica - até certo ponto - projeção) você pode querer mudar tanto a assinatura eo nome, deixando-o com algo como isto:

public sealed class FacebookMerger : IUserMerger
{
    public void MergeInformation(IEnumerable<User> users)
    {
        var facebookUsers = GetFacebookUsers(users);
        var uidDictionary = facebookUsers.ToDictionary(fb => fb.uid);

        foreach (var user in users)
        {
            FacebookUser fb;
            if (uidDictionary.TryGetValue(user.FacebookUid, out fb)
            {
                user.Avatar = fb.pic_square;
            }
        }
    }

    private Facebook.user[] GetFacebookUsers(IEnumerable<User> users)
    {
        var uids = (from u in users
                    where u.FacebookUid != null
                    select u.FacebookUid.Value).Distinct().ToList();

        // return facebook users for uids using WCF
    }
}

Mais uma vez, esta não é uma solução particularmente "LINQ-y" (na operação principal) mais - mas isso é razoável, como você não está realmente "consulta"; você é "atualizar".

Outras dicas

Eu estaria inclinado a escrever algo como isto em vez disso:

public class FacebookMapper : IMapper
{
    public IEnumerable<User> MapFacebookAvatars(IEnumerable<User> users)
    {
        var usersByID =
            users.Where(u => u.FacebookUid.HasValue)
                 .ToDictionary(u => u.FacebookUid.Value);

        var facebookUsersByID =
            GetFacebookUsers(usersByID.Keys).ToDictionary(f => f.uid);

        foreach(var id in usersByID.Keys.Intersect(facebookUsersByID.Keys))
            usersByID[id].FacebookAvatar = facebookUsersByID[id].pic_sqare;

        return users;
    }

    public Facebook.user[] GetFacebookUsers(IEnumerable<int> uids)
    {
       // return facebook users for uids using WCF
    }
}

No entanto, eu não diria que foi uma grande melhoria sobre o que você tem (a menos que as coleções de usuários do usuário ou do facebook são muito grandes, caso em que você pode acabar com uma diferença de desempenho perceptível.)

(eu recomendo contra o uso Select como um loop foreach para executar uma ação mutante real em um elemento de um conjunto, o jeito que você fez em suas tentativas refatoração. Você pode fazê-lo, mas as pessoas vão se surpreender com o seu código , e você tem que manter a avaliação preguiçosa em mente o tempo todo.)

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top