使用哪种设计模式来验证数据并创建对象?

8

我经常遇到这样的情况,我想通过传递一些给定的数据或者另一个对象来创建一个对象实例,但是这些数据或者对象必须有效或处于正确的状态。我总是有点不清楚如何做到“正确”。以下是我的示例:

假设有这个类:

class BusinessObject()
{
    const Threshold = 10;

    public BusinessObject(SetOfData<SomeType> setofdata)
    {
        // an example of some validation
        if (setofdata.count > Threshold)
        {
            // performance some business logic
            // set properties
        }
    }
}

如果你这样做,可能会遇到一些问题:

var setofdata = new SetOfData<SomeType>();

// if data is not valid then the object will be created incorrectly
var businessObject = new BusinessObject(setofdata);

所以,我的解决方案一直是:

class BusinessObjectBuilder()
{
    public BusinessObject Build(SetOfData<SomeType> setofdata)
    {
        // an example of some validation
        if (setofdata.count > Threshold)
            return new BusinessObject(setofdata);
        }
        else
        {
            return null;
        }
    }
}

或者将构造函数设为私有,并添加一个静态工厂方法:

class BusinessObject()
{
    const Threshold = 10;

    public static Create(SetOfData<SomeType> setofdata)
    {
        if (setofdata.count > Threshold)
        {
            return new BusinessObject(setofdata);
        }
        else
        {
            return null;
        }
    }

    private BusinessObject(SetOfData<SomeType> setofdata)
    {
        // performance some business logic
        // set properties
    }
}

理想情况下,如果数据无效,我不想抛出异常,因为在一个过程中可能会创建多个业务对象,如果有一个验证失败,我不希望整个过程失败,而捕获和抑制异常是不好的。

此外,我阅读的所有Abstract Factory或Factory方法的示例都涉及传递某种类型或枚举,并构建和返回正确的对象。他们似乎从来没有涵盖过这种情况。

那么,在这种情况下的惯例是什么?任何建议将不胜感激。


只是一个注释,工厂并不“需要”传入“某种类型或枚举”; 它们可以接受任何类型的数据(甚至是SetOfData<SomeType>),也可以不接受任何数据(无参数)。示例倾向于使用它们,因为这是一种相当常见/简单的使用/描述方式。如果您愿意,您始终可以为工厂创建一个BusinessObjectValidator来利用它来检查参数,但如果检查像您所描述的那样简单,我宁愿在工厂创建方法中添加它。 - Chris Sinclair
5个回答

12

依我看,构造函数验证在许多情况下是最好的选择,特别是当你需要确保只有在指定参数被设置时才能创建对象时。

public class BusinessObject
{
    const Threshold = 10;

    public BusinessObject(SetOfData<SomeType> setofdata)
    {
        // an example of some validation
        if (setofdata.count > Threshold)
        {
            throw new InvalidOperationException("Set data must be above treshold");
        }
    }
}

然而,当出现以下情况时,实现方式存在问题:

  • 可能会有无效的对象,例如处于草稿状态等
  • 在需要默认构造函数的ORM中使用
  • 如果发生了繁重的验证逻辑。

对于第1和第2点,我除了请求-验证-提交机制之外,无法建议其他选项。

对于第3点,原因是该类将执行太多自身的验证操作并创建庞大的代码。如果有很多验证逻辑,我建议使用Builder模式,并注入验证器,在BusinessObject的构造函数中使用internal关键字。

public class BusinessObjectBuilder
{
    public BusinessObjectBuilder(IBusinessObjectValidator validator){
        this.validator = validator;
    }
    IBusinessObjectValidator validator;

    public BusinessObject Build(SetOfData<SomeType> setofdata)
    {
        // an example of some validation
        if (validator.IsValid(setofdata))
            return new BusinessObject(setofdata);
        }
        else
        {
            throw new //exception
        }
    }
}

这有助于实行模块化编程并防止庞大的代码。

两种代码都具备以下特点:

  • 易于测试
  • 易于审查
  • 可扩展

2
也许你可以在你的工厂(方法)中实现策略模式,以提供一些验证功能:
public interface DataValidationStrategy {
    boolean isValid(SetOfData<SomeType> setofdata);
}

public class ThresholdValidation implements DataValidationStrategy {
    private int threshold;

    public ThresholdValidation(int threshold) {
        this.threshold = threshold;
    }

    @Override
    public booleam isValid(SetOfData<SomeType> setofdata) {
        return setofdata.count > threshold;
    }
}

现在创建尽可能多的不同 验证类,然后更改您的 create 方法:
public static Create(SetOfData<SomeType> setofdata, DataValidationStrategy validation)
{
    if (validation.isValid(setofData))
    {
        return new BusinessObject(setofdata);
    }
    else
    {
        return null;
    }
}

编辑:另外,您可能要考虑使用原型空对象来替代null的返回值。

2
我认为在这种情况下,空对象会有害。消费者可能会检查 null,并假设如果返回的 BusinessObject 不是 null,则已正确创建。而检查 Create 返回的对象是否不是 Null Object 将是模式使用不正确的示例。 - lisp

1

我认为,当参数不正确时,像Create这样的方法抛出异常是一种通用做法。您对于在此情况下返回null并尝试避免使用异常进行流程控制的保留是正确的。您可以简单地使用:

public bool CanBuild(SetOfData<SomeType> setofdata)
{
    return validator.IsValid(setofdata);
}

public BusinessObject Build(SetOfData<SomeType> setofdata)
{
    if (validator.IsValid(setofdata))
    {
        return new BusinessObject(setofdata);
    }
    throw new ArgumentException();
}

我仍然保留异常抛出,因为不能保证setofdata已经通过CanBuild进行了验证。这提供了避免使用异常进行流程控制的手段,但缺点是需要进行两次验证。为了消除双重验证,请将TryCreate放在Create旁边或替换它。只需查看签名,您就会发现,除了创建业务对象之外,此方法还执行输入数据的验证,并以异常以外的形式返回此验证的结果。(参见int.Parseint.TryParse)
public bool TryBuild(SetOfData<SomeType> setofdata, out BusinessObject businessObject)
{
    if (validator.IsValid(setofdata))
    {
        businessObject = new BusinessObject(setofdata);
        return true;
    }
    businessObject = null;
    return false;
}

这些验证方法会调用其他构造函数,这些构造函数对外不可见且不包含验证,而所有广泛可访问的构造函数仍将进行验证并抛出异常。
当然,方法“Built”和“TryBuilt”可以是静态的(如int中),或者您可以应用工厂模式。

1
我不能告诉你“正确”的方法。但是我可以告诉你我的方法=)
我会选择工厂模式。如果您不想因验证错误而中止应用程序,则工厂可以使用默认值填充有问题的部分。

-1

看看OVal框架。你可以用自己的注释扩展它。


当有人访问Stack Exchange时,问题的“答案”应该实际包含一个答案,而不仅仅是指向答案的一堆方向。 - Erik Philips

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接