One of the applications I work on is a planning system, used for managing the operations of the business over the next week, month and financial year.
Almost every entity in this application has a fixed ‘applicable period’ — a lifetime that begins and ends at certain dates. For example:
- An employee’s applicable period lasts as long as they are employed
- A business unit’s lifetime lasts from the day it’s formed, to the day it disbands
- A policy lasts from the day it comes into effect to the day it ends
- A shift starts at 8am and finishes at 6pm
Previous incarnations of the application simply added StartDate and EndDate properties to every object, and evaluated them ad-hoc as required. This resulted in a lot of code duplication — date and time logic around overlaps, contiguous blocks etc were repeated all over the place.
As we’ve been carving off bounded contexts and reimplementing them using DDD, I’m proud to say this concept has been identified and separated out into an explicit value type with encapsulated behaviour. We call it a Time Period:
It’s sort of like a .NET TimeSpan but represents a specific period of time, e.g. seven days starting from yesterday morning — not seven days in general.
Here’s the behaviour we’ve implemented so far, taking care of things like comparisons and overlapping periods:
/// <summary>/// A value type to represent a period of time with known end points (as/// opposed to just a period like a timespan that could happen anytime)./// The end point of a TimeRange can be infinity. /// </summary>public class TimePeriod : IEquatable<TimePeriod>{ public DateTime Start { get; } public DateTime? End { get; } public bool IsInfinite { get; } public TimeSpan Duration { get; } public bool Includes(DateTime date); public bool StartsBefore(TimePeriod other); public bool StartsAfter(TimePeriod other); public bool EndsBefore(TimePeriod other); public bool EndsAfter(TimePeriod other); public bool ImmediatelyPrecedes(TimePeriod other); public bool ImmediatelyFollows(TimePeriod other); public bool Overlaps(TimePeriod other); public TimePeriod GetRemainingSlice(); public TimePeriod GetRemainingSliceAsAt(DateTime when); public bool HasPassed(); public bool HasPassedAsAt(DateTime when); public float GetPercentageElapsed(); public float GetPercentageElapsedAsAt(DateTime when);}
Encapsulating logic all in one place means we can get rid of all that ugly duplication (DRY), and it still maps cleanly to StartDate/EndDate columns in the database as an NHibernate component or IValueType.
You can grab our initial implementation here:
- TimePeriod.cs
- TimePeriodTests.cs