TLDR
It depends on how configurable and extendable you want to have the reusable library (project), without touching it.
In the end you either have factories in the library itself, or the library does not have them and the client (the user) of the library is responsible for creating them and instantiating the library himself.
Longer answer
No matter how object oriented your code is, a part of OO code will always have to be procedural. The part where the object graph is constructed, the new
ing of the classes.
When dealing with construction of classes belonging to another project (this will usually be some common library which contains code useful throughout more projects), you usually have two options how to expose the project to the outer world.
For this demostration purpose I chose the cache layer, which, from my experience, usually has very similar API throghout many different projects, only the configuration is different.
1. Creating a public endpoint of the library to expose it
If you were to follow this ideology, all the classes inside your cache
library project would be set to internal
except one class, perhaps a YourNamespace.Library.Cache
class, which would be set to public
, thus if you were to use the cache library, the only class you would have publicly access to is one single endpoint.
The YourNamespace.Library.Cache
class constructor could look like this:
public class Cache : CachingInterface
{
public Cache(string configuration)
{
// this is where the magic happens
}
}
Not saying very much, is it? What exactly is the magic I am talking about?
Notice the configuration
string as a parameter. It is a variable in which you define what you want to have, you insert your configuration, and this one public endpoint, the YourNamespace.Library.Cache
class, will, in its constructor, parse this string, extract the data it needs from it, and based on the user input construct everything necessary.
Inside this constructor the Cache
endpoint will instantiate the factories belonging to the cache project, it will the use those to instantiate the correct implementations.
In your main project, the code then could be as simple as this:
var cache = new YourNamespace.Library.Cache("type:redis,connection:[schema:tcp,host:localhost,port:3000,database:master],options:[replication:true]");
By parsing the string, the constructor will then see, you want to cache to redis, and provided the configuration, it will use the configuration and pass it onto the internal classes of the cache library, which know what to do with it.
Be aware the configuration does not need to be a string, you may use
whatever you want.
The placement of the factories
When using this approach, the factories are inside the cache project. In fact, the one public endpoint, the Cache
class, acts as a factory itself.
The project itself calls new
on desired object based on the configuration
string and after the construction of the endpoint, should it not throw an exception, you are set to use all the functionality of the library.
The good
- the construction of a very complex library is simplified to one single endpoint, which only requires some configuration (be it a string, an array,...) described in documentation
- you do not expose the internal parts of the library, so as long as you do not change the endpoint, you can optimize it, release new versions and the clients of this library need not to change anything in their code
The bad
- if someone uses your library and wants to add his custom caching mechanism (perhaps a faster Redis driver or a completely new caching mechanism), he has to contact the owner of the library the add it
- you need to create thorough documentation, so people know, how to use the endpoint and what configuration it offers
2. Exposing everything inside the library and letting the client of the library decide what he wants to construct
Using this approach, (almost) all classes inside the cache library will be set to public, all the interfaces, all the implementations, everything.
Besides the unit tests part of the cache library, there will probably be no usage of the new
keyword inside the cache library. The library itself will not construct anything, it will only provide constructors and methods with predefined types of parameters, to let you know which exact class you will need if you want to instantiate a class.
The placement of the factories
Considering the cache library itself does not construct anything, the only place where you will have the factories is your main project, or the project of the client using the library.
The client himself will be responsible for constructing the object graph, by new
ing the dependencies, until he creates the final object representing the cache itself.
The good
- the library is very easily extendable by the end user, would he want to use some custom implementation of a part of the library, he simply needs to adhere to the contract presented by the type of an object a class requires for its construction, perhaps by implementing an interface or extending a class
- the creator of the library itself does not need to create a lot of documentation describing the construction, because by following the dependency injection principle, it is always pretty clear, which other classes you need in order to construct the class you desire
- the client may very easily manipulate concrete implementations to make the library suitable for the project he is using it in
The bad
- the construction in client code may become (really) ugly
You are likely to see something like this in the client code:
var depOne = new One();
var depTwo = new Two(depOne);
var depThree = new Three(depTwo);
var depFour = new Four(depThree);
// ...
var depTwentySeven = new TwentySeven(depTwentySix);
var cache = new Cache(depTwentySeven);
This is an extreme example, the amount of dependencies would probably vary, based on the type of cache you would like to construct, but it can happen.
- because the client is responsible for creating all the dependencies, some of them may use the first pattern I am describing in this example for construction, meaning the client of the cache library is forced to check the documentation of the third party library to know how he is supposed to construct the dependency for the cache library.
Summary
Before diving in, you should decide, what it is you want.
If you want to encapsulate the library and want to have it its own environment and hide its complexity behind a simple abstraction layer, I would say go with a variation the first approach.
I would also use the first approach when I wanted to reflect all the internal changes in the library in all the projects where the library is used.
If you want the library to be easily extendable and you are likely to provide custom implementations, go with approach number two, where the library is not responsible for the construction, but gives clues on how the construction should happen.
But be aware, that change in the library will most likely need more change in the client code.
There is also always the third approach, which is a result of combining the first and second together, where then there are factories in both the library itself and also the client code.
Best Answer
You will need to have a reference to your logging library (e.g. log4net) in all projects that contain any code that wants to log. In a lot of cases that will mean nearly all your projects have a reference to your logging library.
In addition you will likely need a reference in your "entry point" project (e.g. your web app) so that you can configure log sinks etc...
You could introduce abstractions to avoid referencing your logging library everywhere, e.g. raise events instead of logging, or introduce a logging interface in your Core project. In some cases this can be worth it (e.g. an open source project where users will likely have different logging libraries), however in most projects this is a bad idea and just adds complexity for little real-world gain.