简体   繁体   中英

Concrete Class inherit from Abstract class which inherits from Generic Abstract Class

I'm an "old school" programmer here that is struggling with using inheritance to my advantage. I found myself repeating code and it started to smell. I wasn't adhering to DRY, so I'm trying to refactor a bit here to cut down on the code duplication!

I'm trying to write Value Object classes to be used in my Entities, which will enforce basic invariants. I have a generic abstract ValueObject class that handles equality and hashes like so:

public abstract class ValueObject<T> where T : ValueObject<T>
{
    protected abstract IEnumerable<object> GetEqualityCheckAttributes();

    public override bool Equals(object other)
    {
        return Equals(other as T);
    }

    public bool Equals(T other)
    {
        if (other == null)
        {
            return false;
        }
        return GetEqualityCheckAttributes().SequenceEqual(other.GetEqualityCheckAttributes());
    }

    public static bool operator == (ValueObject<T> left, ValueObject<T> right)
    {
        return Equals(left, right);
    }

    public static bool operator != (ValueObject<T> left, ValueObject<T> right)
    {
        return !(left == right);
    }

    public override int GetHashCode()
    {
        int hash = 17;
        foreach (var obj in this.GetEqualityCheckAttributes())
        {
            hash = hash * 31 + (obj == null ? 0 : obj.GetHashCode());
        }
        return hash;
    }
}

I have been then creating my value object classes which then implement this abstract class, and provide the logic to make sure the object can't be created in an invalid state. This is when I started violating DRY, and was creating many objects with the same code (eg required string with max length of 50 or 30 or 10).

So I wish to put the code that enforces the invariant in its own class, and have my concrete value object class inherit that capability. Something like (this doesn't compile, see below):

public abstract class RequiredStringValueObject : ValueObject<string>
{
    private string _value;
    protected string _fieldName;
    protected byte _maxLength;

    public string Value
    {
        get
        {
            return _value;
        }
        protected set
        {
            if (value == null || string.IsNullOrWhiteSpace(value))
            {
                throw new ArgumentNullException(_fieldName, _fieldName + " must be supplied.");
            }
            value = value.Trim();
            if (value.Length > _maxLength)
            {
                throw new ArgumentOutOfRangeException(_fieldName, value, _fieldName + " can't be longer than " + _maxLength.ToString() + " characters.");
            }
            _value = value;
        }
    }
}

Then I could "use" all of this functionality in the concrete class like so:

public class FirstName : RequiredStringValueObject
{
    private FirstName(string value, string FieldName, byte MaxLength)
    {
        _fieldName = FieldName;
        _maxLength = MaxLength;
        Value = value;
    }
    public static FirstName Create(string value, string FieldName, byte MaxLength)
    {
        return new FirstName(value, FieldName, MaxLength);
    }

    protected override IEnumerable<object> GetEqualityCheckAttributes()
    {
        return new List<object> { Value };
    }
}

All of this seems like a reasonable way to solve the problem (to me). Problem is I'm getting a compiler error in the RequiredStringValueObject declaration:

The type string cannot be used as a type parameter T in the generic type or method ValueObject<T> . There is no implicit reference conversion from string to ValueObject<string> .

I don't understand the error message exactly. Is what I'm trying to do possible? Is there a way to make this work? Or is there another approach I could/should be taking?

You have a where clause on your generic type T:

public abstract class ValueObject<T> where T : ValueObject<T>

This is telling the compiler that T must derive from ValueObject, and string does not.

What are you trying to enforce with that where T: clause? You probably want to omit it.

The problem stems from this line:

abstract class ValueObject<T> where T : ValueObject<T>

You are requiring T to inherit from ValueObject<T> so when you write:

RequiredStringValueObject : ValueObject<string>

string doesn't inherit from ValueObject (obviously) so you need to inherit from ValueObject<ValueObject<string>> , except that also violates the constraint and well... its turtles all the way down.

The easy fix is to remove the type constraint; it seems like your code is mostly set up for dealing with object any ways so you shouldn't need it. Putting any kind of "recursive" type constraint is just going to cause you problems in this setup. If you really need such a thing, you might need to go with composition instead, something like:

public interface IValueMethods<T>
{
   //required methods
}

//Constructor for value object
public ValueObject<T>(IValueMethods<T> commonMethods)
{
}

And then you can pass in the set of methods to use as a separate object.

In agreement with what @BradleyDotNET said. A possible fix could look like following:

public abstract class ValueObjectBase
{
    public abstract IEnumerable<object> GetEqualityCheckAttributes();
}

public abstract class ValueObject<T> : ValueObjectBase where T : class
{
    public override bool Equals(object other)
    {
        if (other is ValueObjectBase)
            return Equals(other as ValueObjectBase);

        return Equals(other as T);
    }

    public bool Equals(T other)
    {

        if (other == null)
        {
            return false;
        }
        return other.Equals(this);

    }

    public bool Equals(ValueObjectBase other)
    {
        return GetEqualityCheckAttributes().SequenceEqual(other.GetEqualityCheckAttributes());
    }

    public static bool operator ==(ValueObject<T> left, ValueObject<T> right)
    {
        return Equals(left, right);
    }

    public static bool operator !=(ValueObject<T> left, ValueObject<T> right)
    {
        return !(left == right);
    }

    public override int GetHashCode()
    {
        int hash = 17;
        foreach (var obj in this.GetEqualityCheckAttributes())
        {
            hash = hash * 31 + (obj == null ? 0 : obj.GetHashCode());
        }
        return hash;
    }
}

Thanks to all of your help, here is the final working solution:

ValueObject

public abstract class ValueObjectBase
{
    public abstract IEnumerable<object> GetEqualityCheckAttributes();
}

public abstract class ValueObject<T> : ValueObjectBase
{
    public override bool Equals(object other)
    {
        if (other is ValueObjectBase)
        {
            return Equals(other as ValueObjectBase);
        }
        return Equals(other as IEquatable<T>);
    }

    public bool Equals(T other)
    {
        if (other == null)
        {
            return false;
        }
        return other.Equals(this);
    }

    public bool Equals(ValueObjectBase other)
    {
        return GetEqualityCheckAttributes().SequenceEqual(other.GetEqualityCheckAttributes());
    }

    public static bool operator == (ValueObject<T> left, ValueObject<T> right)
    {
        return Equals(left, right);
    }

    public static bool operator != (ValueObject<T> left, ValueObject<T> right)
    {
        return !(Equals(left, right));
    }

    public override int GetHashCode()
    {
        int hash = 17;
        foreach (var obj in this.GetEqualityCheckAttributes())
        {
            hash = hash * 31 + (obj == null ? 0 : obj.GetHashCode());
        }
        return hash;
    }
}

A variation on the theme, the RequiredStringValueObject :

public abstract class RequiredStringValueObject : ValueObject<string>
{
    private string _value;
    protected string _fieldName;
    protected byte _maxLength;

    public string Value
    {
        get
        {
            return _value;
        }
        protected set
        {
            if (value == null || string.IsNullOrWhiteSpace(value))
            {
                throw new ArgumentNullException(_fieldName, _fieldName + " must be supplied.");
            }
            value = value.Trim();
            if (value.Length > _maxLength)
            {
                throw new ArgumentOutOfRangeException(_fieldName, value, _fieldName + " can't be longer than " + _maxLength.ToString() + " characters.");
            }
            _value = value;
        }
    }

    protected RequiredStringValueObject(string fieldName, byte maxLength, string value)
    {
        _fieldName = fieldName;
        _maxLength = maxLength;
        Value = value;
    }

    public override IEnumerable<object> GetEqualityCheckAttributes()
    {
        return new List<object> { Value };
    }
}

And the concrete implementation, the FirstName (a required string based value object with maximum length):

 public class FirstName : RequiredStringValueObject
{
    private FirstName(string value) : base(nameof(FirstName),30, value) { }

    public static FirstName Create(string value)
    {
        return new FirstName(value);
    }

}

As a child of the 80's would say, "Totally tubular!"

Thanks!

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM