As a user I would like to be able to search for candidates
Given the user is signed-in
Given the user has subscription
Then return Non-anonymized search results
Given the user is signed-in
Given the user does not have a subscription
Then return anonymized search results
Given the user is signed-out
Then return signed-out search results
It is obvious that there are three different behaviours depending on whether or not the user is signed in and has a valid subscription.
if we were to write all this functionality into a single object we would be breaking the single responsibility principle and would have a number of if and else statements, increasing the cyclomatic and maintenance complexities of our code. so I set out to apply the single responsibility principle, and the first I took was to define the responsibilities of my objects. from the above user story we can see that there are three different search behaviours and therefore we could easily create three objects that represent each search behaviour.
first I created a "role interface" for my strategies like so:
public interface ISearchStrategy
{
Task<ResultModel> SearchFor(SearchCriteria criteria);
}
Then I started implementing the search strategies for each user story scenario like so, this was all done using tdd but I have omited the tests and implementations as I am trying to demonstrate the use of design patterns:
"then return Non-anonymized search results" :
public class NonAnonymizedSearchStrategy:ISearchStrategy
{
private readonly IQuery query;
public NonAnonymizedSearchStrategy(IQuery Query)
{
this.query = Query;
}
public async Task<ResultModel> SearchFor(SearchCriteria criteria)
{
}
}
"Then return anonymized search results" :
public class AnonymizedSearchStrategy:ISearchStrategy
{
private readonly IQuery query;
public AnonymizedSearchStrategy(IQuery Query)
{
this.query = Query;
}
public async Task<ResultModel> SearchFor(SearchCriteria criteria)
{
}
"Then return signed-out search results" :
public class SignedOutSearchStrategy:ISearchStrategy
{
private readonly IQuery Query;
public SignedOutSearchStrategy(IQuery Query)
{
this.Query = Query;
}
public async Task<ResultModel> SearchFor(SearchCriteria criteria)
{
}
}
Now that we have our behaviours, policies or strategies as they are more popularly called defined, we need to come up with a way of instantiating the right strategy at runtime, this is were the factory pattern is used.
public class SearchStrategyFactory:ISearchStrategyFactory
{
private readonly ICurrentUserService currentUserService;
private readonly IQuery query;
public SearchStrategyFactory(ICurrentUserService currentUserService, IQuery Query)
{
this.currentUserService = currentUserService;
this.query = Query;
}
public ISearchStrategy Create()
{
if (currentUserService.IsSignedIn == false)
{
return new SignedOutSearchStrategy(query);
}
if (currentUserService.HasSubscription)
{
return new NonAnonymizedSearchStrategy(query);
}
return new AnonymizedSearchStrategy(query);
}
}
I used convention over configuration to register all our factories that are appended by the word "Factory" like so in the composition root:
container.Register(
Classes
.FromAssemblyContaining<CandidateService>()
.Where(t => t.Name.EndsWith("Factory"))
.WithService
.AllInterfaces().LifestylePerWebRequest());
and now I am able to inject my factory into my application service and use it like so:
public class SearchService:ISearchService
{
private readonly ISearchStrategyFactory searchStrategyFactory;
public SearchService(ISearchStrategyFactory searchStrategyFactory)
{
this.searchStrategyFactory = searchStrategyFactory;
}
public Task<ResultModel> SearchFor(SearchCriteria criteria)
{
return searchStrategyFactory.Create().SearchFor(criteria);
}
}
No comments :
Post a Comment