Can generics improve this design?
The PartyRoleConstraints class in the model below (on the right) is the subject of this question.
The idea is that when a client tries to associate a Party with a RoleType, the RoleType sees if it has any Constraints that should prevent a given Party from being associated. A Party is a supertype for both Person and Organization.
Here is the totally generic interface I am after:
public interface IRoleConstraint<in T>
{
Func<T, bool> IsSatisfied { get; }
string UnsatisfiedDescription { get; }
bool CanAddRole(T instance);
}
A common constraint would be by Type. So if I have role type of "husband" then I want to make sure the Party instance is a Person. Here is some implementation and a test case proving I can do this:
public class RoleConstraint<T> : IRoleConstraint<T>
{
public RoleConstraint(Func<T, Boolean> isSatisfied, string unsatisfiedDescription) {
if (isSatisfied == null) throw new ArgumentNullException("isSatisfied");
if (unsatisfiedDescription == null) throw new ArgumentNullException("unsatisfiedDescription");
IsSatisfied = isSatisfied;
UnsatisfiedDescription = unsatisfiedDescription;
}
public Func<T,开发者_StackOverflow bool> IsSatisfied { get; protected set; }
public string UnsatisfiedDescription { get; protected set; }
public bool CanAddRole(T instance) { return IsSatisfied.Invoke(instance); }
}
public class PartyRoleConstraint : RoleConstraint<Party>
{
public PartyRoleConstraint(Func<Party, bool> isSatisfied, string unsatisfiedDescription) : base(isSatisfied, unsatisfiedDescription) { }
}
public class PartyRoleConstrainedToType<TRequired> : PartyRoleConstraint where TRequired : Party
{
private static readonly string _unsatisfiedDescription
= string.Format("This role requires a Party instance to be a {0}", typeof(TRequired).Name);
private static readonly Func<Party, bool> _isSatisfied = instance => instance.GetType().Equals(typeof(TRequired));
public PartyRoleConstrainedToType() : base(_isSatisfied, _unsatisfiedDescription) { }
}
[Test]
public void Constraints_IfTypeConstraint_and_InstanceDoesNotMatch_False()
{
var sony = new Organization("Sony Corporation");
var constraint = new PartyRoleConstrainedToType<Person>();
_husbandRoleType.AddConstraint(constraint);
Assert.That(_husbandRoleType.CanAddRole(sony), Is.False);
}
The problem I am hitting is if I want to set up a rule based on an attribute of a subtype of Party. For example, I want the gender of the husband to be Male. I can do this with a cast, as:
[Test]
public void Constraints_IfConstraintConditionIsNotMet_False()
{
_husbandRoleType.AddConstraint(new PartyRoleConstrainedToType<Person>());
Assert.That(_husbandRoleType.CanAddRole(_arthur), Is.True);
//**** here is the cast **** //
var mustBeMale = new PartyRoleConstraint(p => ((Person)p).Gender == Gender.Male, "the husband must be male.");
_husbandRoleType.AddConstraint(mustBeMale);
Assert.That(_husbandRoleType.CanAddRole(_arthur), Is.False);
_arthur.Gender = Gender.Male;
Assert.That(_husbandRoleType.CanAddRole(_arthur), Is.True);
}
The question (finally!) is: can I use generics to avoid that cast, and if so, how?
Yes, you can get rid of the cast, but you will have to specify "Person" somewhere. It's hard to propose a "best place" to do so, without knowing more about your requirements and constraints. One option would be something like:
var mustBeMale = PartyRoleConstraint.For<Person>( p => p.Gender == ...);
Another option would be to tweek PartyRoleConstrainedToType
to allow further restrictions. And example might look like this:
var combinedConstraint = new PartyRoleConstrainedToType<Person>().MustSatisfy(p => p.Gender == ...);
As said before: You will have to specify Person
somewhere, but there a different options to create a nice syntax. What nice means, depends on your requirements and use cases.
精彩评论