Agreement Dispatcher

本文介绍了一种利用事件驱动模型设计业务规则管理系统的实现方式,通过使用事件处理器和时间属性,系统能够灵活应对规则变化并确保历史事件处理的一致性。以一个具体的例子展示了如何将规则整合到系统中,包括如何处理不同类型的事件和时间相关的规则调整。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Structure a processor for Domain Events around business agreements.

In a system driven by Domain Events its important to be able to easily find and change the processing rules that react to the events. Since events often appear later than they occurred, you must also retain the ability to process old events with old rules.

Agreement Dispatcher structures an event processor primarily around the business agreement, since the agreement is a common form of organizing variation in the responses to events. By organizing processors by event type using Temporal Property you can handle changing event rules in a controlled manner.

How it Works

An Agreement Dispatcher is a way of modularizing a processor for Domain Eventsso that the dominant module is lined up with a business agreement. I use the term 'agreeement' rather than 'contract' partly because the agreements are often not formal contracts and partly to avoid collision with the notion of software contracts in Design by Contract.

My discussion of Agreement Dispatcher follows the principle of Delegated Dispatch[TBD: expand on this]. You model each agreement as an object which has a method to process an event. The actual processing code is placed in separate processor objects that are loaded into the agreement in a structured manner. The essential behavior of the agreement is to find the correct processor and then use it to process the event.

This leads to the question of what the correct structure is. Naturally this is to some extent dependent upon your problem domain, but one structure I've seen several times which works well is to structure processors by event type and time.

Figure 1

Figure 1:

(If you're not used to Temporal Property this may seem rather tricky. Essentially think of it as the agreement containing a hash map whose keys are event types and whole values are temporal collections of processors. A temporal collection is a special collection that is an implementation of Temporal Property.)

The value of using Temporal Property here is that it allows you to store processing rules that vary over time, and still be able to process events with old rules.

Example: A shipping company decides to increase its charge from $10 to $15 on March 14th. On March 22nd it receives an event saying a shipment was made on March 7th. It's important that it processes this event using the rules that were valid on March 7th (a $10 charge) rather than the rules that are valid on the day it processes the event.

Inheritance is a natural concept for structuring agreements. Don't use the inheritance in the programming language for this, instead give the agreement a property for the parent. Set up the processing code so that if there isn't a rule on the child agreement and it looks for a rule on the parent.

When to use it

There are two main strengths to Agreement Dispatcher: it's agreement centered nature and the ability to cope nicely with temporal rules.

Using the agreement as the first step in the dispatch delegation makes it easier to handle cases where the logic for handling events varies depending upon the business agreement in place - which is a common scenario.

Dispatching to rules organized in Temporal Propertys is useful because it provides a good structure to cope with changes in rules while allowing old events to be processed with old rules. This is a common cause of messy conditional date logic in systems that lack that facility.

[TBD: Is it worth making a separate pattern for temporal dispatch?]

I've described this pattern here because I've seen it work well in a number of cases. However one thing that makes me uncomfortable is that I've not seen much in terms of varations or alternatives. The most common way of doing this kind of event processor is to have little or no structure, I've not seen cases where there has been a good alternative structure.

However the two main reasons for doing this suggest alternative variations. If the primary point of variation for your business processing isn't the agreements, then use that alternative as the central point for your dispatcher. The idea of putting processors in temporal collections to allow rules to be executed at different times can be used in other contexts.

Agreement Dispatcher is an example of an Adaptive Object Model[TBD: expand on Adaptive Object Model] and as such brings in its advantages and disadvantages. It does deal with complexity well (at least within the grain of its variability). It can make someone who is familiar with the model very productive. However it is also hard to learn and can be quite intimidating for the newcomer.

Example: Utility Billing (Java)

A particular combination of patterns that I've run into several times is that ofDomain EventAgreement Dispatcher, and Accounting Entry. In this combinationDomain Events are processed by a Agreement Dispatcher which creates Accounting Entrys. This combination works particularly well because Agreement Dispatcherwhich creates Accounting Entrys are easy to adjust - allowing you to use the generic adjustment strategies to handle adjustment of erroneous information.

To illustrate this I'll use a utility billing example. The billing system recieves variousDomain Events and processes them into Accounting Entrys on customer accounts. The model is rather complicated to describe so I'll do it in stages.

  • First I'll show the framework classes' structure
  • Then I'll show how the you wire the framework classes up

Framework Structure

Figure 2 shows the basic classes involved. Here's how this data structure looks in code.

Figure 2

Figure 2: Classes for utility example

The Accounting Event is a usage of Domain Event which here. Its data is straightforward.

class AccountingEvent...
    private EventType type;
    private MfDate whenOccurred;
    private MfDate whenNoticed;
    private Subject subject; 

The subject is an interface that defines methods that are needed for the event processor. The only implementation we are interested in is Customer. Customers have two things of interest to us, a service agreement and a set of accounts which will hold the results of our event processing.

class Customer implements Subject...
    private ServiceAgreement serviceAgreement;
    private Map<AccountType, Account> accounts, savedRealAccounts;
    public Customer(String name) {
        _name = name;
        setUpAccounts();
    }
    void setUpAccounts() {
        accounts = new HashMap<AccountType, Account>();
        for (AccountType type : AccountType.values())
            accounts.put(type, new Account(Currency.USD, type));
    }
    public Account accountFor(AccountType type) {
        assert accounts.containsKey(type);
        return accounts.get(type);
    }
    public void addEntry(Entry arg, AccountType type) {
        accountFor(type).post(arg);
    }
    public Money balanceFor(AccountType key) {
        return accountFor(key).balance();
    }

The accounts are stored in a Map, one for each account type.

The data structure in the service agreement is a bit more complicated. Essentially it consists of a Map where the keys are event types and the values are temporal collections of posting rules. I set up this structure dynamically.

class ServiceAgreement...
    private Map postingRules = new HashMap();
    public void addPostingRule(EventType eventType, PostingRule rule, MfDate date) {
        if (postingRules.get(eventType) == null)
            postingRules.put(eventType, new SingleTemporalCollection());
        getRulesTemporalCollectionFor(eventType).put(date, rule);
    }
    private TemporalCollection getRulesTemporalCollectionFor(EventType eventType) {
        TemporalCollection result = (TemporalCollection) postingRules.get(eventType);
        assert result != null;
        return result;
    }

The temporal collection class is the implementation of Temporal Property I discussed there.

public interface TemporalCollection {
    //get and put at a supplied date
  Object get(MfDate when);
  void put(MfDate at, Object item);
  Object get(int year, int month, int date);
    //get and put at today's date
  Object get();
  void put(Object item);
}

Posting rules are simple processors that know about posting an amount to someAccount. I set them up by indicating which account they should post to and whether they are taxable.

class PostingRule...
    private AccountType type;
    private boolean isTaxable;
    protected PostingRule(AccountType type, boolean isTaxable) {
        this.type = type;
        this.isTaxable = isTaxable;
    }

Posting rules are specialized chunks of code which calculate an amount to be charged and then charge it to the given account.

class PostingRule...
    public void process(AccountingEvent evt) {
        makeEntry(evt, calculateAmount(evt));
        if (isTaxable) generateTax(evt);
    }
    abstract protected Money calculateAmount(AccountingEvent evt);
    private void makeEntry(AccountingEvent evt, Money amount) {
        Entry newEntry = new Entry(amount, evt.getWhenNoticed());
        evt.getSubject().addEntry(newEntry, type);
        evt.addResultingEntry(newEntry);
    }

There are various ways of calculating this amount, and these are left to a subclass. (I also have a mechanism for handling taxes, but I'll defer discussion of that till later. [TBD: add details])

Adding a posting rule for usage

So now let's see how we put a single posting rule into this structure.

Example: In a standard agreement, when we receive an event indicating the customer has used some electricity, we make a charge which is the multiple of the usage and rate defined in the customer's service agreement.

To put this simple agreement into place we need to create a service agreement with a posting rule to handle an accounting event that provides information about the electricity usage.

Here is the usage event.

class Usage...
    private Quantity amount;
    public Usage(Quantity amount, MfDate whenOccurred, MfDate whenNoticed, Customer customer) {
        super(EventType.USAGE, whenOccurred, whenNoticed, customer);
        this.amount = amount;
    }
    public Quantity getAmount() {
        return amount;
    }

In order to be able to calculate a charge for the usage we need a posting rule that can multiply the usage by the rate. For this we'll put together a sublcass of posting rule.

class MultiplyByRatePR...
public class MultiplyByRatePR extends PostingRule {
    public MultiplyByRatePR(AccountType type, boolean isTaxable) {
        super(type, isTaxable);
    }
    protected Money calculateAmount(AccountingEvent evt) {
        Usage usageEvent = (Usage) evt;
        return Money.dollars(usageEvent.getAmount().getAmount() * usageEvent.getRate());
    }
}
class Usage...
    double getRate() {
        return ((Customer) getSubject()).getServiceAgreement().getRate(getWhenOccurred());
    }

To complete the exercise we add the new posting rule into a service agreement

class ExampleTester...
    private ServiceAgreement simpleAgreement() {
        ServiceAgreement result = new ServiceAgreement();
        result.setRate(10, MfDate.PAST);
        result.addPostingRule(EventType.USAGE,
                new MultiplyByRatePR(AccountType.BASE_USAGE, false),
                new MfDate(1999, 10, 1));
        return result;
    }

Figure 3

Figure 3: Class diagram showing the extra classes needed for our simple example.

Figure 4

Figure 4: Object diagram showing how the posting rule is connected to the agreement.

How the framework executes

So that's how a simple agreement is connected together, now we'll look at how it executes.

In the begining the event data would come in from some external source, which I'll conveniently ignore. Instead I assume that a reader will instantiate a usage event object for me and put it on an event list. We can then process the event list to fire up the event. I can capture this in a JUnit test case.

class ExampleTester...
    public void testSimpleRule() {
        Customer mycroft = new Customer ("Mycroft Homes");
        mycroft.setServiceAgreement(simpleAgreement());
        AccountingEvent usageEvent = new Usage(Unit.KWH.amount(50),
                new MfDate(1999, 10, 1),
                new MfDate(1999, 10, 15),
                mycroft);
        EventList eventList = new EventList();
        eventList.add(usageEvent);
        eventList.process();
        assertEquals(Money.dollars(500), mycroft.balanceFor(AccountType.BASE_USAGE));
        assertEquals(Money.dollars(0), mycroft.balanceFor(AccountType.SERVICE));
        assertEquals(Money.dollars(0), mycroft.balanceFor(AccountType.TAX));
    }

Restating what the code says: I create an example customer and assign him the standard agreement I created above. I then create a usage event for mycroft, add it to my event list, and process the event list. The resulting charges are then visible in the Mycroft's accounts.

Figure 5

Figure 5:

Executing all this is an exercise in delegation; as Figure 5 shows it's quite a long chain. I start with the event list, all it does is process its unprocessed events.

class EventList...
    public void process() {
        for (AccountingEvent event : unprocessedEvents()) {
            try {
                event.process();
            } catch (Exception e) {
                if (shouldOnlyLogProcessingErrors) logProcessingError (event, e);
                else throw new RuntimeException(e);
            }
        }
    }

If any events fail to process I log and carry on.

For this example I've made the event class be the event processor. Although initially it makes sense to think of the event processor as a separate class, fundamentally the processor does a lot of initimate manipulations on the event - so it makes sense to put the processing behavior on the event itself following the Information Expert [TBD: expand on Information Expert] princple.

class AccountingEvent...
    public void process() {
        assert !isProcessed;
        if (adjustedEvent != null) adjustedEvent.reverse();
        subject.process(this);
        markProcessed();
    }

Ignore the line about adjustments for now, I'll talk about that more in [TBD: link to adjustments].

Essentially all the event does is delegate to its subject, which delegates to its service agreement since multiple customers share the same service agreement.

class Customer...
    public void process(AccountingEvent e) {
        serviceAgreement.process(e);
    }

The service agreement also wants to just delegate, this time to the posting rule. However this time it's a bit more complicated because the service agreement has to determine which rule to use first.

class ServiceAgreement...
    public void process(AccountingEvent e) {
        getPostingRule(e).process(e);
    }
    private PostingRule getPostingRule(AccountingEvent event) {
        final TemporalCollection rules = getRulesTemporalCollectionFor(event.getEventType());
        if (rules == null) throw new MissingPostingRuleException(this, event);
        try {
            return (PostingRule) rules.get(event.getWhenOccurred());
        } catch(IllegalArgumentException e) {
            throw new MissingPostingRuleException(this, event);
        }
    }

The service agreement looks up the posting rule based on the event type and actual time of the event.

Handling More Cases

With this basic framework in place, we can then start extending it to handle more types of event.

Example: Service calls have a cost based fee. On this standard agreement we charge the fee plus 10% of the fee plus $10 for processing.

To add this extra rule all we need to do is add another posting rule to the agreement, the resulting agreement setup looks like this.

class ExampleTester...
    private ServiceAgreement simpleAgreement2() {
        ServiceAgreement result = new ServiceAgreement();
        result.setRate(10, MfDate.PAST);
        result.addPostingRule(EventType.USAGE,
                new MultiplyByRatePR(AccountType.BASE_USAGE, false),
                new MfDate(1999, 10, 1));
        result.addPostingRule(EventType.SERVICE_CALL,
                new AmountFormulaPR(1.1, Money.dollars(10), AccountType.SERVICE, false),
                new MfDate(1999, 10, 1));
        return result;
   }
public class AmountFormulaPR extends PostingRule {
    private double multiplier;
    private Money fixedFee;
    public AmountFormulaPR(double multiplier, Money fixedFee, AccountType type, boolean isTaxable) {
        super(type, isTaxable);
        this.multiplier = multiplier;
        this.fixedFee = fixedFee;
    }
    protected Money calculateAmount(AccountingEvent evt) {
        Money eventAmount = ((MonetaryEvent) evt).getAmount();
        return (Money) eventAmount.multiply(multiplier).add(fixedFee);
    }
}

We need a new kind of posting rule for this that supports a simple formula - in this case just multiplying an amount by a multipier and adding a constant.

The other area of variation is that of time.

Example: On Dec 1 the handling fee for service calls goes up to $15

To add this we add another posting rule with the same event type but a different date. So now our agreement definition looks like this.

class ExampleTester...
    private ServiceAgreement simpleAgreement3() {
        ServiceAgreement result = new ServiceAgreement();
        result.setRate(10, MfDate.PAST);
        result.addPostingRule(EventType.USAGE,
                new MultiplyByRatePR(AccountType.BASE_USAGE, false),
                new MfDate(1999, 10, 1));
        result.addPostingRule(EventType.SERVICE_CALL,
                new AmountFormulaPR(1.1, Money.dollars(10), AccountType.SERVICE, false),
                new MfDate(1999, 10, 1));
        result.addPostingRule(EventType.SERVICE_CALL,
                new AmountFormulaPR(1.1, Money.dollars(15), AccountType.SERVICE, false),
                new MfDate(1999, 12, 1));
        return result;
   }

With this in place service calls before December incur the lower fee but those after get the higher fee.

class ExampleTester...
    public void testSimpleRule3() {
        Customer mycroft = new Customer("Mycroft Homes");
        mycroft.setServiceAgreement(simpleAgreement3());
        EventList eventList = new EventList();

        //service call before rule change
        AccountingEvent call1 = new MonetaryEvent(Money.dollars(100),
                EventType.SERVICE_CALL,
                new MfDate(1999, 10, 1),
                new MfDate(1999, 10, 15),
                mycroft);
        eventList.add(call1);
        eventList.process();
        assertEquals(Money.dollars(0), mycroft.balanceFor(AccountType.BASE_USAGE));
        assertEquals(Money.dollars(120), mycroft.balanceFor(AccountType.SERVICE));
        assertEquals(Money.dollars(0), mycroft.balanceFor(AccountType.TAX));

        //service call after rule change
        AccountingEvent call2 = new MonetaryEvent(Money.dollars(100),
                EventType.SERVICE_CALL,
                new MfDate(1999, 12, 1),
                new MfDate(1999, 12, 15),
                mycroft);
        eventList.add(call2);
        eventList.process();
        assertEquals(Money.dollars(0), mycroft.balanceFor(AccountType.BASE_USAGE));
        assertEquals(Money.dollars(245), mycroft.balanceFor(AccountType.SERVICE));
        assertEquals(Money.dollars(0), mycroft.balanceFor(AccountType.TAX));
    }
资源下载链接为: https://pan.quark.cn/s/9648a1f24758 这个HTML文件是一个专门设计的网页,适合在告白或纪念日这样的特殊时刻送给女朋友,给她带来惊喜。它通过HTML技术,将普通文字转化为富有情感和创意的表达方式,让数字媒体也能传递深情。HTML(HyperText Markup Language)是构建网页的基础语言,通过标签描述网页结构和内容,让浏览器正确展示页面。在这个特效网页中,开发者可能使用了HTML5的新特性,比如音频、视频、Canvas画布或WebGL图形,来提升视觉效果和交互体验。 原本这个文件可能是基于ASP.NET技术构建的,其扩展名是“.aspx”。ASP.NET是微软开发的一个服务器端Web应用程序框架,支持多种编程语言(如C#或VB.NET)来编写动态网页。但为了在本地直接运行,不依赖服务器,开发者将其转换为纯静态的HTML格式,只需浏览器即可打开查看。 在使用这个HTML特效页时,建议使用Internet Explorer(IE)浏览器,因为一些老的或特定的网页特效可能只在IE上表现正常,尤其是那些依赖ActiveX控件或IE特有功能的页面。不过,由于IE逐渐被淘汰,现代网页可能不再对其进行优化,因此在其他现代浏览器上运行可能会出现问题。 压缩包内的文件“yangyisen0713-7561403-biaobai(html版本)_1598430618”是经过压缩的HTML文件,可能包含图片、CSS样式表和JavaScript脚本等资源。用户需要先解压,然后在浏览器中打开HTML文件,就能看到预设的告白或纪念日特效。 这个项目展示了HTML作为动态和互动内容载体的强大能力,也提醒我们,尽管技术在进步,但有时复古的方式(如使用IE浏览器)仍能唤起怀旧之情。在准备类似的个性化礼物时,掌握基本的HTML和网页制作技巧非常
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值