具有不同输入参数的工人的C#设计模式


14

我不确定哪种设计模式可以帮助我解决此问题。

我有一个类“ Coordinator”,该类确定应使用哪种Worker类-不必知道那里有所有不同类型的Worker-它仅调用WorkerFactory并根据通用IWorker接口起作用。

然后,它将适当的Worker设置为工作并返回其“ DoWork”方法的结果。

到目前为止还不错。我们对新的Worker类有一个新的要求,即“ WorkerB”,它需要额外的信息量,即额外的输入参数,才能使其工作。

就像我们需要一个带有额外输入参数的DoWork方法重载...但是所有现有的Workers都必须实现该方法-这似乎是错误的,因为这些Workers确实不需要该方法。

我该如何重构,以使协调器不知道正在使用哪个工作人员,并且仍然允许每个工作人员获取完成其工作所需的信息,但没有任何工作人员执行不需要的信息?

已经有很多现有的工人。

我不想更改任何现有的具体Workers来适应新WorkerB类的要求。

我以为Decorator模式在这里会很好,但是我之前没有看到任何Decorator用相同的方法但参数不同来装饰对象...

代码情况:

public class Coordinator
{
    public string GetWorkerResult(string workerName, int a, List<int> b, string c)
    {
        var workerFactor = new WorkerFactory();
        var worker = workerFactor.GetWorker(workerName);

        if(worker!=null)
            return worker.DoWork(a, b);
        else
            return string.Empty;
    }
}

public class WorkerFactory
{
    public IWorker GetWorker(string workerName)
    {
        switch (workerName)
        {
            case "WorkerA":
                return new ConcreteWorkerA();
            case "WorkerB":
                return new ConcreteWorkerB();
            default:
                return null;
        }
    }
}

public interface IWorker
{
    string DoWork(int a, List<int> b);
}

public class ConcreteWorkerA : IWorker
{
    public string DoWork(int a, List<int> b)
    {
        // does the required work
        return "some A worker result";
    }
}

public class ConcreteWorkerB : IWorker
{
    public string DoWork(int a, List<int> b, string c)
    {
        // does some different work based on the value of 'c'
        return "some B worker result";
    }

    public string DoWork(int a, List<int> b)
    {
        // this method isn't really relevant to WorkerB as it is missing variable 'c'
        return "some B worker result";
    }    
}

IWorker界面中列出的旧版本,或者是一个新的版本,与添加的参数?
JamesFaix

您代码库中当前使用带有2个参数的IWorker的位置是否需要插入第3个参数,还是仅新的调用站点将使用第3个参数?
JamesFaix

2
与其去购物,不考虑是否应用模式,都应着眼于整体设计。推荐读物:“购买模式”类型的问题有多严重?

1
根据您的代码,在创建IWorker实例之前,您已经知道所需的所有参数。因此,您应该将那些参数传递给构造函数而不是DoWork方法。IOW,请使用您的工厂类。隐藏构造实例的细节几乎是工厂类存在的主要原因。如果您采用这种方法,那么解决方案将是微不足道的。另外,您尝试以尝试完成的方式完成的操作很糟糕。它违反了Liskov替代原则。
扣篮

1
我认为您必须回到另一个层次。Coordinator已经必须更改以在其GetWorkerResult功能中容纳该额外参数-这意味着违反了SOLID的Open-Closed-Principle。结果,所有代码调用Coordinator.GetWorkerResult也必须更改。因此,请看一下调用该函数的位置:如何确定要请求哪个IWorker?这可能会导致更好的解决方案。
伯恩哈德·希勒

Answers:


9

您将需要对参数进行一般化,以使它们适合于具有基本接口和可变数量的字段或属性的单个参数。有点像这样:

public interface IArgs
{
    //Can be empty
}

public interface IWorker
{
    string DoWork(IArgs args);
}

public class ConcreteArgsA : IArgs
{
    public int a;
    public List<int> b;
}

public class ConcreteArgsB : IArgs
{
    public int a;
    public List<int> b;
    public string c;
}

public class ConcreteWorkerA : IWorker
{
    public string DoWork(IArgs args)
    {
        var ConcreteArgs = args as ConcreteArgsA;
        if (args == null) throw new ArgumentException();
        return "some A worker result";
    }
}

public class ConcreteWorkerB : IWorker
{
    public string DoWork(IArgs args)
    {
        var ConcreteArgs = args as ConcreteArgsB;
        if (args == null) throw new ArgumentException();
        return "some B worker result";
    }
} 

请注意null检查...因为您的系统是灵活的且后期绑定的,所以它也不是安全的类型,因此您将需要检查强制类型转换以确保传递的参数有效。

如果您确实不想为每种可能的参数组合都创建具体对象,则可以使用元组(这不是我的首选)。

public string GetWorkerResult(string workerName, object args)
{
    var workerFactor = new WorkerFactory();
    var worker = workerFactor.GetWorker(workerName);

    if(worker!=null)
        return worker.DoWork(args);
    else
        return string.Empty;
}

//Sample call
var args = new Tuple<int, List<int>, string>(1234, 
                                             new List<int>(){1,2}, 
                                             "A string");    
GetWorkerResult("MyWorkerName", args);

1
这类似于Windows Forms应用程序处理事件的方式。1个“ args”参数和一个“事件源”参数。所有“ args”都从EventArgs子类化:msdn.microsoft.com/en-us/library/…- >我要说的是,这种模式非常有效。我只是不喜欢“元组”的建议。
马查多

if (args == null) throw new ArgumentException();现在,每个IWorker使用者都必须知道其具体类型-接口是无用的:您也可以摆脱它,而改用具体类型。那是个坏主意,不是吗?
伯恩哈德·希勒

由于具有可插入的体系结构,因此需要IWorker接口(WorkerFactory.GetWorker只能具有一种返回类型)。尽管不在本示例的讨论范围之内,但我们知道调用者可以提出一个workerName;。大概也可以提出适当的论据。
约翰·吴

2

我已经基于@Dunk的评论重新设计了解决方案:

...您已经知道创建IWorker实例之前需要的所有参数。因此,您应该将那些参数传递给构造函数而不是DoWork方法。IOW,请使用您的工厂类。隐藏构造实例的细节几乎是工厂类存在的主要原因。

因此,我已经将创建IWorker所需的所有可能参数转移到IWorerFactory.GetWorker方法中,然后每个工作人员已经拥有了所需的参数,并且协调器可以只调用worker.DoWork();。

    public interface IWorkerFactory
    {
        IWorker GetWorker(string workerName, int a, List<int> b, string c);
    }

    public class WorkerFactory : IWorkerFactory
    {
        public IWorker GetWorker(string workerName, int a, List<int> b, string c)
        {
            switch (workerName)
            {
                case "WorkerA":
                    return new ConcreteWorkerA(a, b);
                case "WorkerB":
                    return new ConcreteWorkerB(a, b, c);
                default:
                    return null;
            }
        }
    }

    public class Coordinator
    {
        private readonly IWorkerFactory _workerFactory;

        public Coordinator(IWorkerFactory workerFactory)
        {
            _workerFactory = workerFactory;
        }

        // Adding 'c' breaks Open/Closed principal for the Coordinator and WorkerFactory; but this has to happen somewhere...
        public string GetWorkerResult(string workerName, int a, List<int> b, string c)
        {
            var worker = _workerFactory.GetWorker(workerName, a, b, c);

            if (worker != null)
                return worker.DoWork();
            else
                return string.Empty;
        }
    }

    public interface IWorker
    {
        string DoWork();
    }

    public class ConcreteWorkerA : IWorker
    {
        private readonly int _a;
        private readonly List<int> _b;

        public ConcreteWorkerA(int a, List<int> b)
        {
            _a = a;
            _b = b;
        }

        public string DoWork()
        {
            // does the required work based on 'a' and 'b'
            return "some A worker result";
        }
    }

    public class ConcreteWorkerB : IWorker
    {
        private readonly int _a;
        private readonly List<int> _b;
        private readonly string _c;

        public ConcreteWorkerB(int a, List<int> b, string c)
        {
            _a = a;
            _b = b;
            _c = c;
        }

        public string DoWork()
        {
            // does some different work based on the value of 'a', 'b' and 'c'
            return "some B worker result";
        }
    }

1
您有一个工厂方法可以接收3个参数,即使不是在所有情况下都使用全部3个参数。如果对象C需要更多的参数,该怎么办?您会将它们添加到方法签名中吗?此解决方案不可扩展,不建议使用IMO
Amorphis

3
如果我需要一个需要更多参数的新ConcreteWorkerC,则可以,它们将被添加到GetWorker方法中。是的,工厂不符合“开放/关闭”原则-但是某些地方必须像这样,我认为工厂是最佳选择。我的建议是:不仅要说这是不正确的建议,还可以通过发布替代解决方案来帮助社区。
JTech

1

我建议以下几件事之一。

如果要维护封装,以便呼叫站点不必了解任何有关worker或worker工厂内部工作的信息,则需要更改接口以具有额外的参数。该参数可以具有默认值,以便某些呼叫站点仍可以仅使用2个参数。这将需要重新编译所有使用的库。

我建议您反对的另一个选择,因为它破坏了封装并且通常是糟糕的OOP。这还要求您至少可以修改的所有呼叫站点ConcreteWorkerB。您可以创建一个实现该IWorker接口的类,但也可以使用DoWork带有额外参数的方法。然后在您的调用站点中尝试强制转换IWorkerwith var workerB = myIWorker as ConcreteWorkerB;,然后DoWork在具体类型上使用三个参数。同样,这是一个坏主意,但这是您可以做的。


0

@Jtech,您考虑过使用该params参数吗?这允许传递可变数量的参数。

https://msdn.microsoft.com/zh-CN/library/w5zay9db(v=vs.71).aspx


如果DoWork方法对每个参数执行相同的操作并且每个参数的类型相同,则params关键字可能有意义。否则,DoWork方法将需要检查params数组中的每个参数的类型是否正确-但可以说我们有两个字符串,并且每个字符串都用于不同的用途,DoWork如何确保它具有正确的类型一个...它必须根据数组中的位置来假设。太松散了,我不喜欢。我觉得@JohnWu的解决方案比较严格。
JTech
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.