【问题标题】:Avoiding two-way dependency when correlating derived classes from different base classes在关联来自不同基类的派生类时避免双向依赖
【发布时间】:2020-12-02 05:12:00
【问题描述】:

我正在开发一个模型,它可以用一堆不同的车辆来做一些事情。 每辆车都应该做一些事情,但每种车辆类型做不同的事情。 所以我以这种方式实现了它,使用 .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 并输出正确的报告 或将抽象报告(或接口 IReport)传递给 Vehicle “GetReport” 方法 但由于我的 RunModel 方法不知道它正在处理什么类型的 Vehicle,我找不到将其映射到正确报告类型的方法。

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

【问题讨论】:

  • 你在这里使用 .NET Core 还是 .NET Framework?而且我假设您提供的 sn-p 中带有MotorcycleReport 的构造函数是错字吗?
  • 感谢您的评论和编辑。确实是 MotorcycleReport 在那一行。我正在使用 .NET Framework,我会将其添加到帖子中!
  • 在我的回答中添加了一种从Vehicle 中完全删除Report 依赖项的方法,请看看它是否是您所需要的

标签: c# dependencies polymorphism


【解决方案1】:

您可以使用泛型在报告与其对应的车辆之间创建链接。根据这些信息,您可以创建基于车辆的报告。

车辆和报告是这样的:

    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;
        }
    }

我想以报告依赖于 车辆,但车辆不依赖于报告。

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

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

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;
        }
    }

【讨论】:

  • 如果没有 Car and Motorcycle 的实现,你就无法编译这段代码。
  • @Biju Kalanjoor: Car and Motorcycle 的实现在上面,就在选项1中。我想我会更新答案以使其更清晰
  • 在 x.BaseType.GetGenericArgumemts () 中运行此 ReportFactory 时出错,返回 null。
  • 我还注意到了一件事情。如果用户想为另一辆车(如奔驰)使用相同的汽车报告。在这种情况下,我们需要移除 CarReport 和 Car 类之间的依赖关系
  • @Biju Kalanjoor:请看一下小提琴:dotnetfiddle.net/85F9hz。有一个小错误
【解决方案2】:

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

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;
    }
}

【讨论】:

    【解决方案3】:

    为了避免双向依赖,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()。这样,车辆和报告的当前和未来问题就可以分开。

    【讨论】:

    • 您的 Quad 类依赖于 QuadReportGenerator 类。
    【解决方案4】:

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

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

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

    编辑:

    听起来您正在寻找一种设计模式来实现控制反转 (IoC)。在这种情况下,正如不同答案所指出的那样,您可以使用 Factory PatternService Locator PatternDependency Injection Pattern

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

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

    【讨论】:

    • DI 不是解决 OP 问题的正确建议。 @funk 提供的工厂设计模式实现更多的是正确的路径。也许在那之后,这就是图片
    • 听起来 OP 正在尝试解耦车辆类和报告类之间的联系。工厂模式和 DI 的主要区别在于对象引用的获取方式。顾名思义,依赖注入意味着引用被注入或提供给您的代码。使用工厂模式,您的代码必须请求引用,以便您的代码获取对象。两种实现都删除或解耦代码与代码使用的对象引用的底层类或类型之间的链接 (stackoverflow.com/a/22384293/5187440)。
    【解决方案5】:

    保持核心域尽可能简单是对的。它应该只需要处理其自身的复杂性,并尽量减少来自外部的干扰和依赖。

    首先想到的是,即使继承对于Vehicle 层次结构可能有意义。问题是,这对报告有意义吗?你会单独使用抽象基类Report 吗?仅具有共同属性的一种。

    如果是的话

    您可以使用经理来接管创建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();
        }
    }
    

    区别

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

    最后的想法

    报告结构在未来版本中可能会有很大变化

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

    【讨论】:

    • 在第一个解决方案中,我认为使用访问者模式而不是使用 switch 语句更健壮,因为由于编译时错误,不可能忘记新具体类的实现
    • 您当然可以在这里实现一些 GoF 行为模式,但我选择保持简单。最好不要过度设计解决方案,添加设计模式也会增加复杂性。我把它留给实施者来决定是否值得。
    • 从赏金页面访问,想知道您是从哪里了解到这个解决方案的!我经常遇到这类数据建模问题,而作为一名自学成才的程序员,尝试用通用语框定问题似乎徒劳无功。对于这类事情,你有什么推荐的文字吗?
    • @mehdi.loa:不使用 switch 语句的想法类似于我在下面的回答(选项 2)。只是我使用工厂方法设计模式而不是访问者,我们不会忘记新的具体类的实现。
    猜你喜欢
    • 2012-03-08
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-08-12
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多