-
Notifications
You must be signed in to change notification settings - Fork 4k
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
Comments
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 |
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 |
@HaloFour 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:
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. |
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. |
@HaloFour 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. |
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. |
@HaloFour 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. |
@HaloFour |
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. |
@HaloFour I'm going to use the following example to address Mr. Morris' assertions: 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:
First of all, this point which is the one that I have taken issue with can be debated, since the In my designs,
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
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
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 Additionally, composition still plays a part in my design in that the 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 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. |
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. |
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:
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:
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:
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:
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.
The text was updated successfully, but these errors were encountered: