Dynamic list of methods to execute (in D365FO)

Imagine that you’re running a set of payment matching rules or you’re trying to find discounts based on various criteria.

You’ll try one way to find the data, if it doesn’t bring anything, you try another one, and so on.

A typical implementation looks like this:

found = trySomething();
 
if (!found)
{
    found = trySomethingElse();
}
 
if (!found)
{
    found = tryYetAnotherThing();
}

It’s straightforward and it works reasonable well as long as the list is static. But what if you need more flexibility? For example, maybe you want the caller code to decide which methods should be used (e.g. just those selected by a user) and in which order. Then this approach doesn’t really work.

A classic object-oriented solution would be creating a separate class for each method – each class would represent a single strategy for performing the task. These classes would implement a common interface, which would allow working with them in a polymorphic manner. Then you could easily create a collection of such classes and run them one by one.

This approach is fine and I would recommend it in more complex scenarios, but if the logic is simple, creating an extra class for each method may be cumbersome. Isn’t there any alternative?

A solution may be a collection of delegates. A delegate is basically a reference to a method (or more methods at once, because delegates can be combined together, but that’s not what we need today). In F&O, they’re typically used to allow extensions, but their potential usage is much broader.

Instead of creating a separate class for each method, we’ll create a single class representing a strategy to do something – in this case, to find a discount.

public final class DiscountFinderStrategy
{
    delegate void findDiscountDelegate(ItemId _itemId, DiscountFinderResult _result)
    {
    }
 
    public void execute(ItemId _itemId, DiscountFinderResult _result)
    {
        this.findDiscountDelegate(_itemId, _result);
    }
}

I thought that I could call the delegate directly from other classes, but it doesn’t work. That’s why I added execute() method, which does nothing but calling the delegate.

Here are three methods that we’ll want to use for the price search:

void findDiscountForItem(ItemId _itemId, DiscountFinderResult _result)
{
    info("Finding discount for specific item...");
    if (_itemId == 'aaa')
    {
        _result.parmFound(true);
        _result.parmDiscount(5);
    }
}
 
void findDiscountForItemGroup(ItemId _itemId, DiscountFinderResult _result)
{
    info("Finding discount for item group...");
}
 
void findDefaultDiscount(ItemId _itemId, DiscountFinderResult _result)
{
    info("Finding default discount...");
 
    if (System.DateTime::Now.DayOfWeek == System.DayOfWeek::Monday)
    {
        _result.parmFound(true);
        _result.parmDiscount(1);
    }
}

They don’t do anything real; their main purpose is adding messages to infolog, so we can see that they were called.

findDiscountForItem() returns a result for item aaa, therefore subsequent methods won’t be called. We’ll test it later.

The discount in findDefaultDiscount() is there just to make Mondays more bearable for you. 😉

Delegates in CLR can return values and if it was possible in F&O, I could return the result directly from each method. Unfortunately delegates in F&O allow only void return type, most likely because they were designed to be used as events in particular and not as general-purpose delegates. But it’s not a big deal – we can make a workaround by using an object as a parameter and set the values there. That’s the purpose of my DiscountFinderResult class.

public class DiscountFinderResult
{
    boolean found;
    real discount;
 
    public boolean parmFound(boolean _found = found)
    {
        found = _found;
        return found;
    }
 
    public real parmDiscount(real _discount = discount)
    {
        discount = _discount;
        return discount;
    }
}

To call my three methods, I’ll create three instances of DiscountFinderStrategy class and subscribe one method to each.

DiscountFinderStrategy findByItem = new DiscountFinderStrategy();
findByItem.findDiscountDelegate += eventhandler(this.findDiscountForItem);
 
DiscountFinderStrategy findByGroup = new DiscountFinderStrategy();
findByGroup.findDiscountDelegate += eventhandler(this.findDiscountForItemGroup);
 
DiscountFinderStrategy findDefault = new DiscountFinderStrategy();
findDefault.findDiscountDelegate += eventhandler(this.findDefaultDiscount);

By the way, I could add all three methods to a single delegate, but then all of them would execute (unless an exception is raised). That’s not what I want in this case. I want to run once, check the result, run the other one and so on.

Then I need something that will actually execute the methods. I’ve designed DiscountFinder class for this purpose:

public final class DiscountFinder
{
    List strategies = new List(Types::Class);
 
    public void addStrategy(DiscountFinderStrategy _strategy)
    {
        strategies.addEnd(_strategy);
    }
 
    public real findDiscount(str _itemId)
    {
        setPrefix(strFmt("Item %1", _itemId));
 
        DiscountFinderResult result = new DiscountFinderResult();
        ListEnumerator enumerator = strategies.getEnumerator();
        while (enumerator.moveNext())
        {
            DiscountFinderStrategy strategy = enumerator.current() as DiscountFinderStrategy;
            strategy.execute(_itemId, result);
 
            if (result.parmFound())
            {
                return result.parmDiscount();
            }
        }
 
        return 0;
    }
}

It holds an ordered collection of DiscountFinderStrategy objects in strategies variable. Caller code can add them by calling addStrategy() method. findDiscount() iterates the collection, calls execute() method (which runs the delegate and therefore the subscribed method) and checks the result. If a result was found, the execution ends and a discount is returned. If not, the next strategy to find a discount is used.

Finally, here is an example of the caller code, implemented as a runnable class:

internal final class DiscountFinderDemo
{
    public static void main(Args _args)
    {
        new DiscountFinderDemo().run();
    }
 
    void run()
    {
        setPrefix("Discount search");
 
        DiscountFinder discFinder = this.createDiscountFinder();
 
        discFinder.findDiscount('aaa');
        discFinder.findDiscount('bbb');
 
    }
 
    DiscountFinder createDiscountFinder()
    {
        DiscountFinder discFinder = new DiscountFinder();
 
        DiscountFinderStrategy findByItem = new DiscountFinderStrategy();
        findByItem.findDiscountDelegate += eventhandler(this.findDiscountForItem);
        discFinder.addStrategy(findByItem);
 
        DiscountFinderStrategy findByGroup = new DiscountFinderStrategy();
        findByGroup.findDiscountDelegate += eventhandler(this.findDiscountForItemGroup);
        discFinder.addStrategy(findByGroup);
 
        DiscountFinderStrategy findDefault = new DiscountFinderStrategy();
        findDefault.findDiscountDelegate += eventhandler(this.findDefaultDiscount);
        discFinder.addStrategy(findDefault);
 
        return discFinder;
    }
 
    // Here are the three find* methods that we saw earlier
 
    void findDiscountForItem(ItemId _itemId, DiscountFinderResult _result)
    {
        info("Finding discount for specific item...");
        if (_itemId == 'aaa')
        {
            _result.parmFound(true);
            _result.parmDiscount(5);
        }
    }
 
    void findDiscountForItemGroup(ItemId _itemId, DiscountFinderResult _result)
    {
        info("Finding discount for item group...");
    }
 
    void findDefaultDiscount(ItemId _itemId, DiscountFinderResult _result)
    {
        info("Finding default discount...");
 
        if (System.DateTime::Now.DayOfWeek == System.DayOfWeek::Monday)
        {
            _result.parmFound(true);
            _result.parmDiscount(1);
        }
    }
}

And this is the result:

Only one method was called for item aaa, because it managed to find a discount. On the other hand, we had to try all available strategies for item bbb.

This is just one example of how delegates can change the way how you design applications. I’m going to show another one soon.

Leave a Reply

Your email address will not be published.