A Fluent API Design Pattern (Part 2)

Download source code for this sample

In the first post of this 2 part series, we looked at a class named Syntax<T> designed to be a pattern allowing for the easy creation and maintenance of a Fluent API.  In the second post, we’ll explore what it takes to extend and implement that pattern. Recalling the example set forth in the first post, in this post we're going to create a fluent API for building a schedule type consumed like this.

// Starting a job 10 minutes from now.
var schedule = Schedule.Create()
	.StartAt(10.Minutes().From(now))
	.ToSchedule();
// Repeat a job infinately every 10 seconds and retrieve the next instance.
var schedule = Schedule.Create()
	.StartAt(now)
	.Repeat()
	.Every(10.Seconds())
	.Next()
	.ToSchedule();
// Conditional scheduling, even when contradicting.
var schedule = Schedule.Create()   		// Create a schedule
	.StartAt(now)						// Start it now
	.Repeat()							// Repeat it
	.Every(2.Seconds())					// Every 2 seconds
	.While(s => s.OccurrenceNumber < 3) // Up to 2 occurrences
	.Take(3)							// But take the first 3 occurrences :)
	.ToSchedule();

There are 3 classes at work that make this possible.  The first is the Syntax<T> class, the second and third are Schedule, and ScheduleExtensions a static class with extension methods for Syntax<Schedule>.

Schedule Class Diagram

Breaking down Schedule.cs

Schedule.cs contains a class that navigates a schedule and can retrieve dates for any occurrence.  We want our consumer to create a schedule using a static factory method, and we want to seed each schedule at the first occurrence.

/// <summary>
/// Constructs a new instance of <see cref="Schedule"/>.
/// </summary>
/// <remarks>
/// This method is marked protected as we want consumers to use the factory method below.
/// </remarks>
protected Schedule()
{
	this.OccurrenceNumber = 1;
}

/// <summary>
/// Static factory method creates a new schedule class for use in method chaining.
/// </summary>
/// <returns></returns>
public static Syntax<Schedule> Create()
{
	return Syntax.For(new Schedule());
}

We then seal many of the properties inside Schedule that are used to maintain it’s state.

/// <summary>
/// Time span between occurrences (if this is a schedule for a repeating event).
/// </summary>
protected internal virtual TimeSpan Interval { get; set; }

/// <summary>
/// Predicate function that indicates how to limit scheduled occurrences.
/// </summary>
protected internal virtual Predicate<Schedule> LimitPredicate { get; set; }

/// <summary>
/// The maximum number of occurrences of this schedule for a repeating event.
/// </summary>
protected internal virtual Int32? MaximumOccurrences { get; set; }

/// <summary>
/// An internal value that holds the next scheduled run for a repeating event.
/// </summary>
protected internal virtual DateTime? NextRunValue { get; set; }

/// <summary>
/// Enumerated value indiciating if this is a one time event or recurring event.
/// </summary>
protected internal virtual Occurrence Occurrence { get; set; }

/// <summary>
/// Seed date as to when the event should start.
/// </summary>
protected internal virtual DateTime? Seed { get; set; }

The remaining properties are public and readonly, and provide information to the consumer as to the location and date/time of any given occurrence of a schedule.  They are as follows:

#region Calculated 

/// <summary>
/// Boolean indicating if the Skip(), Take(), Next(), Previous() and MoveTo() operations can be used.
/// </summary>
public bool CanNavigate
{
	get
	{
		return ((this.Occurrence == Occurrence.OneTime && !this.NextRunValue.HasValue) || this.Occurrence == Occurrence.Repeat)
			&& (this.LimitPredicate == null || this.LimitPredicate(this))
			&& (!this.MaximumOccurrences.HasValue || this.MaximumOccurrences.Value <= this.OccurrenceNumber);
	}
}

/// <summary>
/// Indicates if a repeating schedule has started.
/// </summary>
public bool HasStarted
{
	get
	{
		return this.NextRunValue.HasValue;
	}
}

/// <summary>
/// Gets the next scheduled time in the schedule pattern.
/// </summary>
public DateTime NextRunTime
{
	get 
	{ 
		return this.NextRunValue.HasValue 
			? this.NextRunValue.Value 
			: CalculateNextRun(this, this.OccurrenceNumber); 
	}
}

#endregion

#region Public

/// <summary>
/// Gets or sets the current occurance number for the given schedule.
/// </summary>
public Int32 OccurrenceNumber { get; protected internal set; }

#endregion

The remaining portion of this class simply calculates a single occurrence from the schedule and navigates appropriately.  The good stuff in this design is what happens with the extensions.

ScheduleExtensions.cs

This class contains all of the public API for creating and navigating a schedule.  Public consumers will leverage methods here, instead of those in Schedule.cs.  Each method extends Syntax<Schedule> and returns Syntax<Schedule>.  Here is an example:

public static Syntax<Schedule> Every(this Syntax<Schedule> schedule, TimeSpan span)
{
	schedule.Value.Interval = span;

	return schedule;
}

The extensions provided in this sample are as follows:

Repeat() Indicates the schedule should have more than one occurrence
Once() Indicates the schedule should have one occurrence, default behavior
Every(TimeSpan) Indicates how frequently the schedule should occur
StartAt(DateTime) Indicates when the scheduled event should begin
While(Predicate<Schedule>) Conditional evaluation indicating if an occurrence should occur
Maximum(Int32) Indicates the maximum number of occurrences
MoveTo(Int32) Move to the specified occurrence
Next() Move to the next occurrence
Previous() Move to the previous occurrence
Skip() Skip 1 occurrence, but keep the sequencing of occurrences in tact
Skip(Int32) Skip X occurrences, but keep the sequencing of occurrences in tact
Take(Int32) Same as calling Next() X times
ToSchedule() Takes Syntax<Schedule> and returns Schedule.
Modify() Takes Schedule and returns Syntax<Schedule>


There are also a number of extension methods that are provided for other types to provide fluency.

Weeks() Extends Int32 and returns a TimeSpan for X weeks
Days() Extends Int32 and returns a TimeSpan for X days
Hours() Extends Int32 and returns a TimeSpan for X hours
Minutes() Extends Int32 and returns a TimeSpan for X minutes
Seconds() Extends Int32 and returns a TimeSpan for X seconds
From(DateTime) Extends TimeSpan and returns a date that adds the specified timespan

The resulting API

The resulting API is a fluent syntax for managing a schedule of recurring events.  Here’s an example of how to navigate occurrences using the fluent schedule API.

var now = DateTime.Now;
var schedule = Schedule.Create()
	.StartAt(now)
	.Repeat()
	.Every(10.Seconds())
	.Next()
	.ToSchedule();

Assert.AreEqual(now.AddSeconds(10), schedule.NextRunTime);
Assert.AreEqual(2, schedule.OccurrenceNumber);

schedule.Previous();

Assert.AreEqual(now, schedule.NextRunTime);
Assert.AreEqual(1, schedule.OccurrenceNumber);

schedule.Previous();

Assert.AreEqual(now, schedule.NextRunTime);
Assert.AreEqual(1, schedule.OccurrenceNumber);

schedule.MoveTo(3);

Assert.AreEqual(now.AddSeconds(20), schedule.NextRunTime);
Assert.AreEqual(3, schedule.OccurrenceNumber);

And as an additional sample, here’s what it looks like to set and then modify a schedule:

var now = DateTime.Now;
var schedule = Schedule.Create()
	.StartAt(2.Minutes().From(now))
	.ToSchedule();

Assert.AreEqual(now.AddMinutes(2), schedule.NextRunTime);
Assert.AreEqual(1, schedule.OccurrenceNumber);

// Modify the schedule and get the next occurrence.  Should be the same, since
// we didn't tell our schedule to repeat.
schedule.Modify().Next();

Assert.AreEqual(now.AddMinutes(2), schedule.NextRunTime);
Assert.AreEqual(1, schedule.OccurrenceNumber);

// Modify the schedule and make it repeat every 2 minutes, then get the next occurance.
schedule.Modify()
	.Repeat()
	.Every(2.Minutes())
	.Next();

Assert.AreEqual(now.AddMinutes(4), schedule.NextRunTime);
Assert.AreEqual(2, schedule.OccurrenceNumber);

Wrap Up

With this simple example, we were able to show how to use a generic Syntax<T> class to create a fluent API with ease.  Although this is just an example, and not production ready, it’s my hope that this pattern can be used in a number of other scenarios.  The complete source code for this sample is available for download at the link below.

Download source code for this sample

Pingbacks and trackbacks (1)+

Comments are closed

About the author

I'm your host, Brandon Kelly.  I'd like to thank you for taking the time to read some of my thoughts on software develoment, the technology industry and the Florida developer community.

Month List

On My Blog

Recent Comments

Comment RSS