【问题标题】:Breaking SOLID Principles in multiple implementation of an Interface在接口的多个实现中打破 SOLID 原则
【发布时间】:2016-11-23 07:45:48
【问题描述】:

我在 factory 方法中遇到了依赖倒置的问题,它也违反了开放封闭原则。我的代码如下所示

    public interface IWriter
    {
        void WriteToStorage(string data);
    }

    public class FileWriter : IWriter
    {
        public void WriteToStorage(string data)
        {
            //write to file
        }
    }

    public class DBWriter : IWriter
    {
        public void WriteToStorage(string data)
        {
            //write to DB
        }
    }

现在我使用工厂类来解决对象创建问题。它看起来像下面的代码

public interface IFactory
{
    IWriter GetType(string outputType);
}

public class Factory : IFactory
{
    public IWriter GetType(string outputType)
    {
        IWriter writer = null;
        if (outputType.Equels("db"))
        {
            writer = new FileWriter();
        }
        else if (outputType.Equels("db"))
        {
            writer = new DBWriter();
        }
    }
}

现在的问题是Factory 类违反了开放封闭原则,因此它也违反了依赖倒置原则

然后

public interface ISaveDataFlow
{
    void SaveData(string data, string outputType);
}

public class SaveDataFlow : ISaveDataFlow
{
    private IFactory _writerFactory = null;
    public SaveDataFlow(IFactory writerFactory)
    {
        _writerFactory = writerFactory;
    }
    public void SaveData(string data, string outputType)
    {
        IWriter writer = _writerFactory.GetType(outputType);
        writer.WriteToStorage(data);
    }
}

由于上述工厂类破坏了依赖倒置,我删除了 Factory 类并更改了 SaveDataFlow 类,如下所示

public class SaveDataFlow : ISaveDataFlow
{
    private IWriter _dbWriter = null;
    private IWriter _fileWriter = null;
    public SaveDataFlow([Dependency("DB")]IWriter dbWriter,
                        [Dependency("FILE")]IWriter fileWriter)
    {
        _dbWriter = dbWriter;
        _fileWriter = fileWriter;
    }
    public void SaveData(string data, string outputType)
    {
        if (outputType.Equals("DB"))
        {
            _dbWriter.WriteToStorage(data);
        }
        else if (outputType.Equals("FILE"))
        {
            _fileWriter.WriteToStorage(data);
        }
    }
}

并使用 Unity 框架解决了这些依赖项

container.RegisterType<IWriter, DBWriter>("DB");
container.RegisterType<IWriter, FileWriter>("FILE");

但最终我还是打破了开放封闭原则。 我需要更好的设计/解决方案来解决这样的问题,但我必须遵循 SOLID 原则。

【问题讨论】:

  • 在我看来你不想要工厂。实际上,您需要一种策略模式。但是,如果您正在使用[Dependency("DB")],那么您将自己耦合到您的 DI 框架,这也不理想。
  • 为什么SaveDataFlow 需要处理字符串输入类型?你不能简单地注入合适的作家吗?至于工厂,如果您希望使其可扩展,那么您可以使用反射来扫描程序集并找到IWriter 的所有实现者,以便自动注册它们。

标签: c# dependency-injection unity-container solid-principles open-closed-principle


【解决方案1】:

我只想把它变成一种策略模式:

namespace UnityMutliTest
{
    using System;
    using System.Collections.Generic;
    using System.Linq;

    using Microsoft.Practices.Unity;

    class Program
    {
        static void Main(string[] args)
        {
            IUnityContainer container = new UnityContainer();

            container.RegisterType<IWriter, FileWriter>("file");
            container.RegisterType<IWriter, DbWriter>("db");

            container.RegisterType<IWriterSelector, WriterSelector>();

            var writerSelector = container.Resolve<IWriterSelector>();

            var writer = writerSelector.SelectWriter("FILE");

            writer.Write("Write me data");

            Console.WriteLine("Success");

            Console.ReadKey();
        }
    }

    interface IWriterSelector
    {
        IWriter SelectWriter(string output);
    }

    class WriterSelector : IWriterSelector
    {
        private readonly IEnumerable<IWriter> writers;

        public WriterSelector(IWriter[] writers)
        {
            this.writers = writers;
        }

        public IWriter SelectWriter(string output)
        {
            var writer = this.writers.FirstOrDefault(x => x.CanWrite(output));

            if (writer == null)
            {
                throw new NotImplementedException($"Couldn't find a writer for {output}");
            }

            return writer;
        }
    }

    interface IWriter
    {
        bool CanWrite(string output);

        void Write(string data);
    }

    class FileWriter : IWriter
    {
        public bool CanWrite(string output)
        {
            return output == "FILE";
        }

        public void Write(string data)
        {
        }
    }

    class DbWriter : IWriter
    {
        public bool CanWrite(string output)
        {
            return output == "DB";
        }

        public void Write(string data)
        {
        }
    }
}

您可以拥有任意数量的IWriters,只需注册即可:

container.RegisterType<IWriter, LogWriter>("log");

如果您愿意,您甚至可以在编写器上实现装饰器。

您使用(名字不好的)IWriterSelector 作为如何选择作家的实现,这应该只关注获得作家!这里的throw 异常非常有用,如果没有适合您需要的实现,它会很快失败!

如果您遇到过Open Closed 问题,请使用策略或模板模式来解决。

我一直使用这种模式,效果很好。

我创建了一个小扩展方法来避免你必须命名你的实例:

static class UnityExtensions
{
    public static void RegisterMultipleType<TInterface, TConcrete>(this IUnityContainer container)
    {
        var typeToBind = typeof(TConcrete);
        container.RegisterType(typeof(TInterface), typeToBind, typeToBind.Name);
    }
}

container.RegisterMultipleType<IWriter, FileWriter>();

【讨论】:

  • 当您使用 IEnumerable writers 时,Unity 是否本机了解要注入的内容?在以前的版本中,您需要使用数组。
  • 我安装了版本 4。这段代码取自一个正在运行的控制台程序
  • 酷。我从来没有让它工作。这就是为什么我在上面的备选方案 2 中建议了一个数组。很好的解决方案!
  • 好吧,我又跑了一次,出现异常,所以会改变
  • 就个人而言,我远离 Unity。这适用于没有命名依赖项的 Autofac、StructureMap、Ninject 和 IEnumerable&lt;Type&gt;
【解决方案2】:

解决方案 1

在实例化之前选择并使用作用域

using(var scope = new Scope(unity))
{
    scope.register<IWriter, ConcreteWriter>();
    var flow = scope.Resolve<ISaveDataFlow>();

}

解决方案 2

在运行时注入您的策略。

ISaveDataFlow flow = ....
IWriter writer = GetWriterBasedOnSomeCondition();
flow.SaveData(data, writer);

我怀疑解决方案 2 更接近您想要实现的目标。请记住,您不需要传递一个字符串来描述您要使用的strategy。

您可以改为传递您想要使用的实际 strategy,在这种情况下,您想要使用的实际 IWriter。

然后您可以做的是在每个 IWriter 上添加元数据,以帮助用户选择要使用的 IWriter。

例如

public interface IWriter
{
   void WriteData(data);
   string Name {get;}
}

void GetWriterBasedOnSomeCondition()
{
    Dictionary<string, IWriter> writers = ...ToDictionary(x => x.Name);
    var choice = Console.ReadLine();
    return writers[choice];
}

【讨论】:

  • 你不会有一个状态模式字典,你只需将它们全部绑定到依赖框架中并通过构造函数IEnumerable&lt;IWriter&gt;
  • @CallumLinington 你会的。但随后您使用字典将元数据映射回实际策略。或者,您可以使用List 中的位置...但是可以。 DI 将参与Chooser 类。
【解决方案3】:

我倾向于使用其中一种方法。

1.分成不同的界面

public interface IWriter
{
    void WriteToStorage(string data);
}

public interface IFileWriter : IWriter
{
}

public interface IDBWriter: IWriter
{
}

public class FileWriter : IFileWriter 
{
    public void WriteToStorage(string data)
    {
        //write to file
    }
}

public class DBWriter : IDBWriter
{
    public void WriteToStorage(string data)
    {
        //write to DB
    }
}

优点:您可以根据接口注入正确的实现,不会破坏 OCP。

缺点:你的接口是空的。


2。使用枚举来分隔它们(策略模式)

public interface IWriter
{
    void WriteToStorage(string data);
    StorageType WritesTo { get; }
}

public enum StorageType 
{
    Db = 1,
    File = 2
}

public class Factory : IFactory
{
    public IEnumerable<IWriter> _writers;

    public Factory(IWriter[] writers)
    {
        _writers = writers;
    }

    public IWriter GetType(StorageType outputType)
    {
        IWriter writer = _writers.FirstOrDefault(x => x.WritesTo == outputType);
        return writer;
    }
}

优点:您可以同时注入它们,然后使用枚举来使用您想要的那个。

缺点:我猜它有点像你的第一个例子那样违反了 OCP 原则。

Mark Seemann 的 this excellent answer 中有关策略模式的更多信息。


3.构建一个基于 func 创建项目的工厂。

在您的注册中:

container.RegisterType<IWriter, DBWriter>("DB");
container.RegisterType<IWriter, FileWriter>("FILE");
container.RegisterType<IFactory, Factory>(
    new ContainerControlledLifetimeManager(),
    new InjectionConstructor(
        new Func<string, IWriter>(
            writesTo => container.Resolve<IWriter>(writesTo));

还有你的工厂

public class Factory : IFactory
{
    private readonly Func<string, IWriter> _createFunc;

    public Factory(Func<string, IWriter> createFunc)
    {
        _createFunc = createFunc;
    }

    public IWriter CreateScope(string writesTo)
    {
        return _createFunc(writesTo);
    }
}

优点:将整个依赖移动到注册中。

缺点:服务定位器模式的包装器。可能有点难以阅读。


以上示例都不是完美的,因为每个示例都有其优点和缺点。

这里有类似的问题: Inject require object depends on condition in constructor injection

【讨论】:

    【解决方案4】:

    在 .NET Core 中(从问题中不清楚正在使用什么框架),您可以使用内置的 DI 以非常少的代码轻松实现策略模式。

    在Startup.ConfigureServices:

    services
        .AddScoped<IWriter, FileWriter>()
        .AddScoped<IWriter, DBWriter>()
        .AddScoped<ISaveDataFlow, SaveDataFlow>();
    

    为IWriter添加一个方法用于策略算法:

    public interface IWriter
    {
        bool CanWrite(string outputType);
        void WriteToStorage(string data);
    }
    
    public class FileWriter : IWriter
    {
        bool CanWrite(string outputType) => outputType == "FILE";
        public void WriteToStorage(string data) {}
    }
    
    public class DBWriter : IWriter
    {
        bool CanWrite(string outputType) => outputType == "DB";
        public void WriteToStorage(string data) {}
    }
    

    然后将SaveDataFlow的构造函数改为使用集合类型,将SaveData改为调用所有解析的IWriter类型的算法方法。

    public class SaveDataFlow : ISaveDataFlow
    {
        private readonly IWriter _writers;
    
        public SaveDataFlow(IEnumerable<IWriter> writers)
        {
            _writers= writers;
        }
    
        public void SaveData(string data, string outputType)
        {
            _writers.Single(w => w.CanWrite(outputType)).WriteToStorage(data);
        }
    }
    

    这现在符合开放/封闭原则,因为具体选择仅在具体类本身内。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多