logo

In-memory Drain abstractions applied to A Functional Architecture with F#

This post shows how to apply the Drain filter abstraction to the master branch of the Pluralsight course about A Functional Architecture with F# by Mark Seemann.

As the diff-output shows, applying filtering with Drains cuts the maintenance of multiple homogenous abstractions, and makes the code cleaner and easier to reason about.

(Also available as a GitHub Gist.)

diff --git a/BookingApi/DomainModel.fs b/BookingApi/DomainModel.fs
index 4a4260f..c2079cf 100644
--- a/BookingApi/DomainModel.fs
+++ b/BookingApi/DomainModel.fs
@@ -7,6 +7,23 @@ type Period =
     | Month of int * int
     | Day of int * int * int

+[<AutoOpen>]
+module Drain =
+    type IDrainable<'a, 'b> =
+        inherit seq<'a>
+        abstract On : 'b -> seq<'a>
+
+    let on x (d : IDrainable<'a, 'b>) = d.On x
+
+    let ofSeq areEqual s =
+        { new IDrainable<'a, 'b> with
+            member this.On x = s |> Seq.filter (fun y -> areEqual y x)
+            member this.GetEnumerator() = s.GetEnumerator()
+            member this.GetEnumerator() =
+                (this :> 'a seq).GetEnumerator() :> System.Collections.IEnumerator }
+
+    let empty<'a, 'b> = Seq.empty<'a> |> ofSeq (fun x (y : 'b) -> false)
+
 module Dates =
     let InitInfinite (date : DateTime) =
         date |> Seq.unfold (fun d -> Some(d, d.AddDays 1.0))
@@ -30,24 +47,11 @@ module Dates =

 module Reservations =

-    type IReservations =
-        inherit seq<Envelope<Reservation>>
-        abstract Between : DateTime -> DateTime -> seq<Envelope<Reservation>>
-
-    type ReservationsInMemory(reservations) =
-        interface IReservations with
-            member this.Between min max =
-                reservations
-                |> Seq.filter (fun r -> min <= r.Item.Date && r.Item.Date <= max)
-            member this.GetEnumerator() =
-                reservations.GetEnumerator()
-            member this.GetEnumerator() =
-                (this :> seq<Envelope<Reservation>>).GetEnumerator() :> System.Collections.IEnumerator
-
-    let ToReservations reservations = ReservationsInMemory(reservations)
+    let ToReservations reservations =
+        reservations |> Drain.ofSeq (fun x y -> x.Item.Date >= fst y && x.Item.Date <= snd y)

-    let Between min max (reservations : IReservations) =
-        reservations.Between min max
+    let Between min max (reservations : IDrainable<Envelope<Reservation>, DateTime * DateTime>) =
+        reservations |> Drain.on(min, max)

     let On (date : DateTime) reservations =
         let min = date.Date
@@ -73,18 +77,8 @@ module Reservations =

 module Notifications =

-    type INotifications =
-        inherit seq<Envelope<Notification>>
-        abstract About : Guid -> seq<Envelope<Notification>>
-
-    type NotificationsInMemory(notifications : Envelope<Notification> seq) =
-        interface INotifications with
-            member this.About id =
-                notifications |> Seq.filter (fun n -> n.Item.About = id)
-            member this.GetEnumerator() = notifications.GetEnumerator()
-            member this.GetEnumerator() =
-                (this :> Envelope<Notification> seq).GetEnumerator() :> System.Collections.IEnumerator
-
-    let ToNotifications notifications = NotificationsInMemory(notifications)
-
-    let About id (notifications : INotifications) = notifications.About id
+    let ToNotifications notifications =
+        notifications |> Drain.ofSeq (fun x y -> x.Item.About = y)
+
+    let About id (notifications : IDrainable<Envelope<Notification>, Guid>) =
+        notifications |> Drain.on id
diff --git a/BookingApi/Controllers.fs b/BookingApi/Controllers.fs
index 2234ae0..986c5a9 100644
--- a/BookingApi/Controllers.fs
+++ b/BookingApi/Controllers.fs
@@ -41,7 +41,7 @@ type ReservationsController() =
         if disposing then subject.Dispose()
         base.Dispose disposing

-type NotificationsController(notifications : Notifications.INotifications) =
+type NotificationsController(notifications) =
     inherit ApiController()

     member this.Get id =
@@ -61,7 +61,7 @@ type NotificationsController(notifications : Notifications.INotifications) =

     member this.Notifications = notifications

-type AvailabilityController(reservations : Reservations.IReservations,
+type AvailabilityController(reservations,
                             seatingCapacity : int) =
     inherit ApiController()
diff --git a/BookingApi/Infrastructure.fs b/BookingApi/Infrastructure.fs
index 18867a8..7082d19 100644
--- a/BookingApi/Infrastructure.fs
+++ b/BookingApi/Infrastructure.fs
@@ -9,7 +9,7 @@ open System.Reactive
 open FSharp.Reactive
 open Ploeh.Samples.Booking.HttpApi.Reservations

-type CompositionRoot(reservations : IReservations,
+type CompositionRoot(reservations,
                      notifications,
                      reservationRequestObserver,
                      seatingCapacity) =
diff --git a/BookingApi.UnitTests/TestDsl.fs b/BookingApi.UnitTests/TestDsl.fs
index df09626..cde8e4d 100644
--- a/BookingApi.UnitTests/TestDsl.fs
+++ b/BookingApi.UnitTests/TestDsl.fs
@@ -37,19 +37,20 @@ type DateStringCustomization() =
                             (context.Resolve typeof<DateTime> :?> DateTime).ToString "yyyy.MM.dd" :> obj
                         | _ -> NoSpecimen(request) :> obj }

+open Ploeh.Samples.Booking.HttpApi
+open Ploeh.Samples.Booking.HttpApi.Notifications
+
 type NotificationsCustomization() =
     interface ICustomization with
         member this.Customize fixture =
-            fixture.Customizations.Add(
-                TypeRelay(
-                    typeof<Ploeh.Samples.Booking.HttpApi.Notifications.INotifications>,
-                    typeof<Ploeh.Samples.Booking.HttpApi.Notifications.NotificationsInMemory>))
-
+            let notifications =
+                fixture.CreateMany<Envelope<Notification>>() |> ToNotifications
+            fixture.Inject notifications
+
 type ReservationsCustomization() =
     interface ICustomization with
         member this.Customize fixture =
-            fixture.Inject<Ploeh.Samples.Booking.HttpApi.Reservations.IReservations>(
-                [] |> Ploeh.Samples.Booking.HttpApi.Reservations.ToReservations)
+            fixture.Inject Drain.empty<Envelope<Reservation>, DateTime * DateTime>

 type TestConventions() =
     inherit CompositeCustomization(
diff --git a/BookingApi.UnitTests/DomainModelTests.fs b/BookingApi.UnitTests/DomainModelTests.fs
index 221e560..c021706 100644
--- a/BookingApi.UnitTests/DomainModelTests.fs
+++ b/BookingApi.UnitTests/DomainModelTests.fs
@@ -92,14 +92,10 @@ module DatesTests =

 module ReserverationsTests =
     open Reservations
-
-    [<Theory; TestConventions>]
-    let ReservationsInMemoryAreReservations (sut : ReservationsInMemory) =
-        Assert.IsAssignableFrom<IReservations>(sut)

     [<Theory; TestConventions>]
     let ToReservationsReturnsCorrectResult (expected : Envelope<Reservation> seq) =
-        let actual : ReservationsInMemory = expected |> ToReservations
+        let actual = expected |> ToReservations
         Assert.Equal<Envelope<Reservation>>(expected, actual)

     [<Theory; TestConventions>]
@@ -111,7 +107,7 @@ module ReserverationsTests =
         let expected = reservations |> Seq.skip 2 |> Seq.take 6
         let sut = reservations |> ToReservations

-        let actual = (sut :> IReservations).Between min.Item.Date max.Item.Date
+        let actual = sut |> Drain.on(min.Item.Date, max.Item.Date)

         Assert.Equal<Envelope<Reservation>>(expected, actual)

@@ -125,7 +121,7 @@ module ReserverationsTests =

         let actual = sut |> Between min.Item.Date max.Item.Date

-        let expected = (sut :> IReservations).Between min.Item.Date max.Item.Date
+        let expected = sut |> Drain.on(min.Item.Date, max.Item.Date)
         Assert.Equal<Envelope<Reservation>>(expected, actual)

     [<Theory; TestConventions>]
@@ -203,12 +199,8 @@ module NotificationsTest =
     open Notifications

     [<Theory; TestConventions>]
-    let NotificationsInMemoryAreNotifications (sut : NotificationsInMemory) =
-        Assert.IsAssignableFrom<INotifications> sut
-
-    [<Theory; TestConventions>]
     let ToNotificationsReturnsCorrectResult (expected : Envelope<Notification> seq) =
-        let actual : NotificationsInMemory = expected |> ToNotifications
+        let actual = expected |> ToNotifications
         Assert.Equal<Envelope<Notification>>(expected, actual)

     [<Theory; TestConventions>]
@@ -219,7 +211,7 @@ module NotificationsTest =
         let expected = notifications |> PickRandom
         let sut = notifications |> ToNotifications

-        let actual = (sut :> INotifications).About expected.Item.About
+        let actual = sut |> Drain.on expected.Item.About

         Assert.Equal(1, actual |> Seq.length)
         Assert.Equal(expected, actual |> Seq.head)
@@ -232,7 +224,7 @@ module NotificationsTest =
         let sut = generator |> Seq.take 10 |> Seq.toList |> ToNotifications
         Assert.False(sut |> Seq.exists (fun n -> n.Item.About = about))

-        let actual = (sut :> INotifications).About about
+        let actual = sut |> Drain.on about

         Assert.True(actual |> Seq.isEmpty)

@@ -241,7 +233,7 @@ module NotificationsTest =
         let sut = notifications |> ToNotifications
         let about = (sut |> Seq.toList |> PickRandom).Item.About

-        let actual : Envelope<Notification> seq = sut |> About about
+        let actual = sut |> About about

-        let expected = (sut :> INotifications).About about
+        let expected = sut |> Drain.on about
         Assert.Equal<Envelope<Notification>>(expected, actual)
diff --git a/BookingApi.UnitTests/ControllerTests.fs b/BookingApi.UnitTests/ControllerTests.fs
index 166278e..05f9363 100644
--- a/BookingApi.UnitTests/ControllerTests.fs
+++ b/BookingApi.UnitTests/ControllerTests.fs
@@ -79,11 +79,11 @@ module NotificationsControllerTests =

     [<Theory; TestConventions>]
     let NotificationsAreExposedForExpection
-        ([<Frozen>]expected : Notifications.INotifications)
+        ([<Frozen>]expected : IDrainable<Envelope<Notification>, Guid>)
         (sut : NotificationsController) =

-        let actual : Notifications.INotifications = sut.Notifications
-        Assert.Equal<Notifications.INotifications>(expected, actual)
+        let actual = sut.Notifications
+        Assert.Equal<IDrainable<Envelope<Notification>, Guid>>(expected, actual)

     [<Theory; TestConventions>]
     let GetWithoutMatchingNotificationReturnsCorrectResult
@@ -187,7 +187,7 @@ module AvailabilityControllerTests =
                                                     yearsInFuture : int) =
         // Fixture setup
         let reservations = mutableReservations |> Reservations.ToReservations
-        fixture.Inject<Reservations.IReservations> reservations
+        fixture.Inject reservations
         let sut =
             fixture.Generate<AvailabilityController>()
             |> Seq.filter (fun c -> c.SeatingCapacity > 1)
@@ -284,7 +284,7 @@ module AvailabilityControllerTests =
                                                      yearsInFuture : int) =
         // Fixture setup
         let reservations = mutableReservations |> Reservations.ToReservations
-        fixture.Inject<Reservations.IReservations> reservations
+        fixture.Inject reservations
         let sut =
             fixture.Generate<AvailabilityController>()
             |> Seq.filter (fun c -> c.SeatingCapacity > 1)
@@ -395,7 +395,7 @@ module AvailabilityControllerTests =
                                                    yearsInFuture : int) =
         // Fixture setup
         let reservations = mutableReservations |> Reservations.ToReservations
-        fixture.Inject<Reservations.IReservations> reservations
+        fixture.Inject reservations
         let sut =
             fixture.Generate<AvailabilityController>()
             |> Seq.filter (fun c -> c.SeatingCapacity > 1)

To apply the diff, access the source code by getting a Pluralsight subscription which is totally worth it for this course.