F# and the Open/Closed Principle

One of the advantages of using F# is that it is a .NET language.  Although F# is a functional-first language, it also supports object-oriented constructs.  One of the most powerful (indeed, the most powerful) technique in OO programming is using interfaces to follow the Open/Closed principle.  If you are not familiar, a good explanation of Open/Closed principle is found here.

As part of the F# for beginners dojo I am putting on next week, we are consuming and then analyzing Twitter.  The problem with always making calls to Twitter is that

1) The data changes every call

2) You might get throttled

Therefore, it makes good sense to have an in-memory representation of the data for testing and some Twitter data on disk so that different experiments can be run on the same data to see the result.  Using Interfaces in F# makes this a snap.

First, I created an interface:

  1. namespace NewCo.TwitterAnalysis
  3. open System
  4. open System.Collections.Generic
  6. type ITweeetProvider =
  7.    abstract member GetTweets : string -> IEnumerable<DateTime * int * string>

Next, I created the actual Twitter feed.  Note I am using TweetInvi (available on Nuget) and that this file has to be below the interface in the solution explorer:

  1. namespace NewCo.TwitterAnalysis
  3. open System
  4. open System.Configuration
  5. open Tweetinvi
  7. type TwitterProvider() =
  8.     interface ITweeetProvider with
  9.         member this.GetTweets(stockSymbol: string) =
  10.             let consumerKey = ConfigurationManager.AppSettings.["consumerKey"]
  11.             let consumerSecret = ConfigurationManager.AppSettings.["consumerSecret"]
  12.             let accessToken = ConfigurationManager.AppSettings.["accessToken"]
  13.             let accessTokenSecret = ConfigurationManager.AppSettings.["accessTokenSecret"]
  15.             TwitterCredentials.SetCredentials(accessToken, accessTokenSecret, consumerKey, consumerSecret)
  16.             let tweets = Search.SearchTweets(stockSymbol);
  17.             tweets
  18.                 |> Seq.map(fun t -> t.CreatedAt, t.RetweetCount, t.Text)


I then hooked up a unit (integration, really) test

  1. [TestClass]
  2. public class UnitTest1
  3. {
  4.     [TestMethod]
  5.     public void GetTweetsUsingIBM_returnsExpectedValue()
  6.     {
  7.         ITweeetProvider provider = new TwitterProvider();
  8.         var actual = provider.GetTweets("IBM");
  9.         Assert.IsNotNull(actual);
  10.     }
  11. }

Sure enough, it ran green with actual Twitter data coming back:


I then created an In-Memory Tweet provider that can be used to:

1) Provide repeatable results

2) Have 0 external dependencies so that I can monkey with the code and a red unit test really does mean red

Here is its implementation:

  1. namespace NewCo.TwitterAnalysis
  3. open System
  4. open System.Collections.Generic
  6. type InMemoryProvider() =
  7.     interface ITweeetProvider with
  8.         member this.GetTweets(stockSymbol: string) =
  9.             let list = new List<(DateTime*int*string)>()
  10.             list.Add(DateTime.Now, 1,"Test1")
  11.             list.Add(DateTime.Now, 0,"Test2")
  12.             list :> IEnumerable<(DateTime*int*string)>

The only really interesting thing is the smiley/bird character (: >).  F# implements interfaces a bit differently than what I was used to –> F# implements interfaces explicitly.  I then fired up a true unit test and it also ran green:

  1. [TestClass]
  2. public class InMemoryProviderTests
  3. {
  4.     [TestMethod]
  5.     public void GetTweetsUsingValidInput_ReturnsExpectedValue()
  6.     {
  7.         ITweeetProvider provider = new InMemoryProvider();
  8.         var tweets = provider.GetTweets("TEST");
  9.         var tweetList = tweets.ToList();
  10.         Int32 expected = 2;
  11.         Int32 actual = tweetList.Count;
  12.         Assert.AreEqual(expected, actual);
  13.     }
  14. }

Finally, I created a file-system bound provider so that I can download and then hold static a large dataset.  Based on past experience dealing with on-line data sources, getting data local to run multiple tests against is generally a good idea.  Here is the implementation:

  1. namespace NewCo.TwitterAnalysis
  3. open System
  4. open System.Collections.Generic
  5. open System.IO
  7. type FileSystemProvider(filePath: string) =
  8.     interface ITweeetProvider with
  9.         member this.GetTweets(stockSymbol: string) =
  10.             let fileContents = File.ReadLines(filePath)
  11.                                 |> Seq.map(fun line -> line.Split([|'\t'|]))
  12.                                 |> Seq.map(fun values -> DateTime.Parse(values.[0]),int values.[1], string values.[2])
  13.             fileContents

And the covering unit (integration really) tests look like this:

  1. [TestClass]
  2. public class FileSystemProviderTests
  3. {
  4.     [TestMethod]
  5.     public void GetTweetsUsingValidInput_ReturnsExpectedValue()
  6.     {
  7.         var baseDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
  8.         var testFile = Path.Combine(baseDir, "TweetData.csv");
  9.         ITweeetProvider provider = new FileSystemProvider(testFile);
  10.         var tweets = provider.GetTweets("TEST");
  11.         var tweetList = tweets.ToList();
  12.         Int32 expected = 2;
  13.         Int32 actual = tweetList.Count;
  14.         Assert.AreEqual(expected, actual);
  15.     }
  16. }

Note that I had to add the actual file in the test project. 


Finally, the F# code needs to include try..catches for the external calls (web service and disk) and some argument validation for the strings come in.

In any event, I now have 3 different implementations that I can swap out depending on my needs.  I love having the power of Interfaces combined with benefits of using a functional-first language.

2 Responses to F# and the Open/Closed Principle

  1. Pingback: F# Weekly #16, 2014 | Sergey Tihon's Blog

  2. Pingback: 4/24/2014 Article Links | The Puntastic Programmer

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s


Get every new post delivered to your Inbox.

Join 31 other followers

%d bloggers like this: