Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to inject service to domain entity and how to persist entity

I'm creating a Tour planner. The Tour always starts and ends at the same coordinates. Between the start and end, there are Stops. Stops contain a specific Sight to visit during the tour + the time of visit. When adding a Sight to the tour, I insert a new Stop, and recalculate my Stops' arrival time, based on their distances from one another. For this I use the injected ITravelService instance, in the private recalculateStopTimes(int fromIdx) method.


My problem is the following: this works until I want to persist a Tour object in the database through an ORM. The functionality will be lost after retrieval, because of the private ITravelService. I thought about injecting the serivce through the InsertSight/RemoveSight/RemoveStop methods, but then I would need to inject it with every public method I create, that modifies Stops. Is there a better way to inject a service like this, to an entity? Or should I even inject it? If not, how could I get the same functionality (tour recalculating it's stops)?

public interface ITravelService
{
        public TimeSpan CalculateTimeBetween(Coordinates from, Coordinates to);
}

public class Tour : ITour
    {
        private readonly List<Stop> _stops;
        private ITravelService _travelService;

        public IReadOnlyList<Stop> Stops { get { return _stops; } }
        public bool IsWithinLimit { get { return _stops.Last().TimeRange.From < (StartTime.TimeOfDay + Length); } }


        public Tour(DateTime startTime, TimeSpan length, Coordinates start, ITravelService travelService)
        {
            StartTime = startTime;
            Length = length;
            Stop firstStop = new Stop(start, new TimeRange(startTime.TimeOfDay, startTime.TimeOfDay));
            Stop lastStop = new Stop(start, new TimeRange(startTime.TimeOfDay, startTime.TimeOfDay));
            _stops = new List<Stop>() { firstStop, lastStop };
        }

        private void recalculateStopTimes(int fromIdx)
        {
            for (int i = fromIdx; i < _stops.Count - 1; i++)
            {
                Stop currentStop = _stops[i];
                Stop nextStop = _stops[i + 1];
                var travelTime = _travelService.CalculateTimeBetween(currentStop.Coordinates, nextStop.Coordinates);
                nextStop.Arrival = currentStop.TimeRange.To + travelTime;
            }
        }

        public void InsertSight(Sight sight, int index)
        {
            if (index == 0 || index == Stops.Count) throw new ArgumentOutOfRangeException("Cannot insert before first, or after last stop.");
            _stops.Insert(index, new SightStop(sight, StartTime.DayOfWeek));

            recalculateStopTimes(index - 1);
        }

        public void RemoveSight(Sight sightToRemove)
        {
            if (_stops.Count == 2) throw new ArgumentException("Sight is not in tour");
            int idx = 1;
            while (((_stops[idx] as SightStop).Sight != sightToRemove) && idx <= _stops.Count - 1)
            {
                idx++;
            }
            if (idx < _stops.Count)
            {
                RemoveStopAt(idx);
            }
            else
            {
                throw new ArgumentException("Sight is not in tour");
            }
        }

        public void RemoveStopAt(int index)
        {
            if (index > 0 && index < _stops.Count - 1)
            {
                _stops.RemoveAt(index);
                recalculateStopTimes(index - 1);
            }
            else
            {
                throw new ArgumentOutOfRangeException("Index was out of range");
            }
        }

        public IReadOnlyList<Sight> SightsInTour
        {
            get
            {
                return _stops.Where(stop => stop is SightStop).Select(x => (x as SightStop).Sight).ToList();
            }
        }
    }
like image 680
sharp328 Avatar asked Sep 19 '25 10:09

sharp328


1 Answers

If you want to stick with the DDD approach where business logic resides inside the Domain Entity, the answer is to apply Method Injection:

// ITravelService is injected into the public InsertSight method
public void InsertSight(Sight sight, int index, ITravelService travelService)
{
    ...
}

Method Injection injection is ideal, because constructing objects with both runtime data and dependencies (using Constructor Injection) causes all kinds of trouble. With Method Injection, instead, the consuming class, Tour, only uses the dependency, but never stores the dependency in any field.

Tips:

  • If you need the dependency in other private methods, pass the dependency from method to method
  • If your Domain method requires many dependencies, you'll experience a problem similar to the problem of constructor over-injection. Constructor over-injection is caused by a class that does too much. With method over-injection, that method does too much. There are many refactorings and design techniques that can be applied to solve this problem, such as the Facade Service refactoring, or designs such as Domain Events, which is a really powerful part of Domain-Driven Design.
like image 125
Steven Avatar answered Sep 21 '25 22:09

Steven