Использование Linq для сопоставления профиля в facebook с моей информацией о пользователе - PullRequest
3 голосов
/ 02 марта 2009

После прочтения книги по LINQ я думаю о переписывании класса mapper, который я написал на c # для использования LINQ. Мне интересно, кто-нибудь может мне помочь. Примечание: это немного сбивает с толку, но объект User - это локальный пользователь, а user (нижний регистр) - это объект, созданный из XSD Facebook.

Оригинальный картограф

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

Мои первые две попытки поразили стены

Попытка 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;
}

Попытка 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);
}

Ответы [ 2 ]

9 голосов
/ 01 апреля 2009

Ладно, не совсем понятно, что именно есть в IMapper, но я бы предложил несколько вещей, некоторые из которых могут оказаться неосуществимыми из-за других ограничений. Я написал это в значительной степени, поскольку я думал об этом - я думаю, что это помогает видеть ход мыслей в действии, поскольку это облегчит вам делать то же самое в следующий раз. (Конечно, если вам нравятся мои решения:)

LINQ по своей природе функционально по стилю. Это означает, что в идеале запросы не должны иметь побочных эффектов. Например, я бы ожидал метод с подписью:

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

чтобы вернуть новую последовательность пользовательских объектов с дополнительной информацией, а не мутировать существующих пользователей. Единственная информация, которую вы сейчас добавляете, - это аватар, поэтому я бы добавил метод в User в соответствии с:

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

Возможно, вы даже захотите сделать User полностью неизменным - существуют различные стратегии, такие как шаблон строителя. Спросите меня, если вы хотите больше деталей. В любом случае, главное, что мы создали нового пользователя, который является копией старого, но с указанным аватаром.

Первая попытка: внутреннее соединение

Теперь вернемся к вашему картографу ... в настоящее время у вас есть три открытых метода, но мое предположение состоит в том, что только первый должен быть публичным, а остальные API на самом деле не нужно показывать пользователям Facebook. Похоже, что ваш метод GetFacebookUsers в целом нормальный, хотя я бы, вероятно, выстроил запрос в терминах пробелов.

Итак, учитывая последовательность локальных пользователей и коллекцию пользователей Facebook, мы оставляем фактический бит отображения. Прямое предложение «присоединиться» проблематично, потому что оно не даст локальных пользователей, у которых нет подходящего пользователя Facebook. Вместо этого нам нужен некоторый способ обращения с пользователями, не являющимися Facebook, как если бы они были пользователями Facebook без аватара. По сути, это шаблон нулевого объекта.

Мы можем сделать это, придумав пользователя Facebook, у которого нулевой идентификатор (при условии, что объектная модель позволяет это):

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

Тем не менее, мы действительно хотим последовательность этих пользователей, потому что это то, что Enumerable.Concat использует:

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

Теперь мы можем просто «добавить» эту фиктивную запись в нашу реальную и выполнить обычное внутреннее соединение. Обратите внимание, что предполагает , что при поиске пользователи Facebook всегда найдут пользователя для любого «реального» UID Facebook. Если это не так, мы должны вернуться к этому и не использовать внутреннее соединение.

В конце мы добавляем «нулевого» пользователя, затем выполняем объединение и проектирование, используя 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);
}

Таким образом, полный класс будет:

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

Несколько моментов здесь:

  • Как отмечалось ранее, внутреннее объединение становится проблематичным, если пользовательский UID Facebook не может быть выбран как действительный пользователь.
  • Точно так же у нас возникают проблемы, если у нас есть дубликаты пользователей Facebook - каждый локальный пользователь будет выходить дважды!
  • Это заменяет (удаляет) аватар для пользователей не из Facebook.

Второй подход: групповое объединение

Посмотрим, сможем ли мы решить эти вопросы. Я предполагаю, что если мы выбрали несколько пользователей Facebook для одного UID Facebook, то не имеет значения, с какого из них мы берем аватар - они должны быть одинаковыми.

Нам нужно групповое объединение, чтобы для каждого локального пользователя мы получали последовательность совпадающих пользователей Facebook. Затем мы будем использовать DefaultIfEmpty, чтобы облегчить жизнь.

Мы можем сохранить WithAvatar, как это было раньше - но на этот раз мы будем звонить только в том случае, если у нас есть пользователь Facebook, с которого можно получить аватар. Групповое объединение в выражениях запросов C # представлено join ... into. Этот запрос достаточно длинный, но не слишком страшный, честный!

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

Вот снова выражение запроса, но с комментариями:

// "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);

Решение не от LINQ

Другой вариант - использовать LINQ очень незначительно. Например:

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

При этом используется выражение блока итератора вместо выражения запроса LINQ. ToDictionary сгенерирует исключение, если получит один и тот же ключ дважды - один из способов обойти это - изменить GetFacebookUsers, чтобы убедиться, что он ищет только отдельные идентификаторы:

    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
    }

Это предполагает, что веб-сервис работает, конечно, - но если это не так, вы, вероятно, все равно захотите вызвать исключение:)

Заключение

Возьми свой выбор из трех. Групповое объединение, вероятно, труднее понять, но оно ведет себя лучше. Решение блока итераторов, возможно, самое простое, и оно должно вести себя хорошо с модификацией GetFacebookUsers.

Делать User неизменным почти наверняка было бы позитивным шагом.

Одним из приятных побочных продуктов всех этих решений является то, что пользователи выходят в том же порядке, в котором они были. Это может быть не важно для вас, но это может быть хорошим свойством.

Надеюсь, это поможет - это был интересный вопрос:)

РЕДАКТИРОВАТЬ: мутации путь?

Увидев в ваших комментариях, что локальный тип пользователя на самом деле является типом сущности из структуры сущностей, может не подходить для такого курса действий. О том, чтобы сделать его неизменным, в значительной степени не может быть и речи, и я подозреваю, что большинство применений этого типа будут ожидать мутации.

Если это так, возможно, стоит изменить интерфейс, чтобы сделать его более понятным. Вместо того, чтобы возвращать IEnumerable<User> (что подразумевает - в некоторой степени - проекцию), вы можете изменить как подпись, так и имя, оставив вам что-то вроде этого:

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

Опять же, это уже не особо "LINQ-y" решение (в основной операции) - но это разумно, поскольку вы на самом деле не "запрашиваете"; вы "обновляете".

5 голосов
/ 29 марта 2009

Я был бы склонен написать что-то вроде этого:

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

Однако я бы не стал утверждать, что это было значительное улучшение по сравнению с тем, что у вас есть (если только коллекции пользователей или пользователей Facebook не очень большие, в этом случае вы можете столкнуться с заметной разницей в производительности.)

(Я бы рекомендовал не использовать Select, как цикл foreach, для выполнения фактического действия с мутированием над элементом набора, как вы делали это в своих попытках рефакторинга. Вы можете сделать это, но люди будут Вы будете удивлены вашим кодом, и вам придется все время помнить о ленивой оценке.)

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...