Continuing the theme of conventions in code; I talked about validating the conventions, but I didn't touch upon one more basic issue. What to base the conventions on? Short answer is - anything you like (as long as it’s black). Long answer is, well - longer.
Strong typed nature of .NET gives us rich set of information we could use to build conventions on. Let's go over some of them. I will concentrate mostly on single scenario as an example - IoC container registration, but the discussion can be easily extrapolated to cover any other scenario.
Assemblies are the main, most coarse grained building blocks of application. While they carry quite a bit of information we could use (name, version, strong name, culture) it rarely makes sense to use all of that information. Most common usage is directly pointing to our assembly of interest, either by name
or in a more strongly typed manner for example via type container in that assembly in cases where we don't mind having strong reference to that assembly in your composition root.
Most of the time that is enough (at least at this level, in this situation).
Directly pointing at certain assembly is generally what we want, and this is the most straightforward and safe way to do that. In some cases (for example extensibility scenarios) you may want to leave the door open for consuming more than one assembly, often unknown at the time of compilation. In this case it may make sense to use as much information as possible to filter out unwanted assemblies as early as possible. This obviously depends on the specifics of the scenario at hand, for example who will be authoring the extensions? If only you (meaning your team/company), you can use information in assembly name and additionally verify the key that the assembly is signed with appropriate key.
In case where third party vendors will provide extensions, you may just require that the assembly conforms to certain naming pattern (name contains ".Crm.Extensions."). This not only gives you quick way of filtering out assemblies you're not interested in - it also helps you keep your project cleaner. With convention like this just a quick glance at directory with your project's assemblies will be enough information to tell how many extension assemblies there are.
This obviously is rarely enough - finding right assemblies is often just the first step and there's a wealth of information elsewhere you can use. Modules are seldom useful as most of the time you have just one per assembly, so I'm mentioning them only for completeness. Within (and between) assemblies namespaces are often used to partition types. They have two important characteristics that can be useful when defining conventions - their name, and hierarchy.
You rarely use namespaces on their own. Most often they are one of a few boundaries you set to narrow down the set of types you're interested in. For example you may find your repositories like this:
Broadly speaking you can find namespaces using similar techniques as with assemblies - strongly typed, as demonstrated in this paragraph or via string.
While the former is more type safe and refactoring friendly, the latter is more explicit and reads better - you get the information you want directly, without having to go through additional layer of indirection.
It is often valuable to take advantage of namespace hierarchy to use additional layer in the hierarchy to carry additional information. One such case could be to divide services in "Acme.Crm.Services" namespace to sub-namespaces based on their lifestyle, such that singleton CacheProviderService will live in "Acme.Crm.Services.Global" namespace, and per web request UnitOfWorkService would live in "Acme.Crm.Services.PerRequest".
Types themselves carry a lot of information we can use for conventions: they have meaningful names (hopefully), inherit other types, implement interfaces, have custom attributes, generic constraints etc. There are two popular usages for type names when dealing with conventions.
We often put meaningful suffixes, or more broadly speaking - name our types in a certain, meaningful way. For example type implementing IRepository<Customer> would be named CustomerRepository. Or we may require all our services to have "Service" suffix. While the former helps us keep thins clean and consistent the latter (by some regarded as certain form of dreaded Hungarian notation), often serves as safety net layer for validation purposes, to help you catch cases where you by mistake put a type in inappropriate namespace.
The other common usage is using certain prefixes to build decorator chains. You may have the following types: LoggingCustomerRepository, CachingCustomerRepository, CustomerRepository, LoggingProductRepository, CachingProductRepository, ProductRepository etc. When putting consistent, well known prefixes on your types you can use that information to build decorator chains, so that Logging*foo* will decorate Caching*foo* which will decorate actual *foo* type.
In IoC registration case base types (or interfaces) are very commonly used. Most popular case if layer supertype.
registers all controllers in our application.
registers all generic repositories we might have. It sometimes may also be worthwile to take greater advantage of generics.
public interface IHandler<TCommand> where TCommand: ICommand
public interface ICommand
we might quite easily tie the two together - handlers to their respective commands.
Attributes, marker interfaces
In early days of IoC containers it was common to see custom Attributes being used for configuring components. In Java (which gained attribute-like abilities much later after .NET) containers tend to prefer this approach even now. While this approach is generally discouraged, using domain specific attributes, or marking interfaces for your components may sometimes be beneficial.
You may for example use attributes to mark components you want to decorate, (put [CachedAttribute] on repositories you want to put cache around). Marking interfaces can be useful for this as well - attributes are preferred solution where you want to associate some additional data with the component (for example cache duration).
Similar information as outlined in case of types, can be used to build conventions around type members. While it usually makes little sense in context of IoC to do so, it's very useful in other cases - like Rob's MVVM framework and wiring Can*Foo* with *Foo* property/method pairs.
Anything can be used as basis for convention. You only have to know what makes sense in given context and looks for opportunities to improve that. There are a few things to keep in mind when deciding upon conventions. Keep them tight - use more than one constraint in order to minimize false positives. For example use namespace, base type and name suffix to find repositories in your application. You will pay small additional price of more reflection you do when starting, but it will save you lots of time trying to figure out why your application is misbehaving later. Make your conventions well known to the entire team. Put short description on a board in your team room, in wiki engine you're using, on in conventions.txt file in your solution. In addition to that make that rules executable to spot potential mistakes and disconforming code (see my previous post for more detailed discussion on the topic). Don't be afraid to refine the conventions. Once they are decided upon, they don't get written in stone. If your application grows and you find them inadequate, adjust them (trying not to loosen them up too much).
04-08-2010 6:26 PM