Linqを使用してfacebookプロファイルをユーザー情報にマップする
質問
LINQに関する本を読んだ後、LINQを使用するためにc#で記述したマッパークラスを書き直すことを考えています。誰かが私に手を貸してくれないかと思っています。注:少しわかりにくいですが、ユーザーオブジェクトはローカルユーザーであり、ユーザー(小文字)はFacebook XSDから生成されたオブジェクトです。
オリジナルマッパー
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;
}
}
最初の2回の試みが壁にぶつかった
試行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);
}
解決
OK私はそれについて考えたのとほぼ同じようにこれを書きました-それはあなたが次回同じことをするのをより簡単にするので、行動中の思考の流れを見るのに役立つと思います(もちろん、私のソリューションが好きだと仮定して:)
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
を完全に不変にしたい場合もあります。ビルダーパターンなど、さまざまな戦略があります。詳細が必要な場合は私に尋ねてください。とにかく、主なことは、古いユーザーのコピーであるが指定されたアバターを持つ新しいユーザーを作成したことです。
最初の試行:内部結合
マッパーに戻ります...現在3つの public メソッドがありますが、私の guess は最初のメソッドのみがパブリックである必要があり、 APIの残りは、実際にはFacebookユーザーを公開する必要はありません。 GetFacebookUsers
メソッドは基本的に問題ないように見えますが、おそらく空白の観点からクエリを並べます。
したがって、ローカルユーザーのシーケンスとFacebookユーザーのコレクションが与えられた場合、実際のマッピングビットはそのままにします。まっすぐな「結合」句は問題があります。これは、一致するFacebookユーザーを持たないローカルユーザーを生成しないためです。代わりに、非FacebookユーザーをアバターのないFacebookユーザーであるかのように扱う何らかの方法が必要です。基本的に、これはnullオブジェクトパターンです。
これを行うには、null uidを持つ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ユーザーのルックアップは、常に「本物」のユーザーを見つけることを想定することに注意してください。 Facebook UID。そうでない場合は、これを再検討し、内部結合を使用しないでください。
「null」を含めます;最後にユーザー、次に 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
}
}
ここにいくつかのポイントがあります:
- 前述のように、ユーザーのFacebook UIDが有効なユーザーとして取得されない場合、内部結合に問題が生じます。
- 同様に、Facebookユーザーが重複していると問題が発生します。各ローカルユーザーが2回出てくることになります!
- これにより、Facebook以外のユーザーのアバターが置き換えられます(削除されます)。
2番目のアプローチ:グループ参加
これらのポイントに対処できるかどうか見てみましょう。 1つの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ソリューション
他のヒント
代わりにこのようなものを書きたいと思います:
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のユーザーコレクションが非常に大きい場合を除き、大幅なパフォーマンスの改善は得られない可能性があります。 >
( foreach
ループのような Select
を使用して、セットの要素に対して実際の変更アクションを実行することはお勧めしません。あなたはそれを行うことができますが、人々はあなたのコードに驚くでしょう、そしてあなたはいつも怠wholeな評価を心に留めておく必要があります。)