At its core, Simpler is a philosophy on or a pattern for C# class design. Simpler can help developers—especially teams of developers—build complex projects using consistent, readable classes that easily integrate with each other.
- Eliminates the need to discuss and decide on class design
- Makes the code more understandable, consistent, and readable
- Simplifies writing tests
- Provides a cleaner method of addressing cross-cutting concerns
- Simplifies maintenance by making it easier to find business logic
In the traditional Object-oriented Programming (OOP) approach, classes define objects (named with nouns) and include data and business logic (methods named with verbs).
But when an application needs to interact with several classes, it isn’t always clear where that business logic should be stored. So “manager” or “service” classes are often created for all of the business logic affecting multiple object classes.
With OOP, developers must constantly make decisions about where to place new business logic, a problem which is exacerbated by complex projects or when working with a team. Even with careful class design, it’s not always obvious where to find particular business logic, especially when maintaining code.
However, with Simpler, data and business logic are divided into small, discrete “building blocks.” Although you’ll have a lot of small classes—instead of a few, very large classes—these classes can easily integrate with each other without “manager” or “service” classes.
Simpler also separates data from business logic, with data defined in Model classes and business logic in Task classes.
To make finding the Tasks and Models easy, you can organize them within directories.
Model classes are typically just plain old CLR objects (POCOs) and only contain properties. But a Task does things; each Task class is the equivalent of a discrete action. Simpler provides functionality for Tasks.
Use Nuget.
Using Simpler is, well, simple.
- Create a Task class.
- Instantiate the Task using the Task.New() method.
- Execute the task.
Simpler also provides some additional functionality:
- From within a Task, execute other Tasks (sub-tasks)
- Perform actions before or after execute, which is especially useful for addressing cross-cutting concerns such as logging
- Use
Statsfor profiling or for testing scenarios based on a Task executing or executing within a duration - Use
Namefor getting the name of the Task class, which is often useful for logging Task information - When testing a Task with sub-tasks, isolate the Task logic by mocking the sub-tasks
You should create a new Task when you will be writing enough business logic to warrant tests. For example, if logic can be done in one line or using LINQ, there’s no reason to create a Task just for that.
If an existing Task grows in size over time and becomes hard to test, consider refactoring it into multiple Tasks. As a general rule, try to keep Tasks under 100 lines of code.
When you create a Task, you should name it so that everyone can easily identify what the Task does. Follow these naming rules:
- Begin each Task name with a verb (because the Task is action, it’s doing something)
- Clearly state the Task’s purpose in the name
Example: A Task that parses an JSON file containing teams might be called ParseTeamsJson. But it shouldn’t be called JsonTeamsParser or JsonTeams.
Simpler provides 3 base classes for defining Tasks:
InTask(applies business logic to an input but doesn’t produce any output)OutTask(produces output using business logic but does not accept any input)InOutTask(applies business logic to an input and produces an output)
Simpler also includes a Task base class. This Task base class
- Includes the static
Task.New<TTask>()method, which is a factory method for instantiating tasks - Enables you to create a Task that has no inputs or outputs
In addition, all Tasks inherit the Name property and the Stats property from this base class.
An InTask applies business logic to an input but doesn’t produce an output. For example, an InTask might receive an input of information and write it to the console.
For an InTask, you must enter a generic parameter type that defines the type of input. This input is exposed to the Execute() method through the In property.
To make the In property a container for all input, you can define an Input class inside the InTask. This Input class contains the input as properties and is passed to the InTask as the generic parameter type.
public class OutputStat: InTask<OutputStat.Input>
{
public class Input
{
public Stat Stat { get; set; }
}
public override void Execute()
{
var builder = new StringBuilder();
builder.Append(String.Format("Question: {0}", In.Stat.Question));
builder.Append(Environment.NewLine);
builder.Append(String.Format(" Answer: {0}", In.Stat.Answer));
builder.Append(Environment.NewLine);
builder.Append(String.Format(" Reason: {0}", In.Stat.Details));
Console.WriteLine(builder);
}
}An OutTask has no input, but it uses business logic to produce an output. For example, an OutTask might load a set of data and make the results available.
For an OutTask, you must enter a generic parameter type that defines the type of output. This output is exposed in the Execute() method through the Out property.
To make the Out property a container for all output, you can define an Output class inside the OutTask. This Output class contains the output as properties and is passed to the OutTask as the generic parameter type.
public class FetchTeams: OutTask<FetchTeams.Output>
{
public class Output
{
public Team[] Teams { get; set; }
}
public override void Execute()
{
var teams = new List<Team>();
dynamic baseball = Config.FromFile("examples.json");
foreach (dynamic team in baseball.Teams)
{
teams.Add(new Team {
League = team.league,
Division = team.division,
Name = team.name,
FirstSeason = team.since,
G = Int64.Parse((string)team.g, NumberStyles.AllowThousands),
W = Int64.Parse((string)team.w, NumberStyles.AllowThousands),
Pennants = team.pennants,
WorldSeries = team.worldSeries,
Playoffs = team.playoffs,
R = Int64.Parse((string)team.r, NumberStyles.AllowThousands),
AB = Int64.Parse((string)team.ab, NumberStyles.AllowThousands),
H = Int64.Parse((string)team.h, NumberStyles.AllowThousands),
HR = Int64.Parse((string)team.hr, NumberStyles.AllowThousands),
BA = team.ba,
RA = Int64.Parse((string)team.ra, NumberStyles.AllowThousands),
Era = team.era
});
}
Out.Teams = teams.ToArray();
}
}An InOutTask is a combination of an InTask and an OutTask. An InOutTask applies business logic to an input and produces an output. For example, an InOutTask might answer a question by taking some information and using it to produce an answer.
For an InOutTask, you must enter 2 generic parameter types: one for the type of input and one for the type of output. As with an InTask and an OutTask, these parameters are exposed in the Execute() method through the In and Out properties respectively. You can also define Input and Output classes inside the InOutTask. Refer to InTask and OutTask for additional information.
public class FindBestTeam: InOutTask<FindBestTeam.Input, FindBestTeam.Output>
{
public class Input
{
public Team[] Teams { get; set; }
}
public class Output
{
public Team BestTeam { get; set; }
public double WorldSeriesPercent { get; set; }
}
public override void Execute()
{
var wonWorldSeries = In.Teams.Select(t => new {
Team = t,
Percent = t.WorldSeries != 0
? Math.Round(t.WorldSeries / (double)(t.Age) * 100, 2)
: 0
});
var best = wonWorldSeries.OrderByDescending(t => t.Percent).First();
Out.BestTeam = best.Team;
Out.WorldSeriesPercent = best.Percent;
}
}When you have created a Task, you can instantiate it using the Task.New() method.
Note: Task.New() appears to return an instance of the Task. However, it actually returns a proxy of the Task. This proxy allows Simpler to intercept Task Execute() calls to perform actions before and/or after the Task executes using the EventsAttribute.
Do not use the Task.New() method to instantiate a Task from within another Task. Instead, use sub-task injection.
public class Program
{
static int Main(string[] args)
{
var outputBestTeams = Task.New<OutputBestTeams>();
outputBestTeams.Execute();
return 0;
}
}With Simpler, a Task contains the smallest piece of useable functionality. Therefore, you’ll often need a Task to execute other Tasks, referenced as sub-tasks, which creates a dependency between the Tasks. To prevent tight coupling between the dependencies, Simpler provides automatic sub-task injection.
Note: Sub-task injection supports injecting dependencies at runtime. This type of injection is typically used for testing purposes.
To inject sub-tasks within a Task class, simply define the sub-tasks as properties. Any Task can be referenced as a sub-task. A sub-task is no different from a normal Task—it’s only called a sub-task when it’s defined as a property on another Task.
Before executing a Task, Simpler checks whether the Task has any sub-tasks. If so, Simpler automatically creates the sub-tasks and injects them into the Task properties.
[Log]
public class OutputBestTeams: Task
{
public FetchTeams FetchTeams { get; set; }
public FindBestTeam FindBestTeam { get; set; }
public OutputStat OutputStat { get; set; }
public override void Execute()
{
FetchTeams.Execute();
var allTeams = FetchTeams.Out.Teams;
var divisions = new[] {
Filter(allTeams, "American", "East"),
Filter(allTeams, "American", "Central"),
Filter(allTeams, "American", "West"),
Filter(allTeams, "National", "East"),
Filter(allTeams, "National", "Central"),
Filter(allTeams, "National", "West")
};
foreach (var division in divisions)
{
FindBestTeam.In.Teams = division.Teams;
FindBestTeam.Execute();
var team = FindBestTeam.Out.BestTeam;
var percent = FindBestTeam.Out.WorldSeriesPercent;
OutputStat.In.Stat = new Stat {
Question = String.Format(Question, division.Name),
Answer = team.Name,
Details = String.Format(Details, team.WorldSeries, percent, team.Age),
};
OutputStat.Execute();
}
}
const string Question = "Who is {0}'s best team?";
const string Details = "They've won the World Series {0} times ({1}%) in their {2} years.";
#region Helpers
static dynamic Filter(IEnumerable<Team> allTeams, string league, string division)
{
return new {
Name = String.Format("{0} League {1}", league, division),
Teams = allTeams.Where(t => t.League == league && t.Division == division).ToArray()
};
}
#endregion
}When you use the Task.New<TTask>() method, it returns a proxy to the Task, which enables Simpler to intercept Task Execute() calls using the EventsAttribute. When the Execute() call is intercepted, Simpler can perform actions before or after the Task executes or when the Task errors.
This intercepting is especially useful for addressing cross-cutting concerns, such as logging, as the EventsAttribute can be sub-classed and easily applied to Tasks.
The following example shows a custom EventsAttribute for logging Task activity.
public class LogAttribute : EventsAttribute
{
public override void BeforeExecute(Task task)
{
if (Enabled) Console.WriteLine("{0} started.", task.Name);
}
public override void AfterExecute(Task task)
{
if (Enabled) Console.WriteLine("{0} finished.", task.Name);
}
public override void OnError(Task task, Exception exception)
{
if (Enabled) Console.WriteLine("{0} bombed! Details: {1}.", task.Name, exception);
}
public static bool Enabled = true;
}This LogAttribute can be applied to any Task to add logging functionality. You might have noticed the OutputBestTeams Task in the sub-task injection example has the Log attribute. The OutputBestTeams output will be wrapped with "started" and "finished" log entries if OutputBestTeams is executed and LogAttribute.Enabled == true.
Examples.Tasks.OutputBestTeams started.
Question: Who is American League East's best team?
Answer: New York Yankees
Reason: They've won the World Series 27 times (23.89%) in their 113 years.
Question: Who is American League Central's best team?
Answer: Detroit Tigers
Reason: They've won the World Series 4 times (3.54%) in their 113 years.
Question: Who is American League West's best team?
Answer: Oakland Athletics
Reason: They've won the World Series 9 times (7.96%) in their 113 years.
Question: Who is National League East's best team?
Answer: Miami Marlins
Reason: They've won the World Series 2 times (9.52%) in their 21 years.
Question: Who is National League Central's best team?
Answer: St. Louis Cardinals
Reason: They've won the World Series 11 times (8.33%) in their 132 years.
Question: Who is National League West's best team?
Answer: Arizona Diamondbacks
Reason: They've won the World Series 1 times (6.25%) in their 16 years.
Examples.Tasks.OutputBestTeams finished.
The Stats and Name properties, which all Tasks inherit from the Task base class, are typically used in testing or logging.
A Task's Stats property tracks how many times the Task is executed and the execute durations. Stats are useful for profiling as well as for testing scenarios when you need to assert that a Task or sub-task has been executed as expected.
Refer to the Mocking example to see the Stats property in use.
Because Task.New<TTask>() returns a proxy to the Task it's GetType().Name property will be the proxy class, which isn't useful. To get a read-only name based on the Task class itself, use the Name property.
Refer to the EventsAttribute example to see the Name property in use.
By design, Simpler forces you to create code that is easy to test. Each Task clearly defines its inputs, outputs, and the discrete code to test, so writing a test is typically straightforward.
Simpler also includes functionality to make writing tests easier:
- If your Task includes sub-tasks, isolate the logic of the Task being tested by mocking the sub-task behavior using
Fake.Task<TTask>(). - Use the
Statsproperty to test scenarios such as asserting that a Task has executed as expected or within a expected duration. - Use the
Nameproperty to get a read-only name of the Task class.
[TestFixture]
public class FindBestTeamTest
{
[Test]
public void picks_team_with_highest_World_Series_percentage()
{
var findBestTeam = Task.New<FindBestTeam>();
findBestTeam.In.Teams = new[] {
new Team {
Name = "Good Team",
FirstSeason = 2010,
WorldSeries = 1
},
new Team {
Name = "Better Team",
FirstSeason = 2010,
WorldSeries = 3
}
};
findBestTeam.Execute();
Assert.That(findBestTeam.Out.BestTeam.Name, Is.EqualTo("Better Team"));
Assert.That(findBestTeam.Out.WorldSeriesPercent, Is.EqualTo(75));
}
}When writing a test for a Task with an injected sub-task, you may want to isolate the test to only the Task’s business logic. Using Fake.Task<TTask>(), you override the sub-task’s Execute() logic and replace it with any logic necessary to serve the need of the test. For example, you might choose to skip writing to disk in your test by faking the responsible sub-task.
If the Task has many sub-tasks, it’s easier to begin with a call to Fake.SubTasks() and then fake the individual tasks that are needed for the specific test scenario.
[TestFixture]
public class OutputBestTeamsTest
{
[SetUp]
public void DisableLogging() { LogAttribute.Enabled = false; }
[Test]
public void outputs_best_teams_in_each_division()
{
var stats = new List<Stat>();
var storeStats = Fake.Task<OutputStat>(os => stats.Add(os.In.Stat));
var outputsBestTeams = Task.New<OutputBestTeams>();
outputsBestTeams.OutputStat = storeStats;
outputsBestTeams.Execute();
var questions = String.Join("|", stats.Select(s => s.Question));
Assert.That(questions.Contains("American League East"));
Assert.That(questions.Contains("American League Central"));
Assert.That(questions.Contains("American League West"));
Assert.That(questions.Contains("National League East"));
Assert.That(questions.Contains("National League Central"));
Assert.That(questions.Contains("National League West"));
}
[Test]
public void runs_under_1_second()
{
var skipOutput = Fake.Task<OutputStat>();
var outputsBestTeams = Task.New<OutputBestTeams>();
outputsBestTeams.OutputStat = skipOutput;
outputsBestTeams.Execute();
var seconds = outputsBestTeams.Stats.ExecuteDurations.Max(ed => ed.TotalSeconds);
Assert.That(seconds, Is.LessThan(1));
}
}See Contributing.
Simpler is licensed under the MIT License. See License.
The following individuals are in the Simpler Hall of Fame.
- bobnigh (pre, betas)
- Clancey (contributor, betas)
- corys (betas)
- Crosis (betas)
- danvanorden (betas)
- dchristine (betas)
- jkettell (pre, betas)
- JOrley (betas)
- jshoemaker (pre)
- kamillf (contributor)
- Pete Rose (4,256 hits)
- ralreegorganon (contributor)
- rlgnak (contributor, betas)
- rodel-rdi (betas)
- sonhuilamson (betas)
- timrisi (betas)
pre - Worked on a project that inspired Simpler, which included a Task class with sub-task injection. bobnigh was a co-author of the original Task class.
betas - Provided feedback on Simpler 1 or later beta versions of Simpler.
contributor - Has contributed to Simpler.

