What design pattern to use for validating data and

2020-07-07 11:05发布

I often come across situations where a I want to create an instance of an object by passing it some given data or maybe another object but the data or object needs to be valid or in the right state. I am always a bit unclear on the 'correct' way of doing this. Here is my example:

Given this class:

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

It is possible to run into some problems if you do this:

var setofdata = new SetOfData<SomeType>();

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

So my solutions have always been either:

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

Or make the constructor private and add a static factory method:

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

ideally I would not like to throw an exception if the data is invalid as there might be multiple business objects being created in one process and I don't want the whole process to fail if one validation fails and catching and suppressing exceptions is not good.

Also all examples I read of the Abstract Factory or Factory method involve passing in some type or enum and a correct object being built and returned. They never seem to cover this scenario.

So what are the conventions in this scenario? Any advice would be greatly appreciated.

5条回答
beautiful°
2楼-- · 2020-07-07 11:11

I believe it is general practice to throw an exception from a method like Create when the arguments are incorrect. You are right in your reservations about returning null in this scenario and trying to avoid using exceptions for flow control. You could simply have:

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

I still leave the exception throwing, because there's no guarantee that setofdata was validated with CanBuild. This provides means of avoiding using exceptions for flow control, but has the drawback of validating twice. To eliminate the double validation, put TryCreate next to or instead of Create. Just by looking at the signature you see that, apart from creating the business object, this method performs validation of input data and returns the result of this validation in form other than an exception. (See int.Parse and int.TryParse)

public bool TryBuild(SetOfData<SomeType> setofdata, out BusinessObject businessObject)
{
    if (validator.IsValid(setofdata))
    {
        businessObject = new BusinessObject(setofdata);
        return true;
    }
    businessObject = null;
    return false;
}

This validating methods would call constructors inaccessible to others that contain no validation, while all widely accessible constructors would still validate and throw exception.

Of course methods Built and TryBuilt can be static (as in int) or you can apply a factory pattern.

查看更多
疯言疯语
3楼-- · 2020-07-07 11:16

IMHO constructor validation is the best for many situation where you need to make sure that no object can be created unless specified parameter being set.

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

However, this has bad implementation when:

  • You may have invalid object such as when in draft status, etc
  • Used in ORM when default constructor needed
  • If heavy validation logic occurs.

For point no.1 and 2, I cannot suggest any other option except request - validate - submit mechanism.

For point no.3, the reason is, the class will do too much for validation itself and creating a monolithic code. If there is much validation logic, I suggest to implement builder pattern with injected validator, and make the constructor of 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
        }
    }
}

This enforce modular programming and prevent monolithic code.

Both of the code is:

  • easy to test
  • easy to review
  • extendable
查看更多
▲ chillily
4楼-- · 2020-07-07 11:26

Have a look at OVal framework. You can extend it whit your own annotations.

查看更多
Animai°情兽
5楼-- · 2020-07-07 11:31

Maybe you could implement the Strategy Pattern to your Factory (method) to provide some validation capabilities:

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

Now create as many different validation classes as you require and then change your create method:

public static Create(SetOfData<SomeType> setofdata, DataValidationStrategy validation)
{
    if (validation.isValid(setofData))
    {
        return new BusinessObject(setofdata);
    }
    else
    {
        return null;
    }
}

Edit: In addition you might consider using a prototype or a null object instead of a null return value.

查看更多
疯言疯语
6楼-- · 2020-07-07 11:34

I cant tell the "correct" way. But I can tell you my way =)

I would pick the factory-pattern. If you don't want to abort your application in cause of a validation error, the factory could fill the faulty parts with default-values.

查看更多
登录 后发表回答