This example shows the registration and resolving with a simple object hierarchy with SimpleInjector
. How this can be solved if I add a generic type (Shipper
with a contract
) in the hierarchy?
Considerations:
The contract
is parsed as a csv
file and they can be totally different for UPS and FedEx. Only the UPS and FedEx classes know what to look for in the contracts before shipping. The contracts are read from the csv due to the Business can update the contract anytime and drop it in the folder. The app should not start as a result of changes in the contract.
I want to test UPS and FedEx with its dependencies like notifications etc... (not in the code).
Please don't consider the register
and resolve
that I have used in the example is wrong. Should consider only the class hierarchy.
The following is the partial code:
public Setup()
{
var container = new SimpleInjector.Container();
// TODO: Registration required for two different shippers
container.Register<UPS, IShipper>();
}
public interface IShipper<T>
{
T contact;
void Ship(Product product);
T ParseContract(string fileName);
void SetContract(T);
}
public class FiveYearContract
{
public string IsSigned { get; set; }
}
public class TenYearContract
{
public string IsPrepaid { get; set; }
}
public interface IUPS<T> : IShipper<T> { }
public interface IFedEx<T> : IShipper<T> { }
public class UPS : IUPS<FiveYearContract> { ... }
public class FedEx : IFedEx<TenYearContract> { ... }
Usage:
public void Ship(String shipper, Product product)
{
// TODO: Retrieve the shipper by name
var shipper = container.GetInstance<IShipper>();
shipper.SetContract(shipper.ParseContract("contract.csv");
// Contract will be checked before sending
shipper.Ship(product);
}
Ship("FedEx", new Product());
To be able to resolve a IShipper<T>
where the T
is unknown at compile time, you will have to use reflection. For instance:
public Setup()
{
container.Register(typeof(IShipper<>), new[] { typeof(FedEx).Assembly });
}
public void Ship(String shipper, Product product)
{
var contract = shipper.ParseContract("contract.csv");
dynamic shipper = container.GetInstance(
typeof(IShipper<>).MakeGenericType(contract.GetType()));
shipper.SetContract((dynamic)contract);
shipper.Ship(product);
}
The SetContract
method however, causes the shipper implementations to become mutable and it causes Temporal Coupling. A better design would typically be to pass the contract
runtime value on with the Ship
method, as follows:
public void Ship(String shipper, Product product)
{
var contract = shipper.ParseContract("contract.csv");
dynamic shipper = container.GetInstance(
typeof(IShipper<>).MakeGenericType(contract.GetType()));
shipper.Ship((dynamic)contract, product);
}
This allows the IShipper<T>
implementations to stay immutable (which can considerably simplify your application) and removes the implicit relationship between the SetContract
and Ship
methods. It most likely makes those implementations easier to test as well.