关联来自不同基础 类 的派生 类 时避免双向依赖

Avoiding two-way dependency when correlating derived classes from different base classes

我正在研究一个模型,该模型可以用一堆不同的车辆做一些事情。 每辆载具都应该做一些事情,但每种载具类型都做不同的事情。 所以我使用 .NET Framework 以这种方式实现它:

abstract class Vehicle
{
   abstract void DoStuff()
}

class Car : Vehicle
{
   override void DoStuff()
   {
       //Do some Car stuff here
   }
}

class Motorcycle : Vehicle
{
   override void DoStuff()
   {
       //Do some Motorcycle stuff here
   }
}
class Model
{
  RunModel(Vehicle[] vehicleCollection)
  {
    foreach(Vehicle currentVehicle in vehicleCollection)
    {
      currentVehicle.DoStuff()
    }
  }
}

这是我的程序的核心功能,它按预期工作。 现在我应该根据每辆车所做的事情输出报告。每种类型的车辆都应该输出不同类型的报告,所以我为它做了一个类似的解决方案:

abstract class Vehicle
{
   abstract void DoStuff();
   abstract Report GetReport();
}

class Car : Vehicle
{
   override Report GetReport()
   {
       return new CarReport(this);
   }
}

class Motorcycle : Vehicle
{
   override Report GetReport()
   {
       return new MotorcycleReport(this);
   }
}

abstract class Report
{
   int Foo {get; set;}

   Report (Vehicle _vehicle)
   {
       Foo = _vehicle.CommonProperty;
   }
      
}

class CarReport : Report
{
   string Bar {get; set;}
   CarReport(Car _car) : base(_car)
   {
       Bar = _car.CarPropoerty;
   }
}

class MotorcycleReport : Report
{
   bool Baz {get; set;}
   MotorcycleReport(Motorcycle _cycle) : base(_cycle)
   {
       Baz= _cycle.MotorcyclePropoerty;
   }
}
class Model
{
  RunModel(Vehicle[] vehicleCollection)
  {
    foreach(Vehicle currentVehicle in vehicleCollection)
    {
      currentVehicle.DoStuff()
      currentVehicle.GetReport()
    }
  }
}

这也工作正常,但问题是 Car 和 Motorcycle 现在依赖于 CarReport 和 MotorcycleReport。由于这是我程序的非核心功能,并且报告结构在未来的版本中可能会发生很大变化,因此我想以报告依赖于车辆但车辆不依赖于报告的方式实现它。

我尝试了一种外部重载方法,该方法获取 Vehicle 并输出正确的 Report 或者将抽象报告(或接口 IReport)传递给车辆“GetReport”方法 但是由于我的 RunModel 方法不知道它处理的是什么类型的 Vehicle,所以我找不到将其映射到正确的 Report 类型的方法。

有没有办法避免这种双向依赖?

依赖注入可能会有所帮助。 .Net Core 中内置的依赖注入不提供在 IReport 的两个不同实现之间切换的选项,但您可以将 ICarReport 的实现注入 Class Car 并将 IMotorcycleReport 的实现注入 Class Motorcycle。然后,如果实现发生变化,您可以在不更改依赖于它们的 Classes 的情况下换出实现。

还有其他 IoC 容器,例如 Lightinject,它们允许您注入 IReport 的不同实现,称为命名依赖项。您可能想要搜索类似的内容。

此外,我不确定您使用的是 .Net Core 还是 .Net Framework。 .Net Core 内置了依赖注入,但您需要为 .Net Framework 安装像 Lightinject 或 Ninject 这样的 Nuget 包。

编辑:

听起来您正在寻找一种设计模式来实现控制反转 (IoC)。在这种情况下,正如不同答案所指出的,您可以使用 工厂模式 服务定位器模式 依赖注入模式.

如果您的项目很旧或已经非常大,依赖注入可能不适合您。在那种情况下,它可能是您下一个项目需要研究的东西。在这种情况下,工厂模式可能正是您要寻找的。这一切都取决于很多我们目前还不知道的细节。

此外,对于不同的模式会有不同的看法,但通常有许多模式可用于解决特定的设计问题。

您可以使用泛型在报告及其相应车辆之间创建 link。根据此信息,您可以创建基于车辆的报告。

车辆和报告是这样的:

    public abstract class Vehicle
    {

    }
    public class Car : Vehicle
    {

    }
    public class Motorcycle : Vehicle
    {

    }

    public abstract class Report
    {

    }

    public abstract class Report<T> : Report where T:Vehicle
    {
        int Foo { get; set; }

        public Report(T _vehicle)
        {

        }
    }
    public class CarReport : Report<Car>
    {
        string Bar { get; set; }
        public CarReport(Car _car) : base(_car)
        {

        }
    }
    public class MotorcycleReport : Report<Motorcycle>
    {
        bool Baz { get; set; }
        public MotorcycleReport(Motorcycle _cycle) : base(_cycle)
        {

        }
    }

1 - 我们可以使用反射根据当前 Vehicle:

生成 Report 对象
public abstract class Vehicle
    {
        public Report GetReport()
        {
            var genericReportType = typeof(Report<>).MakeGenericType(this.GetType());
            var reportType = Assembly.GetExecutingAssembly().GetTypes().Where(x => genericReportType.IsAssignableFrom(x)).Single();
            return Activator.CreateInstance(reportType, this) as Report;
        }
    }

如果我们需要优化性能,我们可以缓存一个字典:

public abstract class Vehicle
    {
        private static Dictionary<Type, Type> vehicleToReport;

        static Vehicle()
        {
            var reports = Assembly.GetExecutingAssembly().GetTypes().Where(x => typeof(Report).IsAssignableFrom(x) && x.IsAbstract == false);

            vehicleToReport = reports.ToDictionary(x => x.BaseType.GetGenericArguments().Single(), x => x);
        }
        public Report GetReport()
        {
            var reportType = vehicleToReport[this.GetType()];
            return Activator.CreateInstance(reportType, this) as Report;
        }
    }

I'd like to implement it in a way that the Reports depends on the Vehicles, but the Vehicles do not depend on the Reports.

2 - 如果您想从 Vehicle 中完全删除 Report 依赖项。您可以创建工厂 class 并将 GetReport 方法从 Vehicle class 移动到此工厂方法。

你可以实现工厂方法(我们通常称之为工厂设计模式)。有 2 个选项可以实现这个工厂方法:

a) 由于报告和车辆的通用实施,使用上述反射动态发现新车辆的新报告。

b) 只需硬编码下面的映射 vehicleToReport 即可将 Vehicle 映射到 Report:

public class ReportFactory
    {
        private static Dictionary<Type, Type> vehicleToReport;

        static ReportFactory()
        {
            //Build the mappings dynamically using reflection or just hardcode it.
            var reports = Assembly.GetExecutingAssembly().GetTypes().Where(x => typeof(Report).IsAssignableFrom(x) && x.IsAbstract == false);

            vehicleToReport = reports.ToDictionary(x => x.BaseType.GetGenericArguments().Single(), x => x);
        }
        public Report GetReport(Vehicle vehicle)
        {
            var reportType = vehicleToReport[vehicle.GetType()];
            return Activator.CreateInstance(reportType, vehicle) as Report;
        }
    }

您关于保持核心域尽可能简单的观点是正确的。它应该只需要处理它自己的复杂性,尽可能少的来自外部的干扰和依赖。

首先想到的是,尽管继承对于 Vehicle 层次结构可能有意义。问题是,这对报告有意义吗?你会单独使用抽象基 Report class 吗?只有共同属性的那个。

如果是

您可以使用经理来接管创建 Reports 的责任。

public class ReportManager
{
    public Report GetReport<T>(T vehicle) where T : Vehicle
    {
        switch (vehicle)
        {
            case Car car:
                return new CarReport(car);

            case Motorcycle motorcycle:
                return new MotorcycleReport(motorcycle);

            default:
                throw new NotImplementedException(vehicle.ToString());
        }
    }
}

你可以这样使用它。

public class Model
{
    private readonly ReportManager _reportManager;

    public Model(ReportManager reportManager)
    {
        _reportManager = reportManager;
    }

    public List<Report> RunModel(Vehicle[] vehicles)
    {
        var reports = new List<Report>();

        foreach (var vehicle in vehicles)
        {
            vehicle.DoStuff();
            reports.Add(_reportManager.GetReport(vehicle));
        }

        return reports;
    }
}

如果没有

您可以将工作分成两个单独的流程。

public class Model
{
    public List<CarReport> CarReports { get; private set; }
    public List<MotorcycleReport> MotorcycleReports { get; private set; }

    public void RunModel(Vehicle[] vehicles)
    {
        // 1. Do stuff
        foreach (var vehicle in vehicles)
        {
            vehicle.DoStuff();
        }
        // 2. Get reports
        CarReports = vehicles.OfType<Car>().Select(car => new CarReport(car)).ToList();
        MotorcycleReports = vehicles.OfType<Motorcycle>().Select(motorcycle => new MotorcycleReport(motorcycle)).ToList();
    }
}

区别

第一种方法 return 是一个基列表 class。第二种方法在对象上存储不同类型的列表。一旦你有不同的类型,你就不能再 return 类型集合中的它们而不先向上转型。

最后的想法

Report structure may change a lot in future versions

您可以在 Vehicle 上实现枚举 ReportType。想象一下未来需要为肌肉车和家用车创建不同的报告。无需深入研究继承,您就可以仅根据枚举值生成不同的报告。

您可以使用 Factory class 删除您的依赖项。这是示例代码。

class Program
{

    static void Main(string[] args)
    {
        // Set your Report Builder Factory (Concrete / Dynamic)
        ConcretReportBuilderFactory concreteReportBuilderFactory = new ConcretReportBuilderFactory();

        DynamicReportBuilderFactory dynamicReportBuilderFactory = new DynamicReportBuilderFactory();

        Vehicle[] vehicleCollection = new Vehicle[]
        {
            new Car(concreteReportBuilderFactory),
            new Motorcycle(dynamicReportBuilderFactory)
        };

        RunModel(vehicleCollection);

        Console.ReadKey();
    }

    static void RunModel(Vehicle[] vehicleCollection)
    {
        foreach (Vehicle currentVehicle in vehicleCollection)
        {
            currentVehicle.DoStuff();
            var vehicleReport = currentVehicle.GetReport();
        }
    }
}

public abstract class Vehicle
{
    protected readonly ReportBuilderFactory reportBuilderFactory;
    // I'm using Constructor Injection, but you can use Property or Method injection 
    // if you want to free your constructor remaining parameter less.
    public Vehicle(ReportBuilderFactory reportBuilderFactory)
    {
        this.reportBuilderFactory = reportBuilderFactory;
    }

    public abstract void DoStuff();
    public abstract Report GetReport();
    public string CommonProperty { get; set; }
}

public class Car : Vehicle
{
    public Car(ReportBuilderFactory reportBuilderFactory) : base(reportBuilderFactory)
    {
    }
    public override void DoStuff()
    {
        //Do some Car stuff here
    }

    public override Report GetReport()
    {
        return this.reportBuilderFactory.GetReport(this);
    }
}

public class Motorcycle : Vehicle
{
    public Motorcycle(ReportBuilderFactory reportBuilderFactory) : base(reportBuilderFactory)
    {
    }
    public override void DoStuff()
    {
        //Do some Motorcycle stuff here
    }
    public override Report GetReport()
    {
        var report = this.reportBuilderFactory.GetReport(this);
        return report;
    }
}

public abstract class Report
{
    public Report(Vehicle vehicle)
    {
        Foo = vehicle.CommonProperty;
    }
    public string Foo { get; set; }

    public virtual void ShowReport()
    {
        Console.WriteLine("This is Base Report");
    }
}

[ReportFor("Car")] // (Pass class name as argument) .For the implementation of DynamicReportBuilderFactory.
public class CarReport : Report
{
    string Bar { get; set; }
    public CarReport(Car _car) : base(_car)
    {
        Bar = _car.CommonProperty;
    }
    public override void ShowReport()
    {
        Console.WriteLine("This is Car Report.");
    }
}

[ReportFor("Motorcycle")] // (Pass class name as argument) .For the implementation of DynamicReportBuilderFactory
public class MotorcycleReport : Report
{
    public MotorcycleReport(Vehicle vehicle) : base(vehicle)
    {
    }

    public override void ShowReport()
    {
        Console.WriteLine("This is Motor Cycle Report.");
    }
}    

[AttributeUsage(AttributeTargets.Class)]
public class ReportFor : Attribute
{
    public string ReportSource { get; private set; }
    public ReportFor(string ReportSource)
    {
        this.ReportSource = ReportSource;
    }
}

public abstract class ReportBuilderFactory
{
    public abstract Report GetReport(Vehicle vehicle);
}

// Static Implementation . this is tightly coupled with Sub Classes of Report class.
public sealed class ConcretReportBuilderFactory : ReportBuilderFactory
{
    public override Report GetReport(Vehicle vehicle)
    {
        switch (vehicle)
        {
            case Car car:
                return new CarReport(car);

            case Motorcycle motorcycle:
                return new MotorcycleReport(motorcycle);

            default:
                throw new NotImplementedException(vehicle.ToString());
        }
    }
}

// Dynamic Implementation . this is loosely coupled with Sub Classes of Report class.
public sealed class DynamicReportBuilderFactory : ReportBuilderFactory
{
    private readonly Dictionary<string, Type> _availableReports;

    public DynamicReportBuilderFactory()
    {
        _availableReports = GetAvailableReportTypes();
    }

    static Dictionary<string, Type> GetAvailableReportTypes()
    {
        var reports = Assembly.GetExecutingAssembly()
                         .GetTypes().Where(t => typeof(Report).IsAssignableFrom(t)
                                             && t.IsAbstract == false
                                             && t.IsInterface == false
                                             // Geting classes which have "ReportFor" attribute
                                             && t.GetCustomAttribute<ReportFor>() != null 
                                         );

        // You can raise warning or log Report derived classes which dosn't have "ReportFor" attribute
       // We can split ReportSource property contains "," and register same type for it . Like "CarReport, BikeReport"

        return reports.ToDictionary(x => x.GetCustomAttribute<ReportFor>()?.ReportSource);
    }

    public override Report GetReport(Vehicle vehicle)
    {
        var reportType = _availableReports[vehicle.GetType().Name];
        return Activator.CreateInstance(reportType,vehicle) as Report;
    }
}

为了避免 two-way 依赖,Vehicles 应该依赖抽象。以及报告。正如智者所说:“高级模块不应该依赖于低级模块。两者都应该依赖 基于抽象。” 这是我的建议,它是为了尽可能少地干扰您的示例程序而提出的。 Model.RunModel 方法仍然不知道它处理的是什么类型的 Vehicle,这没关系。 CarReportGenerator(和 MotorcycleReportGenerator)中的 GenerateReport 方法是对具体报告执行具体操作的正确位置。报告结构的未来变化应该在那里解决。

让我们切入代码:

 abstract class Vehicle
    {
        public int CommonProperty { get; set; }

        public abstract void DoStuff();

        public abstract Report GetReport();

    }

    class Car : Vehicle
    {
        public string CarProperty { get; set; }

        public override void DoStuff()
        {
            //Do some Car stuff here
            Console.WriteLine("Doing Car stuff here.");
        }

        public override Report GetReport()
        {
            // Injecting dependency
            CarReportGenerator crpt = new CarReportGenerator(this);
            
            return crpt.GenerateReport();
        }
    }

    class Motorcycle : Vehicle
    {
        public bool MotorcycleProperty { get; set; }

        public override void DoStuff()
        {
            //Do some Motorcycle stuff here
            Console.WriteLine("Doing Motorcycle stuff here.");
        }

        public override Report GetReport()
        {
            // Injecting dependency
            MotorcycleReportGenerator mrpt = new MotorcycleReportGenerator(this);
            return mrpt.GenerateReport();
        }
    }

    abstract class Report
    {
        public int Foo { get; set; }
    }

    class CarReport : Report
    {
        public string Bar { get; set; }
    }

    class MotorcycleReport : Report
    {
        public bool Baz { get; set; }
    }

    class Model
    {
        internal void RunModel(Vehicle[] vehicleCollection)
        {
            foreach (Vehicle currentVehicle in vehicleCollection)
            {
                currentVehicle.DoStuff();
                //currentVehicle.GetReport();
                Report rpt = currentVehicle.GetReport();
                Console.WriteLine(rpt.Foo);
            }
        }
    }

    interface IReportGenerator
    {
        Report GenerateReport();
    }

    class CarReportGenerator : IReportGenerator
    {
        private Car _car;
        
        public CarReportGenerator(Vehicle car)
        {
            _car = (Car)car;
        }

        public Report GenerateReport()
        {
            CarReport crpt = new CarReport();

            // acces to commom Property from Vehicle
            crpt.Foo = _car.CommonProperty;
            // acces to concrete Car Property from Car
            crpt.Bar = _car.CarProperty;
            // go on with report, print, email, whatever needed

            return crpt;
        }
    }

    class MotorcycleReportGenerator : IReportGenerator
    {
        private Motorcycle _motorc;

        public MotorcycleReportGenerator(Vehicle motorc)
        {
            _motorc = (Motorcycle)motorc;
        }

        public Report GenerateReport()
        {
            MotorcycleReport mrpt = new MotorcycleReport();

            // acces to commom Property from Vehicle
            mrpt.Foo = _motorc.CommonProperty;
            // acces to concrete Motorcycle Property from Motorcycle
            mrpt.Baz = _motorc.MotorcycleProperty;
            // go on with report, print, email, whatever needed

            return mrpt;
        }
    }

Vehicles 和 Reports 之间的依赖关系消失了。如果以后要添加一种新的车辆和报告,应该在不改变现在工作的情况下完成。

// A whole new vehicle, report and report generator.

    class Quad : Vehicle
    {
        public double QuadProperty { get; set; }

        public override void DoStuff()
        {
            //Do some Quad stuff here
            Console.WriteLine("Doing Quad stuff here.");
        }

        public override Report GetReport()
        {
            // Injecting dependency
            QuadReportGenerator crpt = new QuadReportGenerator(this);

            return crpt.GenerateReport();
        }
    }

    class QuadReport : Report
    {
        public double Doe { get; set; }
    }
    class QuadReportGenerator : IReportGenerator
    {
        private Quad _quad;

        public QuadReportGenerator(Vehicle quad)
        {
            _quad = (Quad)quad;
        }

        public Report GenerateReport()
        {
            QuadReport crpt = new QuadReport();

            // acces to commom Property from Vehicle
            crpt.Foo = _quad.CommonProperty;
            // acces to concrete Quad Property from Quad
            crpt.Doe = _quad.QuadProperty;
            // go on with report, print, email, whatever needed

            return crpt;
        }
    }

但是,Vehicles 和 ReportGenerators 之间存在新的依赖关系。它可以通过为车辆 IVehicle 和报告 IReport 创建一个接口来解决,因此 Vehicles 和 ReportGenerators 依赖于它。您可以更进一步,创建一个新接口 IVehicleReport,其中包含 Report GetReport() 方法。这样,车辆和报告的当前和未来关注点就会分开。