我正在创建一个系统来生成数学问题。正如您在数学中所知道的,存在几种不同类型的问题:二元问题,分数,小数,对两个数进行求和等。黑盒,它接收Configuration作为输入,并返回Problem作为输出。


问题:其中包含需要生成问题的属性。
配置:这是范围参数或条件来生成问题。
工厂:他负责创建新问题。



这里我有工厂接口和标记接口:

public abstract class Problem { }
public abstract class Configuration { }

public interface IProblemFactory
{
    Configuration Configuration { get; set; }
    Problem CreateProblem();
}


这是工厂的基类,因为我需要Random类。我实现该代码的所有类都必须具有相同的种子,因此我有一个静态实例。

public abstract class ProblemFactoryBase<P, C> : IProblemFactory
    where P : Problem
    where C : Configuration
{
    private const int DEFAULT_SEED = 100;
    protected C _config;
    private static Random _random;

    public ProblemFactoryBase()
    {
        if (_random == null) _random = new Random(DEFAULT_SEED);
    }

    public ProblemFactoryBase(C config)
    {
        _config = config;

        if (_random == null) _random = new Random(DEFAULT_SEED);
    }

    protected Random Random { get { return _random; } }

    public C Configuration
    {
        get { return _config; }
        set { _config = value; }
    }

    #region IProblemFactory Implementation

    Configuration IProblemFactory.Configuration
    {
        get { return _config; }
        set
        {
            C config = value as C;
            if (config == null) throw new InvalidCastException("config");

            _config = config;
        }
    }

    protected abstract P CreateProblem(C config);

    #endregion

    public Problem CreateProblem()
    {
        if (_config == null) throw new InvalidOperationException("config");
        return CreateProblem(_config);
    }

    public static void SetSeed()
    {
        _random = new Random(DEFAULT_SEED);
    }
}

构造类型时的抽象方法。

当我实现所有这些方法时。例如,用于ProblemFactoryBase<P, C>的模块(例如BinaryProblems)将是:

public class BinaryConfiguration : Configuration
{
    public Range<int> Range1 { get; set; }
    public Range<int> Range2 { get; set; }
    public List<Operators> Operators { get; set; }

    public BinaryConfiguration(Range<int> range1, Range<int> range2, List<Operators> operators)
    {
        this.Range1 = range1;
        this.Range2 = range2;
        this.Operators = operators;
    }

public class BinaryProblem : Problem
{
    public BinaryProblem(decimal x, decimal y, Operators op, decimal response)
    {
        this.X = x;
        this.Y = y;
        this.Response = response;
    }

    public decimal X { get; private set; }
    public decimal Y { get; private set; }
    public decimal Response { get; private set; }
}

public enum Operators
{
    Addition, Substract, Multiplication, Division
}


最重要的部分是混凝土工厂。该类为基本泛型类指定类型参数。为什么?因为我认为这是实现具体价值的最佳方法,所以我的意思是我现在不必强制转换任何价值。

 public class BinaryFactory : ProblemFactoryBase<BinaryProblem, BinaryConfiguration>
{
    protected override BinaryProblem CreateProblem(BinaryConfiguration config)
    {
        var x = GenerateValueInRange(config.Range1);
        var y = GenerateValueInRange(config.Range2);

        var index = Random.Next(config.Operators.Count);
        var op = config.Operators[index];

        return new BinaryProblem(x, y, op, x + y);
    }

    private decimal GenerateValueInRange(Range<int> range)
    {
        return Random.Next(range.Min, range.Max);
    }
}


要实现它是:

        BinaryConfiguration configuration = new BinaryConfiguration() {.. }
        IProblemFactory factory = new BinaryFactory(configuration);
        var a = factory.CreateProblem();


它正在做我喜欢的事情,因为我想将多个工厂排列成一个阵列,但是同时,2+3的实现对我来说是好的,因为无需强制转换类型。

我还在学习设计模式。也许还有其他方法可以改善它,或者我没有遵循建议。您有何反馈?

评论

太简单的2 + 3 =?例。您研究了设计模式并想实现一种,但是为什么呢?我对许多模式的问题是,我看不到要点,它们感觉是虚假的。我几乎无法想象对此有真正的需求。如果眼前的问题足够复杂,我可能已经看到了这种需要,但是学术实例并没有太大的动机。如果您描述了足够的用例以查看模式,那么我可以更加确定地确定Factory Pattern是否是正确的工具。感觉不像一个。通常有替代解决方案。我不强迫您使用代码。

之所以这样做,是因为我正在创建一个模块化应用程序,其中每个模块都包含创建问题的方式。我正在描述数以百计的数学问题。

让我建议一条更好的路线。我相信您想使用域特定语言或某种形式的文本解析,这种方式更加灵活。这是一个很酷的论文的示例:math.utah.edu/~hohn/spemhh.pdf和另一个:任意.name / papers / fpf.pdf这是一个F#DSL的示例:social.msdn.microsoft.com/论坛/ zh-CN / fsharpgeneral / thread /…以及:udooz.pressbooks.com/chapter/external-dsl-using-f-sharp-2-0和Clojure:learningclojure.com/2010/02/…fnc prg规则。

我强烈同意列昂尼德的观点。大部分代码是无用的,您正在尝试实现不需要它的模式。我开始写一个答案,但是我发现问题太高了,您会发现您没有有用的基类和泛型,这迫使您知道实现细节正在消失的工厂模式本身。删除所有内容,然后重新开始(从底部开始!您要编写使用这些类的第一个代码,然后转到通用实现)。

您...您实际上写了一个ProblemFactory。您赢得了互联网。

#1 楼

可以使用构造函数链接删除此重复的代码


public ProblemFactoryBase()
{
    if (_random == null) _random = new Random(DEFAULT_SEED);
}

public ProblemFactoryBase(C config)
{
    _config = config;

    if (_random == null) _random = new Random(DEFAULT_SEED);
}  



,并使用花括号{}进行美化,就像这样
/>
public ProblemFactoryBase()
{
    if (_random == null) { _random = new Random(DEFAULT_SEED); }
}

public ProblemFactoryBase(C config)
    : this()
{
    _config = config;
}  



因为类级别常量DEFAULT_SEED仅与static Random _random一起使用,因此应为static readonly。此外,根据命名准则,应使用PascalCase大小写进行命名,另请参见:https://stackoverflow.com/a/242549/2655508看起来应该像这样

private static readonly int DefaultSeed = 100;  


因为Random _random和属性Random Random(不应这样命名,因为您不应该像其类型一样命名属性)不会在构造函数中更改,所以为什么不创建Random _random一个protected readonly代替?

好吧,我现在发现了这个


public static void SetSeed()
{
    _random = new Random(DEFAULT_SEED);
}  



这是IMO错误,至少方法名暗示了一些问题与所做的不同。更改该方法的名称,或让该方法具有方法参数,然后将其用作种子。


关于#region IProblemFactory Implementation,请阅读区域是否有反味或代码异味?

如果要保留此区域,则至少应包括Problem CreateProblem()方法。顺便说一句,为什么您要对接口进行隐式和显式实现????




有点奇怪。第一个属性仅属于工厂类本身,第二个属性是显式接口实现的属性。

第一个属性需要保留,因为它没有任何帮助,也没有在代码中添加任何值。这样做的唯一好处是,由于显式接口的实现,您无需将实例强制转换为IProblemFactory

在第二个属性设置器中,无需对C进行软转换,因为C已经是Configuration


具体的BinaryFactory的实现或其示例用法都是有缺陷的,因此无法编译,因为示例用法使用了BinaryFactory不提供的构造函数。

#2 楼

当将参数传递给public方法时,您始终需要断言parameter不是null,否则您的代码将抛出NullReferenceException,并且没人会喜欢它们。这条路。 “非书面”约定是默认字母为C。这并不意味着我们应该使用任何字母! :)如果检查.Net框架命名,则应将参数类型名称重命名为PT。我认为您应该为接口切换TProblemTConfig。通过使用抽象类,可以防止子类继承一个可能更有用的类,尤其是考虑到两个类中都没有属性或方法时,尤其如此。因此,使用接口,每个孩子都可以自由决定是否需要实现另一个类。

Problem类中,您可以重复代码。有一个Configuration方法,该方法包含在构造函数中调用的相同代码。您应该在构造函数中调用ProblemFactoryBase而不是调用相同的代码。

,这种SetSeed方法真的有用吗? SetSeed变量是SetSeed,因此您只能决定将其设置为另一个值。但是,您无需更改_random的种子,那么为什么要为此公开private方法呢?我认为您不需要这种方法。而不是公开_random,为什么不公开将返回public static的方法protected Random。我认为这会更清楚。另外,您确定GetRandomNumber()不是实施细节吗?为什么Q4312079q会成为您的基类?如果孩子不想使用_random.Next()怎么办?我认为Random应该被排除在基类之外,孩子会在需要时创建自己的东西。 >
已经有Random实现。这可能会导致一些混乱。我敢肯定,仅公开Random实现就足够了!如果您担心需要转换Random而又不知道类型,那么我会问你:什么时候需要从interface外部转换interface(由于Configuration,谁知道Configuration的类型)?

ProblemFactoryBase消息可能更清晰。毕竟,解释Configuration并不是很长的信息,而且要比TConfig清晰得多。

我不确定您是否使用最佳设计模式来解决问题。毕竟,InvalidOperationException"the configuration must not be null"绝对没有共享。 "config"取决于Problem。由于Configuration不是通过工厂创建的,因此我们将从此处删除它开始。毕竟,为什么一个Problem需要一个Configuration作为参数?

参数1:如果Configuration类或ProblemFactoryBase不需要Configuration类,则表示Problem是实现细节。接口/抽象类不应显示实现细节。为了说明我的观点,这里有一个例子。想象一下,有一天您有两个CreateProblem的子类。一种使用Configuration,另一种使用Configuration。您会在ProblemFactoryBase中添加Configuration吗?您是否会为所有子类添加这样的参数?

参数2:如果IFooBarAlgorithmService是一个属性,因为您不希望它成为IFooBarAlgorithmService方法的参数,那么创建一个不需要ProblemFactoryBase的问题? ProblemFactoryBase方法将抛出Configuration。但这真的无效吗?否。子类不想使用CreateProblem,也不需要使用它,因为它不是Configuration方法的参数。

所以现在您知道为什么CreateProblem属性不是最好的选择,让我们看看解决方案:

您可能会从InvalidOperationException类中完全删除Configuration属性,因为它是一个实现细节。或将CreateProblem添加为Configuration公共方法的参数。基类。

现在,我认为Configuration应该是方法ProblemFactoryBase的参数,让我们检查一下所有内容。首先,我们创建一个Configuration

(注意:我认为CreateProblem会比Configuration更好,它更容易混淆!对于以下示例,我将应用上面所做的评论)

public C Configuration
{
    get { return _config; }
    set { _config = value; }
}


Configuration非常简单。它指出“实现我的任何东西都应该能够使用配置来创建问题”。这正是我们想要的。始终尝试使您的界面尽可能简洁明了! (您以前的界面很好,这只是一个提示!)

接下来,实现的外观是什么样的?并且看起来很像您以前的类,但是请注意,CreateProblem和此interface之间没有基类,并且它没有创建更多代码(我们使用了ProblemConfiguration变量,该变量可能已经在另一个子类中使用!

现在,您将拥有Configuration的多个子类。只有一个入口门可以配置并得到相应的问题,这不是很好吗?哦,是的,这很干净。那就是抽象工厂的所在!

考虑一下它是所有“子”工厂的包装类。 !

public interface IProblemFactory<TProblem, TConfig> 
    where TProblem : IProblem 
    where TConfig : IProblemConfiguration
{
    TProblem CreateProblem(TConfig configuration);
}


繁荣,现在您可以在所有工厂都拥有一个共同的地方。随着时间的流逝,您将不得不添加大量的interface。我们可以通过更改接口来解决此问题,这将更改程序结构中的某些内容,但是此解决方案也有缺点。无论如何,我都会向您显示,以便您可以更好地确定哪个适合您的需求。

因此,如果改用我们的interface

(注意:在键入时,我意识到class应该命名为Random。毕竟,您已经知道要创建由于返回类型是IProblemFactory,因此会出现问题) >但是我们在您的if/else中获得了一个优势:

public class BinaryProblemFactory : IProblemFactory<BinaryProblem, BinaryConfiguration>
{
    private static readonly Random Random = new Random();

    public BinaryProblem CreateProblem(BinaryConfiguration configuration)
    {
        if (configuration == null) throw new ArgumentNullException(nameof(configuration));

        var x = GenerateValueInRange(configuration.Range1);
        var y = GenerateValueInRange(configuration.Range2);

        var index = Random.Next(configuration.Operators.Count);
        var op = configuration.Operators[index];

        return new BinaryProblem(x, y, op, x + y);
    }

    private static decimal GenerateValueInRange(Range<int> range)
    {
        return Random.Next(range.Min, range.Max);
    }
}


我们现在可以使用interface代替CreateProblem的链。老实说,我不是很喜欢第三种解决方案。它涉及反射,这意味着速度较慢且很好……不是很干净。我还是给你看,因为好吧,我不是你的主人。 :p此解决方案使用第一个界面。不要忘了使用反射会松开编译时间检查,这不是一个好的解决方案。

在第一个和第二个之间,因为它们都有优点和缺点,但是它们都应该非常牢固! :)

评论


\ $ \ begingroup \ $
艰难的决定,但是您的答案涵盖了更多的领域,并且由于我真的很喜欢我的工厂接口具有Create方法(KISS,对吗?),因此您将其钉牢。恭喜你!
\ $ \ endgroup \ $
– Mathieu Guindon♦
15年12月20日在13:41

\ $ \ begingroup \ $
返回new BinaryProblem(x,y,op,x + y);您将BinaryProblem的响应参数设置为x + y,但这不应该是x op y(在伪代码中)吗?也就是说,现在您总是会给出加法响应,但实际上它也应该使用问题指定的操作。
\ $ \ endgroup \ $
–skiwi
15年12月20日在16:25

\ $ \ begingroup \ $
@skiwi糟糕,我为这种情况复制了OP的代码,但您说对了,有些事情发生了!
\ $ \ endgroup \ $
–IEatBagels
2015年12月20日下午16:26

#3 楼

这似乎不应该处理工厂模式。工厂模式更像是在不暴露实例化逻辑的情况下创建对象。最好也与此一起使用接口(就像您一样)。

我认为您应该检查的是解释器模式。非常适合处理表达式(在这种情况下为问题)。

检查以下内容:

http://www.dofactory.com/Patterns/PatternInterpreter.aspx#_self2

#4 楼

对已发表的评论进行了少量补充:


private decimal GenerateValueInRange(Range<int> range)
{
    return Random.Next(range.Min, range.Max);
}
为什么会混合decimalint?在我看来,如果生成器产生涉及整数的问题,则不应在任何地方使用小数;并且如果它产生涉及小数的问题,则将Range<decimal>用作界限会更加清楚。我可以为将其包含在Random中(或围绕Random的包装器)做一个论点;如果可以找到一种不会导致类型系统出现问题的方法,我可以为将其包含在Range<T>中做一个论证。并且我可以为将其作为Range<int>的扩展方法而包含一个参数。但是,如果它是生成器中的私有方法,则最终将其复制粘贴到整个地方。


#5 楼

我将重点放在一件事上,因为其他答案和评论已解决了大多数问题。每次您运行应用程序时,都会产生相同的伪随机序列。对我来说,这似乎是一个巨大的缺陷!只要创建它们的距离足够远)。