简体   繁体   中英

Factory Pattern with Generic Type Constraints

I am implementing a type of factory pattern and found this neat-looking pattern on Code Review.

I've implemented this solution with some variations as follows:

I have a factory class which looks like this:

public class SearchableServiceFactory<TSearchableLookupService, TOutputDto>
    where TOutputDto : IBaseOutputDto
    where TSearchableLookupService : ISearchableLookupService<TOutputDto>
{
    static readonly Dictionary<string, Func<TSearchableLookupService>> _SearchableLookupServicesRegistry =
        new Dictionary<string, Func<TSearchableLookupService>>();

    private SearchableServiceFactory() { }

    public static TSearchableLookupService Create(string key)
    {
        if (_SearchableLookupServicesRegistry.TryGetValue(
            key, out Func<TSearchableLookupService> searchableServiceConstructor)
        )
            return searchableServiceConstructor();

        throw new NotImplementedException();
    }

    public static void Register<TDerivedSearchableService>
    (
        string key,
        Func<TSearchableLookupService> searchableServiceConstructor
    )
        where TDerivedSearchableService : TSearchableLookupService
    {
        var serviceType = typeof(TDerivedSearchableService);

        if (serviceType.IsInterface || serviceType.IsAbstract)
            throw new NotImplementedException();

        _SearchableLookupServicesRegistry.Add(key, searchableServiceConstructor);
    }

That works. I call it from code, thus:

...
SearchableServiceFactory<OrgLookupService, OrgOutputDto>.Register<OrgLookupService>
(
    nameof(Organization), () => new OrgLookupService(_Context, _OrganizationRepository)
);
...

That works. A constructor is added to the dictionary, along with a key. Then I go to retrieve that constructor by key, to get an instance and do something with it, like so:

SearchableServiceFactory<ISearchableLookupService<IBaseOutputDto>, IBaseOutputDto>.Create(myKey).DoAThing();

That fails because no such value exists in the dictionary. Because it's static, as are the methods in the class that register and create the instances I need.

I'm using .NET Core 2.1, if that matters (this seems like a strictly C# issue).

SearchableServiceFactory<OrgLookupService, OrgOutputDto> is not the same type as SearchableServiceFactory<ISearchableLookupService<IBaseOutputDto>, IBaseOutputDto> , and as such, even the static properties are different.

They are different types in the eyes of the compiler. Just because OrglookupService is a ISearchableLookupService , not every ISearchableLookupService is a OrglookupService .

A possible workaround would be to use SearchableServiceFactory<ISearchableLookupService<IBaseOutputDto>, IBaseOutputDto> to register your object, but that would require the ISearchableLookupService to be covariant.

public interface ISearchableLookupService<out TOutputDto> 
    where TOutputDto : IBaseOutputDto
{

}

And register like this:

SearchableServiceFactory<ISearchableLookupService<IBaseOutputDto>, IBaseOutputDto>.Register<OrgLookupService>
(
    nameof(Organization), () => new OrgLookupService()
);

The class SearchableServiceFactory<A, B> is not the same class as
SearchableServiceFactory<X, Y> . Therefore, you are dealing with two distinct sets of static members. Particularly, you have two different dictionaries _SearchableLookupServicesRegistry .

You are registering into one of them (in SearchableServiceFactory<OrgLookupService, OrgOutputDto> ). The other one (in
SearchableServiceFactory<ISearchableLookupService<IBaseOutputDto>, IBaseOutputDto> ) remains empty, but you are trying to use it for the retrieval of the constructor. If you set a breakpoint on the throw statement in Create and inspect _SearchableLookupServicesRegistry , you will see that its Count is 0 .

The problem with generics is that they may appear to offer some dynamic behavior, but they do not. All generic type parameters are determined a compile time. Complex scenarios using generics often tend to become highly convoluted. If you need to be highly dynamic, you must sometimes give up full type safety.

This is my suggestion for a service factory:

public static class SearchableServiceFactory
{
    static readonly Dictionary<string, Func<ISearchableLookupService<IBaseOutputDto>>> 
        _SearchableLookupServicesRegistry =
            new Dictionary<string, Func<ISearchableLookupService<IBaseOutputDto>>>();

    public static TSearchableLookupService Create<TSearchableLookupService>(string key)
        where TSearchableLookupService : ISearchableLookupService<IBaseOutputDto>
    {
        if (_SearchableLookupServicesRegistry.TryGetValue(
            key,
            out Func<ISearchableLookupService<IBaseOutputDto>> searchableServiceConstructor))
        {
            return (TSearchableLookupService)searchableServiceConstructor();
        }

        throw new ArgumentException($"Service for \"{key}\" not registered.");
    }

    public static void Register(
        string key,
        Func<ISearchableLookupService<IBaseOutputDto>> searchableServiceConstructor)
    {
        _SearchableLookupServicesRegistry.Add(key, searchableServiceConstructor);
    }
}

Note that SearchableServiceFactory is not generic. This is necessary to have only one single factory and consequently only one static dictionary.

It uses this modified interface having an out modifier. The out modifier adds covariance. Ie, you can supply a derived type for it; however, the generic type decorated with it must only occur as return type or in out parameters.

public interface ISearchableLookupService<out TOutputDto> where TOutputDto : IBaseOutputDto
{
    TOutputDto GetOutputDto();
}

You can register with

SearchableServiceFactory.Register(
    nameof(Organization), () => new OrgLookupService(_Context, _OrganizationRepository));

and create with

IBaseOutputDto result = SearchableServiceFactory
            .Create<ISearchableLookupService<IBaseOutputDto>>(nameof(Organization))
            .GetOutputDto();

or to get a more concrete type

OrgOutputDto result = SearchableServiceFactory
    .Create<ISearchableLookupService<OrgOutputDto>>(nameof(Organization))
    .GetOutputDto();

But this last example makes the string key nameof(Organization) redundant, as the type of OrgOutputDto itself could be used as key. (I would require some Reflection to extract it from TSearchableLookupService .)

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM