Context
I have a service class whose sole purpose is to interact with a specific API, let's say the Automotive API. The API mostly works with generic AutomativeRecord which basically represents a database record on the API backend with its fields stored as an array.
AutomotiveService.cs
public class AutomotiveService : IAutomotiveService
{
public AutomativeRecord GetCar() { ... }
public AutomativeRecord GetEngine(string carSerialNumber) { ... }
public AutomativeRecord GetPedal(string carSerialNumber) { ... }
public void StartEngine(AutomativeRecord engine) { ... }
public void PressPedal(AutomativeRecord pedal) { ... }
}
For readability and segregation of duty purpose, I want to encapsulate my logic into different classes (Car/Engine/Pedal), each of them using not only the AutomotiveService to perform operations on the automotive API, but also services specific to their class.
I came up with the following implementation :
Car.cs
public class Car : ICar
{
public string SerialNumber { get; set; }
private AutomativeRecord _recordCar;
private IAutomotiveService _automotiveService;
public Car(AutomativeRecord recordCar, IAutomotiveService automotiveService, IEngineFactory engineFactory, IPedalFactory pedalFactory)
{
SerialNumber = recordCar["SerialNumber"];
_recordCar = recordCar;
_automotiveService = automotiveService;
_engineFactory = engineFactory;
_pedalFactory = pedalFactory;
}
public void GoForward()
{
IEngine engine = _engineFactory.Get(SerialNumber);
IPedal pedal = _pedalFactory.Get(SerialNumber);
engine.Start();
pedal.Press();
}
}
Engine.cs
public class Engine : IEngine
{
public string Horsepower { get; set; }
private AutomativeRecord _recordEngine;
private AutomotiveService _automotiveService;
private SomeOtherService1 _someOtherService;
public Engine(AutomativeRecord recordEngine, AutomotiveService automotiveService, SomeOtherService1 someOtherService)
{
Horsepower = recordEngine["Horsepower"];
_recordEngine = recordEngine;
_automotiveService = automotiveService;
_someOtherService = someOtherService;
}
public Start()
{
_someOtherService.DoSomething();
_automotiveService.StartEngine(_recordEngine);
}
}
Pedal.cs
public class Pedal : IPedal
{
public string Size { get; set; }
private AutomativeRecord _recordPedal;
private AutomotiveService _automotiveService;
private SomeOtherService2 _someOtherService;
public Pedal(AutomativeRecord recordPedal, AutomotiveService automotiveService, SomeOtherService2 someOtherService)
{
Size = recordPedal["Size"];
_recordPedal = recordPedal;
_automotiveService = automotiveService;
_someOtherService = someOtherService;
}
public Press()
{
_someOtherService.DoSomething();
_automotiveService.PressPedal(_recordPedal);
}
}
EngineFactory.cs
public class EngineFactory
{
private AutomotiveService _automotiveService;
private SomeOtherService1 _someOtherService;
public EngineFactory(AutomotiveService automotiveService, SomeOtherService1 someOtherService)
{
_automotiveService = automotiveService;
_someOtherService = someOtherService;
}
public IEngine Get(string carSerialNumber)
{
AutomativeRecord recordEngine = _automotiveService.GetEngine(carSerialNumber);
IEngine engine = new Engine(recordEngine, _automotiveService, _someOtherService);
return engine;
}
}
PedalFactory.cs
public class PedalFactory
{
private AutomotiveService _automotiveService;
private SomeOtherService2 _someOtherService;
public PedalFactory(AutomotiveService automotiveService, SomeOtherService2 someOtherService)
{
_automotiveService = automotiveService;
_someOtherService = someOtherService;
}
public IPedal Get(string carSerialNumber)
{
AutomativeRecord recordPedal= _automotiveService.GetPedal(carSerialNumber);
IPedal pedal = new Pedal(recordPedal, _automotiveService, _someOtherService);
return pedal;
}
}
In order for the Car class to load it's "children" Engine and Pedal, I am using a Factory approach (I use a Get method instead of Create but this is irrelevant) for the following reasons :
-
For testability, it avoids the Car class to explicitly create new instances of Engine and Pedal, which would make the GoForward() method untestable. Mocked factories can easily be injected in the Car constructor.
-
For performance, since it allows the Car class to load its Engine and Pedal on demand (in the GoForward() method) rather than having them injected in the constructor where they aren't needed yet
-
Factories make it easy to scope the required services for each class. If the Car class was responsible for creating the "child" classes without factories, its constructor would require all services used by all children in order to be able to inject them in their constructors. With factories, the Car class is unaware of the services required for each "child" class so it doesn't need them in its constructor.
-
All factories (including the CarFactory.cs not shown above for simplicity) are initialized automatically in startup.cs using DI which makes everything clean and even more testable
Question
Although the approach above is test friendly, I find it becomes harder to maintain every time I need to add a "child" to the Car class because I have to create another factory for the specific "child" class.
I thought about having a class dedicated for creating the Car/Engine/Pedal instances using all the required services but that would end up in circular references as it would need to inject itself in the Car class in order for the Car class to be able to create Engine and Pedal children…
I guess my question is, is there a better pattern for dealing with a class that needs to instantiate other classes, on demand, with different services, while remaining testable, that does not involve injecting all possible services to the top class or using individual factories like I did?
If anyone can achieve what I did using a cleaner/simpler approach I would be glad to learn it here!
Thanks!
Best Answer
I think you are just going in circles here.
You can either use an ADM approach with one or more services and objects with no logic ie
OR a OOP approach with no services and logic in the class
Either way it unrelated to populating Engine, Pedal etc objects from your data records, which you could do in a factory or repository
Now your objects or service might have other dependent services injected into them, but what you have done is to have both a service and an object for the same bit of logic and inject them into each other.