Next, we want to build some UI to show us the data we've got back from
Twitter. For this we're going to use WPF. Before we get started with
that, however, a bit more refactoring is needed. Since we want our UI to
be nice and responsive, we're going to do all of our talking to twitter
on background threads. To that end, we want to make our TweetSharp code
thread safe. We have basically have two major requests that we need to
make, one to get all of our friends and one to get all of our followers.
We can send those off more or less simultaneously, and wait for twitter
to send back the response, all the while keeping the UI thread free to
repaint the form as needed.
To that end, let's wrap our twitter code up into a class. This also has
the nice side-effect of limiting our dependency on TweetSharp from the
UI layer. We could sprinkle TweetSharp calls throughout the UI, but that
would be a refactoring nightmare should
Daniel and I ever fall off a cliff and stop
updating it, forcing you to move to one of the other (less fun ;) )
libraries out there.
I'm not feeling particularly creative, so I'm going to just call the class "Twitter":
namespace Diller.SocialGraphMinder
{
internal class Twitter
{
private readonly string _userName;
private readonly string _password;
private TwitterUser _User;
public Twitter( string username, string password )
{
_userName = username;
_password = password;
IsAuthenticated = Authenticate();
}
private bool Authenticate()
{
var twitter = FluentTwitter.CreateRequest()
.AuthenticateAs(_userName, _password)
.Account().VerifyCredentials()
.AsJson();
var response = twitter.Request();
_User = response.AsUser();
return _User != null;
}
public bool IsAuthenticated{ get; private set; }
}
}
Nothing particularly interesting there. Upon construction, the class
performs a credentials verification with the given credentials and sets
the IsAuthenticated bool so we know it's safe to proceed.
Next, we'll add the friend and follower fetching code:
//add these methods
public IEnumerable<TwitterUser> GetFriends()
{
if (!IsAuthenticated)
{
throw new InvalidOperationException("Not authenticated");
}
if ( _friends == null ) //only do this if we don't already have the
friends list
{
var twitter = FluentTwitter.CreateRequest()
.AuthenticateAs(_userName, _password)
.Configuration.UseGzipCompression() //now using compression for
performance
.Users().GetFriends().For(_userName)
.CreateCursor()
.AsJson();
_friends = GetAllCursorValues(twitter, s => s.AsUsers());
}
return _friends; //return either the newly fetched list, or the cached copy
}
public IEnumerable<TwitterUser> GetFollowers()
{
if ( !IsAuthenticated)
{
throw new InvalidOperationException("Not authenticated");
}
if (_followers == null)
{
var twitter = FluentTwitter.CreateRequest()
.AuthenticateAs(_userName, _password)
.Configuration.UseGzipCompression()
.Users().GetFollowers().For(_userName)
.AsJson();
_followers = GetAllCursorValues(twitter, s => s.AsUsers());
}
return _followers;
}
//we also need the cursor paging code from part 2
private static IEnumerable<T> GetAllCursorValues<T>(ITwitterLeafNode twitter, Func<string, IEnumerable<T>> conversionMethod)
{
long? nextCursor = -1 ;
var ret = new List<T>();
do
{
twitter.Root.Parameters.Cursor = nextCursor;
var response = twitter.Request();
IEnumerable<T> values = conversionMethod(response);
if (values != null)
{
ret.AddRange(values);
}
nextCursor = response.AsNextCursor();
} while (nextCursor.HasValue &amp;amp;&amp;amp;
nextCursor.Value != 0);
return ret;
}
Finally, lets add some methods to get the data we're really after, the
aforementioned 'spammers'(users who follow us, but whom we don't
follow), and 'jerks' (users whom we follow, but who do not follow us
back).
public IEnumerable<TwitterUser> GetFollowersWhoArentFriends()
{
friends = GetFriends();
followers = GetFollowers();
return followers.Except(friends);
}
If we left this code as-is, it would work perfectly well in a
single-threaded application. Whatever code path got to the 'GetFriends'
and 'GetFollowers' methods first would make all the proper calls and
cache the results in the member variables. However if you try to use
this class in a multi-threaded fashion you're going to be in for a world
of hurt. (Well, really, it most likely just plain ol' won't work).
If, for example, you create two threads, one to call the
'GetFriendsWhoDontFollowBack' method and the other to call the
'GetFollowersWhoArentFriends' method, both are going to call
GetFriends() and GetFollowers(), likely before the other one has
finished. At best, you're duplicating work and chewing up your API limit
unnecessarily, at worst, you're going to confuse twitter by resetting
the cursor on the second thread somewhere in the middle of paging
through it on the first, causing it to throw HTTP 500 errors. (This is
what happened when I tried it, and it's something that probably warrants
its own blog post as it was unexpected).
So, to remedy this, we need to synchronize access to the TweetSharp calls so we're not making a tangly mess:
//add the following private members
private readonly object _friendsLock = new object();
private readonly object _followersLock = new object();
//amend the getfriends and getfollowers methods so they look like
this:
public IEnumerable<TwitterUser> GetFriends()
{
if (!IsAuthenticated)
{
throw new InvalidOperationException("Not authenticated");
}
//see if friends are already fetched
if (_friends == null)
{
//if not already fetched
//wait for exclusive access to this code
lock (_friendsLock)
{
//need to double-check here
//another thread might have set the member
//while we were waiting
if (_friends == null)
{
var twitter = FluentTwitter.CreateRequest()
.AuthenticateAs(_userName, _password)
.Configuration.UseGzipCompression()
.Users().GetFriends().For(_userName)
.CreateCursor()
.AsJson();
_friends = GetAllCursorValues(twitter, s => s.AsUsers());
}
}//lock is released here
}
return _friends;
}
//make the same adjustments to this method:
public IEnumerable<TwitterUser> GetFollowers()
{
if ( !IsAuthenticated)
{
throw new InvalidOperationException("Not authenticated");
}
if (_followers == null)
{
lock (_followersLock)
{
if (_followers == null)
{
var twitter = FluentTwitter.CreateRequest()
.AuthenticateAs(_userName, _password)
.Configuration.UseGzipCompression()
.Users().GetFollowers().For(_userName)
.AsJson();
_followers = GetAllCursorValues(twitter, s => s.AsUsers());
}
}
}
return _followers;
}
A few important points:
- We used separate lock objects around the fetching of _friends and
_followers objects as they are independent and can be fetched
simultaneously.
- For best performance we only lock when we're going to do the fetch.
Once the objects exist they can be read without locking by any
thread.
- The double-check for null after acquiring the lock is important to
prevent duplication of effort.
- If we decide to periodically update the _friends and _followers
lists, we would probably want to convert our locking behavior to a
ReaderWriterLockSlim type, which is ideal for resources that are read
a lot but infrequently updated.
It's also worth noting that Tweetsharp instances aren't thread safe.
You should never try to use the same instance of a FluentTwitter request
across multiple threads without doing your own synchronization. Since
they are designed to be transient, it's best to just create a new one on
each thread that you're using to talk to Twitter and leave it there.
This post has gotten too long already, so let's end it here and take it
up again in part 4...at which point, I promise, we will actually put
pixels on the screen.