3

I have a list of Registration's, on which I need to apply a set of rules to each individually.

Rules can be a single Rule, or a sequence of rules, representable by a tree. One rule is a ConditionalRule, where the condition itself can be a tree (e.g. and,or,not, isOwnedByAdmin), which checks if a specific condition apply to the given Registration. I have used the composite pattern on both Rule and Conditional, which currently recieves the registration like:

$rule->apply($registration)

or conditionally:

if($condition->check($registration))
{
    $rule->apply($registration)
}

This works, but needs to be more flexible in the future.

Eventually, I will have to apply this method on something which is not a Registration, say a Post object instead. Post should be able to be run through this rule tree similarly, on compatible rules and conditions. For example, both could check the condition isOwnedByAdmin, but only Post is compatible with the rule PublishPost. I'm ok with the fact that I will be able to construct an illegal tree (something not able to run on a Registration because it requires a Post).

I could extend my composit interfaces:

interface Rule
{
    public function applyTo(Registration $registration, Env $environment);
}

interface Condition
{
    public function check(Registration $registration);
}

into:

interface Rule
{
    public function applyToRegistration(Registration $registration, Env $environment);
    public function applyToPost(Post $post, Env $environment);
}

interface Condition
{
    public function checkRegistration(Registration $registration);
    public function checkPost(Post $post);
}

In the end, I will have quite a few (10+) Condition classes, many Rule classes and few types of objects it will need to run over (3-10). I would have to modify all those classes whenever I need a new type to check against.

I'm considering using 2 instances of the visitor pattern. A ConditionVisitor and a RuleVisitor. The RegistrationRuleVisitor would be created with a Registration object and a RegistrationConditionVisitor visit the Rule tree with.

But as my composit trees is "actions", applied to a resource, I'm not sure the visitor pattern applies at all? As I'm visiting something which cannot be evaluated without input (a registration/post). Or is there another pattern that I have missed which allows me to use the same tree based application of methods upon an object?

3
  • I think you need to clarify what problems you see having and how the visitor pattern addresses them. The visitor pattern is a little awkward. It's not the first thing I would try. Based on what I follow here, I don't see why you can't just tag the rules with what types they are applicable to in one way or another.
    – JimmyJames
    May 5, 2021 at 15:38
  • I tried clarifying, my main worry is that extension in the future will be very cumbersome.
    – Sheph
    May 5, 2021 at 17:24
  • I think I see where you are going with this. I'm getting stuck on one thing though. The way these interfaces are structured implies that they can be applied to both a Post and a Registration. Is the implementation the same for each or are you going to have different logic in the Post and Registration methods?
    – JimmyJames
    May 5, 2021 at 19:01

2 Answers 2

0

I'm still not sure I completely understand what you intend to accomplish here but I will attempt and answer with the assumption that these rules, while not necessarily applicable to both Registrations and Post, can be universal and when they are, the logic is the same. For example, let's say hypothetically (off the top of my head) that you want to impose a rule that both registrations and posts require the user have a valid email. The logic is the same for both in this case and ideally the implementation for both types is the same.

NOTE: I am not familiar with the details of PHP syntax. If I get it wrong, let me know and I will try to correct it.

If this holds, I think there's a much simpler solution than the visitor pattern. What I would suggest instead is a new interface called Action (or whatever makes sense to you if that doesn't work.) Your rule logic would then be this:

interface Rule
{
    public function applyTo(Action $action, Env $environment);
}

interface Condition
{
    public function check(Action $action);
}

You then define Action based only on what your rules and conditions need in order to execute. Post and Registration can then extend the Action interface (assuming this is allowed in PHP) or you implement Action on any concrete type that you want to apply rules to.

You mention that some rules will not apply to everything e.g. a rule might apply to a Post but not a Registration. That's perfectly manageable in this design. You have two clear approaches you can use for that. One is to check the type of the object when evaluating the rule and return a 'pass' response if the rule doesn't apply. Another option would be to create a method on Action e.g.:

interface Action { public function isFooApplicable(); }

This is the route I would tend to choose especially if you have a set of independent types that such rules apply to and a set where they don't.

0

Yes. Treat rules as structure you traverse, but keep traversal in visitors. There will be a double visitor per class that must be subjugated to rules.

This way each rule must support every new subject, new class for new subject is necessary, but you only need to write cross-subject code if rules application changes (then every visitor need to change).

If those trade offs are not good enough do look up Expression Problem and look for alternatives.

Not the answer you're looking for? Browse other questions tagged or ask your own question.