1

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!

7
  • 2
    What are the functional requirements? The structure feels like a solution in search of a problem - Having classes named after things which sound like data is often a clue that something is seriously wrong because there's nothing in any of those class names which give any indicator about what functional behaviour or business requirements any of those classes should satisfy from an end-user or application perspective; For example, UI features or UX/journey, including what actions and features the user has available to them. if you want testability then focus on behaviour and requirements Commented Jul 12, 2022 at 18:50
  • @BenCottrell Yeah that might be because of me trying to mirror my existing solution into a simplified snippet haha, but let's say its an API for callers to make cars go forward (as little sens it makes). Kind of a wrapper that hides the complexity of the Automotive API to the caller and lets him send a simple JSON payload that says which car needs to go forward. The API is then responsible for looping through each car, start its engine and press its gas pedal.
    – Union3008
    Commented Jul 12, 2022 at 19:09
  • Classes like Car, Engine and Pedal sound like business classes, domain classes or entities. The problem I see is that these classes depend on infrastructure classes. A CarService is a wrapper for a web API call. Something like this is a concern for the infrastructure side of the application. Commented Jul 12, 2022 at 19:49
  • @GregBurghardt They might be named wrong, but the main purpose of these classes is to split the work intuitively so I don’t have 1 class that does everything. But I agree they currently do two things, they are wrappers over results from the Automotive API (results obtained via AutomotiveService) and they also are « Orchestrators » that regroup multiple API calls just like a service would. Curious to hear what you would suggest instead?
    – Union3008
    Commented Jul 12, 2022 at 20:21
  • its a long question but i still dont understand what you are asking. Why are engine and pedal both AutomotiveRecords? does an AutomativeRecord have all the data for both? or is three a record for each car part with different columns? Why do you have a factory as well as a constructor with the same arguments?
    – Ewan
    Commented Jul 12, 2022 at 21:07

2 Answers 2

2

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

Service
{
   StartEngine(Engine e)
   PressPedal(Pedal p)
   ...
}

Engine
{
    int HorsePower
}

OR a OOP approach with no services and logic in the class

Engine
{
    int HorsePower
    void StartEngine() {...}
}

Either way it unrelated to populating Engine, Pedal etc objects from your data records, which you could do in a factory or repository

EngineFactory
{
   Engine Create(AutomotiveRecord d)
   {
      return new Engine()
      {
         HorsePower = d["hp"];
      }

   }
}

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.

8
  • 1
    Something was annoying me when looking at my code and I think you explained it well, my classes kinda have to different purpose and I need to seperate this correctly. I will come up with a revised snippet and hopefully you can confirm if I understood correctly, I really appreciate thank you!
    – Union3008
    Commented Jul 12, 2022 at 21:38
  • upvote button is right there dude!! :)
    – Ewan
    Commented Jul 12, 2022 at 22:04
  • Not familiar with stackexchange, but the following appeared haha : Thanks for the feedback! You need at least 15 reputation to cast a vote, but your feedback has been recorded. I will flag it as the answer after testing it out though!
    – Union3008
    Commented Jul 12, 2022 at 23:19
  • I posted two new versions of my code using both ADM and OOP approaches, if you could validate if it makes sens you would rock :)
    – Union3008
    Commented Jul 13, 2022 at 3:25
  • It might be because I'm tired, but "ADM" = Anemic Domain Model, right? Commented Jul 14, 2022 at 1:19
0

Ok I think I got the ADM approach figured out, not sure about the OOP approach as I didn't change much to the original code other than the Car/Engine/Pedal constructors not being populated by an AutomotiveRecord object anymore, but let me know if this is better :

ADM approach

Since the Car/Engine/Pedal classes are exclusively data classes in this approach, I got rid of the factories and modified the AutomotiveService to directly return the typed objects instead, less code same result :

public class AutomotiveService : IAutomotiveService
{
    public ICar GetCar(string carSerialNumber) { ... }

    public IEngine GetEngine(string carSerialNumber) { ... }

    public IPedal GetPedal(string carSerialNumber) { ... }


    public void StartEngine(IEngine engine) { ... }

    public void PressPedal(IPedal pedal) { ... }
}

Car/Engine/Pedal are now exclusively data classes :

public class Car : ICar
{
    public string SerialNumber { get; set; }

    public Car(String serialNumber)
    {
        SerialNumber = serialNumber;
    }
}

public class Engine : IEngine
{
    public string Horsepower { get; set; }

    public Engine(string horsepower)
    {
        Horsepower = horsepower;
    }
}

public class Pedal : IPedal
{
    public string Size { get; set; }

    public Pedal(string size)
    {
        Size = size;
    }
}

As a single service would defeat the initial purpose of encapsulating for having lighter classes and better segregation of duty, I would use multiple services, each of them DI injected with their own dependencies :

public class CarService: ICarService
{
    private IAutomotiveService _automotiveService;
    private IEngineService _engineService;
    private IPedalService _pedalService;
    
    public CarService(IAutomotiveService automotiveService, IEngineService engineService, IPedalService pedalService)
    {
        _automotiveService = automotiveService;
        _engineService = engineService;
        _pedalService = pedalService;
    }
    
    public void MoveForward(ICar car)
    {
        IEngine engine = _automotiveService.GetEngine(car.SerialNumber);
        _engineService.StartEngine(engine);
        
        IPedal pedal = _automotiveService.GetPedal(car.SerialNumber);
        _pedalService.PressPedal(pedal);
    }
}


public class EngineService: IEngineService
{
    private IAutomotiveService _automotiveService;
    private IFirstRandomService _firstRandomService;
    
    public EngineService(IAutomotiveService automotiveService, IFirstRandomService firstRandomService)
    {
        _automotiveService = automotiveService;
        _firstRandomService = firstRandomService;
    }
    
    public void StartEngine(IEngine engine)
    {
        _firstRandomService.DoSomething();
        _automotiveService.StartEngine(engine);
    }
}


public class PedalService: IPedalService
{
    private IAutomotiveService _automotiveService;
    private ISecondRandomService _secondRandomService;
    
    public PedalService(IAutomotiveService automotiveService, ISecondRandomService secondRandomService)
    {
        _automotiveService = automotiveService;
        _secondRandomService = secondRandomService;
    }
    
    public void PressPedal(IPedal pedal)
    {
        _secondRandomService.DoSomething();
        _automotiveService.StartEngine(engine);
    }
}

With this approach the code is still easily testable using DI, segregation of duty is straight forward and the 3 services can have their own dependencies.

The root code wouldlook like below :

public class App
{
    private IAutomotiveService _automotiveService;
    private ICarService _carService;
    
    public App(IAutomotiveService automotiveService, ICarService carService)
    {
        _automotiveService = automotiveService;
        _carService = carService;
    }
    
    public MoveCars(string jsonCars)
    {
        var configCars = Deserialize(jsonCars);
        
        foreach(var configCar in configCars)
        {
            ICar car = _automotiveService.GetCar(configCar.SerialNumber);
            _carService.MoveForward(car);
        }
    }
}

OOP approach

As for the OOP approach, I didn't change much other than modifying Car/Engine/Pedal constructors so they aren't populated using a AutomotiveRecord object. The Car class still uses factories for creating instances of Engine and Pedal.

As per OOP, the 3 classes have the data AND the behavior, not sure what you mean by "OOP approach with no services and logic in the class" because in my case the logic/behavior does need services.

AutomotiveService stays as is

public class AutomotiveService : IAutomotiveService
{
    public AutomativeRecord GetCar(string carSerialNumber) { ... }

    public AutomativeRecord GetEngine(string carSerialNumber) { ... }

    public AutomativeRecord GetPedal(string carSerialNumber) { ... }


    public void StartEngine(AutomativeRecord engine) { ... }

    public void PressPedal(AutomativeRecord pedal) { ... }
}

Car/Engine/Pedal classes have their constructors modified so they aren't populated by a AutomotiveRecord :

public class Car : ICar
{
    public string SerialNumber { get; set; }

    private IEngineFactory _engineFactory;
    private IPedalFactory _pedalFactory;

    public Car(string serialNumber, IEngineFactory engineFactory, IPedalFactory pedalFactory)
    {
        SerialNumber = serialNumber;

        _engineFactory = engineFactory;
        _pedalFactory = pedalFactory;
    }

    public void GoForward()
    {
        IEngine engine = _engineFactory.Create(SerialNumber);
        IPedal pedal = _pedalFactory.Create(SerialNumber);

        engine.Start();
        pedal.Press();
    }
}


public class Engine : IEngine
{
    public string Horsepower { get; set; }

    private AutomotiveService _automotiveService;
    private FirstRandomService _firstRandomService;

    public Engine(string horsepower, AutomotiveService automotiveService, FirstRandomService someOtherService)
    {
        Horsepower = horsepower;

        _automotiveService = automotiveService;
        _someOtherService = someOtherService;
    }

    public Start()
    {
        _someOtherService.DoSomething();
        _automotiveService.StartEngine(...);
    }
}


public class Pedal : IPedal
{
    public string Size { get; set; }

    private AutomotiveService _automotiveService;
    private SecondRandomService _secondRandomService;

    public Pedal(string size, AutomotiveService automotiveService, SecondRandomService secondRandomService)
    {
        Size = size;

        _automotiveService = automotiveService;
        _secondRandomService = secondRandomService;
    }

    public Press()
    {
        _secondRandomService.DoSomething();
        _automotiveService.PressPedal(...);
    }
}

Factories remain essentially the same :

public class CarFactory
{
    private IAutomotiveService _automotiveService;
    private IEngineFactory _engineFactory;
    private IPedalFactory _pedalFactory;

    public CarFactory(IAutomotiveService automotiveService, IEngineFactory engineFactory, IPedalFactory pedalFactory)
    {
        _automotiveService = automotiveService;
        _engineFactory = engineFactory;
        _pedalFactory = pedalFactory;
    }

    public ICar Create(string serialNumber)
    {
         AutomativeRecord recordCar = _automotiveService.GetCar(serialNumber);
         ICar car = new Car(recordCar["serialNumber"], _engineFactory, _pedalFactory);
         return car;
    }
}


public class EngineFactory
{
    private IAutomotiveService _automotiveService;
    private IFirstRandomService _firstRandomService;

    public EngineFactory(IAutomotiveService automotiveService, IFirstRandomService _firstRandomService)
    {
        _automotiveService = automotiveService;
        _firstRandomService = firstRandomService;
    }

    public IEngine Create(string carSerialNumber)
    {
         AutomativeRecord recordEngine = _automotiveService.GetEngine(carSerialNumber);
         IEngine engine = new Engine(recordEngine["horsepower"], _automotiveService, _firstRandomService);
         return engine;
    }
}


public class PedalFactory
{
    private AutomotiveService _automotiveService;
    private ISecondRandomService _secondRandomService;

    public PedalFactory(IAutomotiveService automotiveService, ISecondRandomService secondRandomService)
    {
        _automotiveService = automotiveService;
        _secondRandomService = secondRandomService;
    }

    public IPedal Create(string carSerialNumber)
    {
         AutomativeRecord recordPedal = _automotiveService.GetPedal(carSerialNumber);
         IPedal pedal = new Pedal(recordPedal["Size"], _automotiveService, _secondRandomService);
         return pedal;
    }
}

Not sure if I understood correctly for the OOP approach as I didn't change much?

Sorry for the 180 pages essay, but I want to be as clear as possible!

Thanks!

7
  • 1
    If classes like Car and Engine are strictly data classes, I would question the value of defining interfaces for them. Interfaces are abstractions for behavior. Data classes do not have behavior, and therefore are not abstractions. Commented Jul 14, 2022 at 1:28
  • seconded. also your services are confused. they shouldnt have to call the AutomotiveService at all. only use that to get your Car, Pedal etc then throw it away
    – Ewan
    Commented Jul 14, 2022 at 12:04
  • but yeah, i dont think you get the stack overflow format. you are supposed to ask a question and click the best answer. if you keep adding answers with multiple re writes it doesnt work
    – Ewan
    Commented Jul 14, 2022 at 12:05
  • @Ewan For the ADM approach I agree those 3 classes shouldn't need interfaces that's just a copy/paste issue. As for the services, ie EngineService, the reason it exists and that not everything is done in the AutomotiveService is that in order to start an engine, multiple operations/validations from different services need to be performed: It needs FirstRandomService to perform validations elsewhere and it needs AutomotiveService to update the record as "started". It kinda is an orchestrator that hides the complexity of the different APIs required to start an engine.
    – Union3008
    Commented Jul 14, 2022 at 13:40
  • 1
    cant really give you an example without all the data. but you shouldnt be injecting automotive service into the objects you get out of it. that is essentially where you are going wrong.
    – Ewan
    Commented Jul 14, 2022 at 17:07

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.