如何向域实体注入服务以及如何持久化实体

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

我正在创建旅游规划器。 Tour 始终在同一坐标开始和结束。在开始和结束之间,有Stops。停靠点包含特定的 Sight 在游览期间参观 + 参观时间。在游览中添加景点时,我插入了一个新的停靠点,并根据停靠点之间的距离重新计算停靠点的到达时间。为此,我在 private recalculateStopTimes(int fromIdx) 方法中使用注入的 ITravelService 实例。


我的问题如下:在我想通过 ORM 将 Tour 对象保存在数据库中之前,这一直有效。由于私有 ITravelService,该功能将在检索后丢失。我考虑过通过 InsertSight/RemoveSight/RemoveStop 方法注入服务,但随后我需要用我创建的每个 public 方法注入它,这会修改 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();
            }
        }
    }

实体代表数据状态,应该只代表数据状态。让您的重新计算服务接收实体,检查并在需要时修改它们。除了验证数据级别的关注点(必需与可选、字符串长度等)之外,业务逻辑应该保持在实体之外。

您可能会遇到的一个问题是您依赖游览中的停靠点顺序。根据实体中集合的顺序,这很可能不可靠,除非您在插入和删除停止点时明确引入类似 StopNumber 的内容,并在使用子集合时使用 OrderBy 子句。添加或删除停靠点的操作涉及 inserting/removing 元素和更新行程时间。这也可以移到服务中。

public interface ITravelService
{
    public void UpdateTravelTimes(Tour tour);
    public void AddSightStopToTour(Tour tour, Sight sight);
    public void RemoveSightStopFromTour(Tour tour, Sight sight);
}

我什至建议避免将实体传递到方法中,而是传递实体的 ID。该服务可以保存对 Repositories 或 DbContext 的依赖关系,这确保了实体加载了他们急切加载的必要集合,而不是可能在没有它的 Stops 的情况下接收 Tour 然后触发延迟加载。

public interface ITravelService
{
    public Tour UpdateTravelTimes(int tourId);
    public Tour AddSightStopToTour(int tourId, int sightId);
    public Tour RemoveSightStopFromTour(int tourId, int sightId);
}

public class TravelService : ITravelSerivce
{
    public Tour UpdateTravelTimes(int tourId, int fromIndex)
    {
        if(fromIndex < 0) 
            throw new IndexOutOfRangeException("fromIndex < 0");

        var tour = Context.Tours
            .Include(x => x.Stops)
            .Single(x => x.TourId == tourId);
           
        if(fromIndex >= tour.Stops.Count())
            return tour; // Nothing to do.

        var orderedStops = tour.Stops
            .OrderBy(x => x.StopNumber)
            .ToList();

        for (int i = fromIndex; i < orderedStops.Count - 1; i++)
        {
            Stop currentStop = orderedStops[i];
            Stop nextStop = orderedStops[i + 1];
            var travelTime = calculateTimeBetween(currentStop.Coordinates, nextStop.Coordinates);
            nextStop.Arrival = currentStop.TimeRange.To + travelTime;
        }

        return tour;
    }
}

以上示例假设 DbContext 是作为依赖项注入的,或者可以从工作单元中解析。

添加和删除停靠站将加载游览和停靠站集合,分别插入/删除停靠站,然后调整 insert/remove 后剩余停靠站的 StopNumber,并重新计算到达时间。然后可以返回游览,其停靠点已准备好用于填充视图模型或发送回视图。

如果您想坚持业务逻辑驻留在域实体内部的 DDD 方法,答案是应用 Method Injection:

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

Method Injection 注入是理想的,因为构造具有运行时数据和依赖项(使用构造函数注入)的对象会导致 all kinds of trouble。相反,使用方法注入时,消耗 class、Tour 使用 依赖项,但从不 存储 依赖项在任何领域。

提示:

  • 如果您需要其他私有方法中的依赖项,将依赖项从一个方法传递到另一个方法
  • 如果您的域方法需要很多依赖项,您会遇到类似于 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 的问题,这是域驱动设计的一个非常强大的部分。