Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Idea: Support for generic(parametric) namespaces or modules #4494

Closed
TyreeJackson opened this issue Aug 11, 2015 · 11 comments
Closed

Idea: Support for generic(parametric) namespaces or modules #4494

TyreeJackson opened this issue Aug 11, 2015 · 11 comments

Comments

@TyreeJackson
Copy link

This is not quite a proposal, but more of an exploratory idea to consider with the possibility of being turned into a proposal.

Problem

Generics in .Net gives us incredible abstraction potential that we can use to dramatically reduce boilerplate code, reduce surface area for bugs and DRY our code on a level that some simply have not tapped into. Over time, it becomes apparent when working on certain types of applications that some type parameters are naturally required to be shared among several types. Sharing these parameters can currently be done in two ways.

The first is to redeclare the type parameter in every generic type that requires it and provide the proper constraints to ensure compatibility between the generic types that are sharing the type parameters.

Consider the following types for example where the shared type parameters are redeclared in each type signature as needed:

public  class       B<TB, TC, TD, TE, TF>
        where       TB : B<TB, TC, TD, TE, TF>
        where       TC : BaseC<TB, TC, TD, TE, TF>
        where       TD : BaseD<TB, TC, TD, TE, TF>
        where       TE : IBaseE<TB, TC, TD, TE, TF>
{}

public  class       BaseC<TB, TC, TD, TE, TF>
        where       TB : B<TB, TC, TD, TE, TF>
        where       TC : BaseC<TB, TC, TD, TE, TF>
        where       TD : BaseD<TB, TC, TD, TE, TF>
        where       TE : IBaseE<TB, TC, TD, TE, TF>
{}

public  class       BaseD<TB, TC, TD, TE, TF>
        where       TB : B<TB, TC, TD, TE, TF>
        where       TC : BaseC<TB, TC, TD, TE, TF>
        where       TD : BaseD<TB, TC, TD, TE, TF>
        where       TE : IBaseE<TB, TC, TD, TE, TF>
{}

public  interface   IBaseE<TB, TC, TD, TE, TF>
        where       TB : B<TB, TC, TD, TE, TF>
        where       TC : BaseC<TB, TC, TD, TE, TF>
        where       TD : BaseD<TB, TC, TD, TE, TF>
        where       TE : IBaseE<TB, TC, TD, TE, TF>
{}

public  class       G<TB, TC, TD, TE, TF, TG, TH>
        where       TB : B<TB, TC, TD, TE, TF>
        where       TC : BaseC<TB, TC, TD, TE, TF>
        where       TD : BaseD<TB, TC, TD, TE, TF>
        where       TE : IBaseE<TB, TC, TD, TE, TF>
        where       TG : G<TB, TC, TD, TE, TF, TG, TH>, TE
        where       TH : IBaseH<TB, TC, TD, TE, TF, TG, TH>
{}

public  interface   IBaseH<TB, TC, TD, TE, TF, TG, TH>
        where       TB : B<TB, TC, TD, TE, TF>
        where       TC : BaseC<TB, TC, TD, TE, TF>
        where       TD : BaseD<TB, TC, TD, TE, TF>
        where       TE : IBaseE<TB, TC, TD, TE, TF>
        where       TG : G<TB, TC, TD, TE, TF, TG, TH>, TE
        where       TH : IBaseH<TB, TC, TD, TE, TF, TG, TH>
{}

public  class       I<TB, TC, TD, TE, TF, TG, TH, TI, TJ>
        where       TB : B<TB, TC, TD, TE, TF>
        where       TC : BaseC<TB, TC, TD, TE, TF>
        where       TD : BaseD<TB, TC, TD, TE, TF>
        where       TE : IBaseE<TB, TC, TD, TE, TF>
        where       TG : G<TB, TC, TD, TE, TF, TG, TH>, TE
        where       TH : IBaseH<TB, TC, TD, TE, TF, TG, TH>
        where       TI : I<TB, TC, TD, TE, TF, TG, TH, TI, TJ>, TH
        where       TJ : BaseJ<TB, TC, TD, TE, TF, TG, TH, TI, TJ>
{
}

public  delegate    IEnumerable<TC> FilterQ<TB, TC, TD, TE, TF, TG, TH, TI, TJ>(IQueryable<TJ> tj, TC tc)
        where       TB : B<TB, TC, TD, TE, TF>
        where       TC : BaseC<TB, TC, TD, TE, TF>
        where       TD : BaseD<TB, TC, TD, TE, TF>
        where       TE : IBaseE<TB, TC, TD, TE, TF>
        where       TG : G<TB, TC, TD, TE, TF, TG, TH>, TE
        where       TH : IBaseH<TB, TC, TD, TE, TF, TG, TH>
        where       TI : I<TB, TC, TD, TE, TF, TG, TH, TI, TJ>, TH
        where       TJ : BaseJ<TB, TC, TD, TE, TF, TG, TH, TI, TJ>;

public  class       BaseJ<TB, TC, TD, TE, TF, TG, TH, TI, TJ>
        where       TB : B<TB, TC, TD, TE, TF>
        where       TC : BaseC<TB, TC, TD, TE, TF>
        where       TD : BaseD<TB, TC, TD, TE, TF>
        where       TE : IBaseE<TB, TC, TD, TE, TF>
        where       TG : G<TB, TC, TD, TE, TF, TG, TH>, TE
        where       TH : IBaseH<TB, TC, TD, TE, TF, TG, TH>
        where       TI : I<TB, TC, TD, TE, TF, TG, TH, TI, TJ>, TH
        where       TJ : BaseJ<TB, TC, TD, TE, TF, TG, TH, TI, TJ>
{}

public  interface   IBaseK<TB, TC, TD, TE, TF, TG, TH, TI, TJ>
        where       TB : B<TB, TC, TD, TE, TF>
        where       TC : BaseC<TB, TC, TD, TE, TF>
        where       TD : BaseD<TB, TC, TD, TE, TF>
        where       TE : IBaseE<TB, TC, TD, TE, TF>
        where       TG : G<TB, TC, TD, TE, TF, TG, TH>, TE
        where       TH : IBaseH<TB, TC, TD, TE, TF, TG, TH>
        where       TI : I<TB, TC, TD, TE, TF, TG, TH, TI, TJ>, TH
        where       TJ : BaseJ<TB, TC, TD, TE, TF, TG, TH, TI, TJ>
{}

The second approach is to use nested classes within generic classes to form parametric boundaries or containers where access to the type parameters can be shared. This form is much DRYer than in the first approach.

Consider the following example where the types from the first example have been refactored to use a class nesting strategy:

public class A<TA> where TA : A<TA>
{
    public  class   B<TB, TC, TD, TE, TF>
            where   TB : B<TB, TC, TD, TE, TF>
            where   TC : B<TB, TC, TD, TE, TF>.BaseC
            where   TD : B<TB, TC, TD, TE, TF>.BaseD
            where   TE : B<TB, TC, TD, TE, TF>.IBaseE
    {
        public  class       BaseC   {}

        public  class       BaseD   {}

        public  interface   IBaseE  {}

        public  class       G<TG, TH> : IBaseE
                where       TG : G<TG, TH>, TE
                where       TH : G<TG, TH>.IBaseH
        {
            public  interface   IBaseH {}

            public  class       I<TI, TJ> : IBaseH
                    where       TI : I<TI, TJ>, TH
                    where       TJ : I<TI, TJ>.BaseJ
            {
                public  delegate    IEnumerable<TC> FilterQ(IQueryable<TJ> tj, TC tc);

                public  class       BaseJ   {}
                public  interface   IBaseK  {}
            }
        }
    }
}

It is easy to see that the code is much DRYer in the second example while still maintaining the exact same accessibility to the shared type parameters.

However, it is still a bit clunky and not as DRY as it could be. Also, note that some classes may have a double use of encapsulating functionality directly and also serving as a container where the type parameters are shared.

Also, some people consider nesting public details inside of a class to be a code smell (for example this answer from Eric Lippert).

Also, this:

Warning 34 CA1034 : Microsoft.Design : Do not nest type 'OuterClass.NestedType'. Alternatively, change its accessibility so that it is not externally visible.

Idea

Considering the above, would it make sense to introduce a new container type that is not explicitly a class but that serves the purpose of organizing types together that share access to the same set of generic parameters? Two ideas come to mind. One would be generic/parametric namespaces and the other would be generic/parametric modules. In both cases, the generic/parametric container would be abstract, have to be qualified and made concrete by deriving the container and supplying either well defined types or by another container that has type parameters that are supplied to the abstract container. In both cases, the container should contain no more and possibly less capability than a static class.

The main difference between the two would be in the ability for others to add types into the container from outside of the assembly. Namespaces can span assemblies, and the same would be true for generic/parametric namespaces. Modules on the other hand would be restricted and closed to a single assembly. Otherwise, either approach would serve identical purposes.

Consider the following example demonstrating a generic/parametric module:

module  ASpace<TA>
where   TA : .A
{
    public  class   A {}

    module  BSpace<TB, TC, TD, TE, TF>
    where   TB : .BaseB
    where   TC : .BaseC
    where   TD : .BaseD
    where   TE : .IBaseE
    {
        module  GSpace<TG, TH>
        where   TG : .BaseG, TE
        where   TH : .IBaseH
        {
            public  class       BaseG   {}

            public  interface   IBaseH  {}

            module  ISpace<TI, TJ>
            where   TI : .BaseI, TH
            where   TJ : .BaseJ
            {

                public  class       BaseI
                {
                    public  delegate    IEnumerable<TC> FilterQ(IQueryable<TJ> tj, TC tc);
                }
                public  class       BaseJ   {}
                public  interface   IBaseK  {}

            }

        }

        public  class       BaseB   {}

        public  class       BaseC   {}

        public  class       BaseD   {}

        public  interface   IBaseE  {}

    }

}

Note the preceding . before each type constraint. This directs the compiler to look for the class constraint for the type parameter within the module itself. Aside from this, all normal considerations and constraints for type parameters would apply.

Thoughts? Are types nested within generic classes sufficient? If so, should this be consider a valid use of exposing public types nested within a class and an exception to CA1034 and Eric Lippert's concerns?

Also, could nesting types within interfaces be considered as justified for supporting this type of design too? C# does not allow nesting types within interfaces, but Visual Basic .Net and the CLR do, including nesting within generic interfaces.

I apologize for the obfuscation of the type names in the example above.

@svick
Copy link
Contributor

svick commented Aug 11, 2015

I really don't think this kind of hugely-generic code is common enough to warrant brand new language features just for that. Especially since your module version is not that much better than the class-based version.

It could make some sense to introduce the where TB : .BaseB syntax for classes, but even that seems to be of very limited use to me.

@HaloFour
Copy link

Namespaces are syntax candy in C#. Such an encapsulating container does not really exist in the CLR. The namespace is simply the dotted prefix of the class.

And I agree with @svick , if you're tying yourself up in such knots with generic classes I think a reevaluation of the solution is in order.

As for nested types within an interface, this came up in #2238. It's definitely possible with the CLR, but it violates CLS. Here's where Eric Lippert chimes on in the subject:

http://stackoverflow.com/questions/16151614/why-cant-an-interface-contain-types

@TyreeJackson
Copy link
Author

@HaloFour
I think you mistook my reason for posting this. I am not tied up in knots with generic classes. In fact, I can happily continue working with them as I have. I brought this post up, as I mentioned on the first line, not as a proposal, but rather to discuss the concept and see if there is the potential for a new language idea here. The fact is, the technique I'm employing has been very successful and makes logical sense, especially as related to modeling 'layered' designs. Each time I've employed the technique, I've reevaluated it, and each time with peer reviews, the solution continues to hold water. After using it for 7 years, I'm still convinced as to its application. However, it flies in the face of conventional wisdom regarding nested types.

As far as namespaces are concerned, I'm aware that they are syntactic sugar. However, the same syntactic sugar techniques could be applied to types declared in a generic/parametric namespace, where in addition to being the dotted prefix of the class, it also serves as the first type parameters along with constraints for all classes "defined" in the namespace.

Consider the following for example:

namespace  ASpace<TA, TB, TC>
where TA : .A
where TB : .B
where TC : .C
{
    public class A {}
    public class B {}
    public class C {}
    public class D<TComparable> where TComparable : IComparable<TComparable>  {}
}

Would be emitted as:

public class ASpace.A<TA, TB, TC>
where TA : ASpace.A<TA. TB. TC>
where TB : ASpace.B<TA, TB, TC>
where TC : ASpace.C<TA, TB, TC>
{
}

public class ASpace.B<TA, TB, TC>
where TA : ASpace.A<TA. TB. TC>
where TB : ASpace.B<TA, TB, TC>
where TC : ASpace.C<TA, TB, TC>
{
}

public class ASpace.C<TA, TB, TC>
where TA : ASpace.A<TA. TB. TC>
where TB : ASpace.B<TA, TB, TC>
where TC : ASpace.C<TA, TB, TC>
{
}

public class ASpace.D<TA, TB, TC, TComparable>
where TA : ASpace.A<TA. TB. TC>
where TB : ASpace.B<TA, TB, TC>
where TC : ASpace.C<TA, TB, TC>
where TComparable : IComparable<TComparable>
{
}

With regards to Eric Lippert's answer to that question, I've read it. I even added an answer on that same question about three weeks ago. :-) In fact, I did so, because Eric said:

Without someone to advance a case for the feature, it's not going to last in the design committee meeting for more than maybe five minutes tops. Do you care to advance a case for the feature?

So, I did.

For what its worth, here is one of the examples I provided in that answer:

public  interface   IEntity
                    <
                        TIEntity, 
                        TDataObject, 
                        TDataObjectList, 
                        TIBusiness, 
                        TIDataAccess, 
                        TPrimaryKeyDataType
                    >
        where       TIEntity            : IEntity<TIEntity, TDataObject, TDataObjectList, TIBusiness, TIDataAccess, TPrimaryKeyDataType>
        where       TDataObject         : IEntity<TIEntity, TDataObject, TDataObjectList, TIBusiness, TIDataAccess, TPrimaryKeyDataType>.BaseDataObject
        where       TDataObjectList     : IEntity<TIEntity, TDataObject, TDataObjectList, TIBusiness, TIDataAccess, TPrimaryKeyDataType>.IDataObjectList
        where       TIBusiness          : IEntity<TIEntity, TDataObject, TDataObjectList, TIBusiness, TIDataAccess, TPrimaryKeyDataType>.IBaseBusiness
        where       TIDataAccess        : IEntity<TIEntity, TDataObject, TDataObjectList, TIBusiness, TIDataAccess, TPrimaryKeyDataType>.IBaseDataAccess
        where       TPrimaryKeyDataType : IComparable<TPrimaryKeyDataType>, IEquatable<TPrimaryKeyDataType>
{

    public  class       BaseDataObject
    {
        public  TPrimaryKeyDataType Id  { get; set; }
    }

    public  interface   IDataObjectList : IList<TDataObject>
    {
        TDataObjectList ShallowClone();
    }

    public  interface   IBaseBusiness
    {
        void            Delete(TPrimaryKeyDataType id);
        TDataObjectList Load(TPrimaryKeyDataType id);
        TDataObjectList Save(TDataObjectList items);
        bool            Validate(TDataObject item);
    }

    public  interface   IBaseDataAccess
    {
        void            Delete(TPrimaryKeyDataType id);
        TDataObjectList Load(TPrimaryKeyDataType id);
        TDataObjectList Save(TDataObjectList items);
    }

}

Of course, IEntity could just as easily be a generic class, or a generic module, or a parametric namespace, so no biggie there. My other reason for supporting mixins could be addressed by adding proper mixins to the language, so that one can also be argued against pretty easily too, if mixin support is added.

What is interesting, however, is whether or not one is nesting types within generic interfaces or generic classes, either way, they are essentially defining a parametric container of some sort.

@HaloFour
Copy link

@TyreeJackson

And it's fine to advocate for the cause. But you're not advancing a reason for it beyond examples of code that just makes people cringe. Even your repository examples seem much more an antipattern than anything else, mixing concerns together than are better separated. I don't think that the language should be modified to better support antipatterns.

@TyreeJackson
Copy link
Author

@HaloFour
It's one thing to critique code examples. But if you are going to say that something is an anti pattern then you should name the anti pattern and say why. Conjecture is useless without giving a reason.

I can present why these "techniques" work. Which by the way, that is all they are. They are techniques for encapsulating real actual patterns, like the repository pattern, or strategy pattern or and several others. The way I am using generics merely takes those well accepted and established patterns and bakes them into abstract classes that can be specialized later, thereby reducing copying and pasting of code to implement those patterns, or needless casting of types on more naive implementations of those patterns.

The concerns are not mixed, the are abstracted, big difference. They are in fact separated within the abstract implementation. If you are simply incapable if thinking at that level of abstraction, then I recommend that you stay away from architecture and stick with rudimentary programming.

In other words, prove it, or get out of the way.

@HaloFour
Copy link

@TyreeJackson

Sorry, you're the one who's going to have to do the proving here. You're the one arguing for new language (and potentially CLR) features in order to enshrine your techniques. Nobody else appears to be having these kinds of problems when it comes to implementing these basic patterns. The overuse of generics, which almost inevitably leads to tying them in knots, is absolutely an antipattern. The generic repository is an antipattern. A repository should declare what it does, not what every potential repository could do.

@TyreeJackson
Copy link
Author

@HaloFour
Do you have a link to the articles on the either of these alleged anti patterns? Any proof? Generics themselves are just language features used to realize implementation details. Is it possible to write bad code in generics? Sure. Same is true about any code. The repository patterns in my example only declare what basic methods all repositories that use that library implementation should do. It does not forbid, but rather encourages repository specific implementation interface types to be specified to the type argument for TIDataAccess.

I am providing (proving) reasons for why I am asking for new language features. I have opened each proposal with the problem statement, support it with examples and proceeded to offer a solution. While I appreciate that you have attempted to provide alternative solutions to some of those, I have shot holes in your alternatives, usually because I had already considered them previously and already encountered the problems with them.

You on the other hand, in this thread, seem to be throwing around declarations of anti patterns on things that you seem to not understand without clearly demonstrating why they are anti patterns or at the very least providing any evidence or linked articles to back your assertions.

If you have nothing constructive to offer to this conversation, please kindly exit it.

@TyreeJackson
Copy link
Author

@HaloFour
Also, most of my proposals are for simplifying the readability of using existing features already supported by the run time. The only proposal that solves an actual problem that cannot be fully guaranteed today without some conventions is the f-bound/CRTP constraint, for which I am not the first to point out this issue.

@HaloFour
Copy link

http://www.ben-morris.com/why-the-generic-repository-is-just-a-lazy-anti-pattern/

It's difficult to be constructive when the proposal seems to exist specifically to further a very specific overcomplicated implementation of specific patterns that are easily implemented without them. I'm sorry that you do not care for those idiomatic implementations, they seem perfectly sufficient for pretty much everyone else. It's not enough that it's possible to be changed, it has to demonstrate well beyond the cost of implementation and permanent support that it will provide significant benefit to the development community.

@TyreeJackson
Copy link
Author

@HaloFour
Now we're having a conversation. I agree nearly completely with Ben Morris' assertions, except one. But it's a minute point that does not apply to this conversation, because I am not advocating for an open generic repository. I'm actually advocating for a template abstract repository that is specialized into the very type of repository that he talks about at the end. Allow me to address how his first points do not apply in the techniques above and how his last point actually supports them:

I'm going to use the following example to address Mr. Morris' assertions:
Consider the following generic abstract types (which incidentally are the de-obfuscated forms of my example in the first entry of this thread):

namespace SomeMagicFramework
{ partial class Context<TContext> // class defined as public abstract with type constraints in its main file
{ partial class Entity<TEntity, TDataObject, TDataObjectList, TIBusiness, TIdType> // class defined as public abstract with type constraints in its main file
{ partial class BaseRepoBusiness<TIDataAccess> // class defined as public abstract with type constraints in its main file and also TIDataAccess is constrained using 'where TIDataAccess : BaseRepoBusiness<TIDataAccess>.IBaseDataAccess'
{
    public interface IBaseDataAccess { ... }
    public abstract class BaseEFDataAccess<TDataAccessObject> : IBaseDataAccess // <- One of two different data access layer generic abstract implementation templates 
    {
    }
    public class BaseSomeObjectDBDataAccess<TDataAccessObject> : IBaseDataAccess // <- One of two different data access layer generic abstract implementation templates 
    {
    }

    protected TIDataAccess dataAccess { get; private set; }
    protected BaseRepoBusiness(TIDataAccess dataAccess) { this.dataAccess = dataAccess; }
}

And the following specialized example of the above generic abstracts:

namespace SomeDomain
{ partial class SomeDomainContext : SomeMagicFramework.Context<SomeDomainContext>
{ partial class User : Entity<User, User.DataObject, User.DataObjectList, User.IBusiness, Guid>
{ partial class RepoBusiness : BaseRepoBusiness<RepoBusiness.IDataAccess>
{
    public interface IDataAccess : IBaseDataAccess // <- Here is the specialized repository interface that the next layer sees.  It is a type parameterless interface, and no longer an open generic repository interface
    {
        IEnumerable<DataObject> FindUserByUserName(string userName);
    }
    public class EFDataAccess : BaseEFDataAccess<EFDataAccess.DataAccessObject> : IDataAccess  // <- Here is one implementation of the specialized repository interface, again no open generic repository here
    {
            public class DataAccessObject  // <- internal EF class for transporting data to/from the data storage layer
            {
                public string UserName;  // <- could use property getters and setters to wrap properties on a private instance of DataObject
                public string FirstName;  // <- could use property getters and setters to wrap properties on a private instance of DataObject
                public string LastName;  // <- could use property getters and setters to wrap properties on a private instance of DataObject
            }
            public IEnumerable<DataObject> FindUserByUserName(string userName)
            {
                // use EF with DataAccessObject (note this is not DataObject which is the domain object, but rather the data access layer model) to work with the database.
            }
    }
    public class SomeObjectDBDataAccess : BaseObjectDBDataAccess<EFDataAccess.DataAccessObject> : IDataAccess  // <- Here is another implementation of the specialized repository interface, again no open generic repository here
    {
            public class DataAccessObject  // <- internal SomeObjectDB class for transporting data to/from the data storage layer
            {
                public string UserName;  // <- could use property getters and setters to wrap properties on a private instance of DataObject
                public string FirstName;  // <- could use property getters and setters to wrap properties on a private instance of DataObject
                public string LastName;  // <- could use property getters and setters to wrap properties on a private instance of DataObject
            }
            public IEnumerable<DataObject> FindUserByUserName(string userName)
            {
                // use SomeObjectDB library classes with DataAccessObject to work with the database.
            }
    }

    public RepoBusiness(IDataAccess dataAccess) : base(dataAccess) {}  // <- Now TIDataAccess has been specialized into User.RepoBusiness.IDataAccess, a specialized type parameterless entity specific interface

}

Note that SomeObjectDB is a made up name to represent another data storage technology, like MongoDB or Raven, etc.*

Now with that established, I will address his points from the article:

It’s a leaky abstraction

First of all, this point which is the one that I have taken issue with can be debated, since the T in the IRepository that he gives as an example is not necessarily the Entity Framework classes. T is likely the domain object. We don't know from his example, so we're left to jump to a conclusion. The Entity Framework classes may be unknown and therefore not leaked. However, the point that he's trying to make is valid in that I have seen others directly expose EF classes outwardly, which is obviously a bad thing. And if the Repository that implements IRepository is in fact an open generic type that does not qualify a data access model type parameter to map to T then his point is very likely correct.

In my designs, TDataObject is the domain object. The Entity Framework classes, which are used in only one implementation of potentially several abstract implementations of IDataAccess, are completely hidden within the type signature for that generic abstract. Moreover, the IDataAccess interface is contained within a very specific Business class implementation. It is publicly exposed because the IDataAccess implementation must be injected into the Business class implementation. For example, the implementation might use EF, or it might use an object database like MongoDB behind its implementation. Either way, those details are hidden, not leaked and more importantly nothing outside of those respective implementations care.

It’s too much of a generalization

This is true if the Repository is an open generic type which seems to be what he is arguing against. Generalization can be a good thing at the right layer and level of abstraction though. The IDataAccess types in my designs are never offered first and them implemented upon. First we model a sample of what we are trying to accomplish (or we already have an existing system that we are improving) and then we look for ways to DRY that model code. Nearly every time I've done this, an IBusiness with a Business implementation requiring an IDataAccess has emerged that required a certain set of base methods. The generic abstracts and interfaces I defined merely reflect the state of that real world system. Moreover, they are internally used, publicly defined, abstract and generic. They are not directly invoked, and they cannot even exist on their own without specialization.

It doesn’t define a meaningful contract

This point stands, but only for open generic repositories. I'm not advocating for that. Quite the contrary, I'm advocating for f-bound/CRTP constrained generic repositories. There is a difference. You can't simply instantiate one of the generic repositories in my designs and go to town writing any type of query that you want. You must specialize these repositories. The type parameters define the abstracts of meaningful contracts, the meta, the fact that for a specific Business implementation, there will be an IDataAccess implementation that supports a minimum amount of boilerplate logic that will be injected into the Business implementation. It is up to the specialized IDataAccess interface to provide the more meaningful concrete methods. Remember, IDataAccess in my examples is actually the Entity.BaseRepoBusiness.IDataAccess<TEntity, TDataObject, TDataObjectList, TIBusiness, TIdType, TIDataAccess> type. IDataAccess cannot stand on its own. Moreover, BaseRepoBusiness does not accept IDataAccess in it's constructor, but rather TIDataAccess, a derived subtype interface that extends IDataAccess. There is no open, meaningless repository being used here.

A generic repository does have a place… just not on the frontline

Which leads us to the last point of his article which absolutely supports what I'm doing. In fact, I would argue that he does not take this last point far enough. My designs are doing precisely this with one caveat. The generic Repository in his example is replaced with a generic abstract interface (important difference) that is derived into a specialized concrete interface, User.RepoBusiness.IDataAccess for example. Instead of composition of a generic type, I use specialization of a template type.

Additionally, composition still plays a part in my design in that the IDataAccess itself is then injected into the concrete RepoBusiness that is specialized from the BaseRepoBusiness. That class in turn implements the specialized version of TIBusiness and thus that implementation is hidden away behind that interface. And then even further, all of the specializations of these types listed above are encapsulated into an implementation assembly containing another layer that converts between the specialized domain objects contained within and contract class objects that are versioned and shared publicly. In other words, the entire specialized form of the generic types I've included in my examples are actually implementation details that are hidden away!

There is no generic repository on the frontline. Whatever generic repository logic is present is buried in the layers where it belongs quietly reducing boilerplate code as the rest of the generic abstract types do for their areas of concerns.

In the end I'm not advocating for mixing concerns, or exposing generic repositories or proliferating anti patterns. Quite the contrary, I'm advocating for modeling well accepted design patterns and their interactions with generic abstracts and then requiring specialization of those abstracts in order to reduce idiomatic boilerplate code that would otherwise have to potentially be maintained repeatedly across numerous types.

Moreover, each nested parametric namespace of design patterns is completely optional! The types from one nested parametric namespace/layer are injected into the implementation that their interfaces are found within one layer out. Those types in turn implement interfaces that are injected into the next outer layer, etc.

If a developer wants to custom implement TIBusiness without using BaseRepoBusiness and it's TIDataAccess, great! There is no coupling there that requires them to build the entire stack. If they want make calls directly to stored procedures from their own custom implementation of their own derived TIBusiness, they most certainly can. Depending on the situation, I might advise against that unless they have a good reason to do that degree of technology coupling. The advantage of these layers is that each one hides one side of the layer from the other and they help to prevent the very kind of abstraction leaks that Mr. Morris talks about.

And I have been successfully doing this for years. Do the models evolve? Of course they do as I find ways to incorporate other patterns that I have not considered. But have I found a better, more efficient and concise set of techniques for baking those patterns into something as reusable as these templates? No.

Now, after having seen these techniques work time and again, I'm asking for consideration for language features to help support and ease the writing of these abstracts and to improve the readability with in the language. Very little of what I'm asking for should require any CLR support. I believe that the enum proposal is the only one. Everything else is language additions with compiler support.

Please, keep trying to shoot holes. I have years of practice defending these techniques. I hope you find one, cause I am always looking to improve upon the above. But if nothing else, I absolutely thank you for your time in this discussion. Without constructive conversations like this it is much more difficult to move the state of the art forward.

@gafter
Copy link
Member

gafter commented Mar 20, 2017

We are now taking language feature discussion on https://github.com/dotnet/csharplang for C# specific issues, https://github.com/dotnet/vblang for VB-specific features, and https://github.com/dotnet/csharplang for features that affect both languages.

@gafter gafter closed this as completed Mar 20, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants