DEV Community

Cover image for Day 8: Declarative Business Rules and Computed Fields
Jeff Tsai
Jeff Tsai

Posted on Originally published at hackmd.io

Day 8: Declarative Business Rules and Computed Fields

What the last few posts grew out of definitions were concrete products: tables, CRUD statements, screens on four front ends. Today's item builds nothing. It takes the expressions written into the definition and evaluates them, either writing the result back into a field (ValueExpression) or stopping the whole save (FormRule).

Required fields, range checks, arithmetic between fields: the content differs at every company and the shape is identical. All of them are take a few fields on this row, work something out, and stop if it does not hold. An identical shape is something a mechanism can absorb, so changing simple rules and calculations of this kind takes no code at all.

What gets standardized is the shape, not the content.

This post covers:

  1. Why FormRule and ValueExpression live in the same definition as the fields
  2. The four points where evaluation happens, and the asymmetry of validation running only on the back end
  3. How the front end and the back end share one expression
  4. The two kinds an expression cannot state, and why they differ in nature
  5. Declarative as the default rather than the only route, with application code stacking on top

1. FormRule lives in the same definition as the fields

Declarative logic has two homes. The two extracts below come from the case study's order form, Order.FormSchema.xml. One hangs off a field:

<FormField FieldName="amount" DbType="Currency" NumberKind="Amount" ReadOnly="true"
           ValueExpression="quantity * unit_price * (1 - discount)" />
Enter fullscreen mode Exit fullscreen mode

The other hangs under the FormSchema as a whole:

<Rules>
  <FormRule RuleId="customer_required" Trigger="BeforeSave"
            When="status != &quot;Draft&quot;"
            Condition="customer_rowid != Guid.Empty"
            Message="Please select a customer for the order." />
  <FormRule RuleId="quantity_positive" Trigger="BeforeSave" TargetTable="OrderDetail"
            Condition="quantity &gt; 0"
            Message="Every detail line must have a quantity greater than zero." />
</Rules>
Enter fullscreen mode Exit fullscreen mode

Both live in FormSchema, and there is no separate rules file. One FormRule reads as a single judgment, and the order of its attributes is the order of its meaning:

  • Trigger: when to check. Both rules in the example check before saving, and checking before a delete has to be marked separately
  • When: whether this check applies this time. If it does not hold, the whole rule is skipped
  • Condition: the condition that has to hold
  • Message: the sentence the user sees when it does not
  • ValueExpression: not part of that judgment at all. It hangs off a field, and what it computes is written back into a column

The When in the example means a customer can be left blank while the order is still a draft, and is required once it leaves draft. It is a separate attribute from Condition rather than folded into one sentence because the folded shape is easy to write backwards, and a rule written backwards does not report an error. It just passes, from then on, always.

Living alongside the fields is deliberate. Apart from Trigger, the subject of every one of those attributes is a field. Rename a field, delete it or change its type, and the FormRule has to move with it. Two separate files means one of them goes stale first, and the stale one does not report an error either. It quietly stops stopping anything, one day.

One effect is especially visible on FormRule. Among the things that change most often after a system goes live, validation conditions come near the top: the maximum discount used to be 20 percent and has been loosened to 30; this document used to be saveable at zero value and now is not. The change is small enough to state in one sentence, and going through code costs a whole round of compiling, packaging and deploying. FormRule is data, so what changes is one string.


2. Evaluation happens at only a few points

Point Where What happens
When a row is created Front end Default value expressions are evaluated once and filled into the fields still empty
When a field is edited Front end The computed fields that depend on it are recalculated immediately, and the amount on screen moves with them
Before a save Back end New rows get their still empty default values filled in first, then new and modified rows recalculate their computed fields, and last the rules whose Trigger is BeforeSave run. Rows nobody touched are not recalculated, because recalculating would mark them as touched
Before a delete Back end The rules whose Trigger is BeforeDelete run. What they check against is not the data in the user's hands, it is the record the framework reads back from the database separately

The front end is doing arithmetic too, which raises a question: at save time, should the values it worked out be trusted?

The framework's answer is no. That recalculation before saving is unconditional, and every ValueExpression result sent up from the front end is overwritten. Data integrity cannot be entrusted to the front end. It may have calculated wrongly, and it may not even be the front end you wrote.

That decision buys a property: the front-end half can be absent entirely. A front end that cannot run live evaluation is simply one where the amount does not move as you type. The operation is unchanged, and the value stored in the database is identical. Live preview is experience, not part of correctness.

An asymmetry: validation runs only on the back end

Computed fields are calculated on both sides. FormRule runs only at the business object step on the back end, and never once on the front end.

The difference is what happens when each one is wrong:

  • A computed field the front end got wrong costs nothing, because the recalculation before saving overwrites it.
  • Validation has no overwriting step. What the front end blocks, the back end never sees at all, and what the front end lets through, the back end blocks anyway.

So moving a copy of validation to the front end buys the user one round trip of waiting, at the cost of the same rule being evaluated in two places. Sooner or later the two stop lining up, and then there is one more question to answer: whose message counts.

FormRule has exactly one line of defense, the step before saving on the back end. What the front end is responsible for is letting the user watch the numbers move.

Where these points sit inside the save flow, and how they relate to the part that writes to the database, is the subject of Extension Points on a Business Object and When to Override Them.


3. One expression, and both sides have to get the same answer

If the same expression is written once on each side, then next year somebody changes only one of them, the screen shows one number and what gets stored is another. Each side looks correct on its own, which is what makes it take so long to track down.

The framework has both sides reference the same code. One line of ValueExpression applies to the front end and the back end at once. The evaluation code sits in the definition layer with no server only dependencies, so what the back end calls before saving and what the front end calls on every keystroke are the same method on the same class. Whether the two could produce different answers is structurally not a question.

That one line of expression gives something else as well. To recalculate live, the front end first has to know which computed fields to recalculate when a given field is edited, and that mapping is not configured separately. It is worked backwards by parsing the ValueExpression once and taking the names out of it.


4. The two kinds an expression cannot state

What an expression can see is the batch of data this operation is holding, and for now only this row of it is opened up. An amount is this row's quantity times this row's unit price, and a required field rule looks at whether this row filled it in.

That range decides what currently cannot be declared, and those things happen to fall into two kinds that are nothing like each other:

Kind Examples Nature
Cross row There must be at least one detail line, or the master total equals the sum of the detail amounts The data is already in hand, so this one is a matter of progress
Has to ask the database A status cannot go back from shipped to draft, or assign an order number The data is not in hand, and this one is left outside deliberately

The cross row kind is reachable by adding an aggregate function, letting an expression state the sum of the detail amounts, since those rows are in the same batch as the master to begin with. The hard part is that evaluation then has to queue up again: the master total depends on the detail amounts, a detail amount is itself an expression, and the ordering needs rules of its own. There is a solution for that. It is simply not implemented yet.

The other kind is what genuinely cannot be stated. The moment an expression can query the database it stops being portable, the front end can no longer evaluate it, and the property from the previous section, both sides referencing the same code, is the first thing to break. So the line is not drawn at one row versus several rows. It is drawn at whether the answer is inside this batch of data. What is inside will be reachable eventually. What requires leaving it to go and ask is withheld on purpose.


5. Declarative is the default, not the only route

The doubt raised back in The Northwind Case Study: Eight Forms and a Custom Business Object is at its most direct here: if validation can only be written as a Condition, what happens to whatever an expression cannot state?

Half the answer is in the previous section, which is that what cannot be stated gets written as code. The other half is that declarative was never the only route. It is the default.

  • That whole evaluation sequence is itself the default behavior of the business object's before saving step, and that step is open to being overridden.
  • An application that overrides it calls the default part first, so the rules still run and the computed fields are still calculated, and then continues into its own code. The two stack. It is not a choice between them.
  • One level further down there is a finer granularity again. FormRule is data, and each rule carries its own Enabled and Order, so a single rule can be disabled and the order within one point can be specified. Turning one rule off does not mean touching the whole definition, let alone changing code.

Flexibility sits in the same shape here as it does at the layout layer, covered in FormLayout and Screen Generation for Multiple Front Ends: not in the capability, in the granularity.


6. The case study: what the definition absorbed, and what stayed in code

One ValueExpression plus three FormRule entries is everything the case study currently declares. What they replaced was code that used to sit in the order's business object: the per row amount calculation, and three checks on customer, product and quantity.

Four things are still in code, and they split two and two across the two kinds above:

  • Cross row: at least one detail line, and the master total
  • Has to ask the database: the status transition, and the order number

The case study post cut the same pile once before, asking whether each thing ought to be written by the application. Today's line asks why each one cannot be declared. The two are not asking the same question, so an item does not have to land in the matching place. The status transition and the order number were on opposite sides back then, one being a business judgment that differs at every company and the other being something that should be absorbed and has not been yet, while today's line puts them both in the same kind.

That line has moved, and nobody will tell you

The order's business object does one more thing that the case study did not count: on create, it sets the order date to today. The framework already does that. A date typed field gets today's date when a row is created, with nothing declared at all. The difference is that when the other items moved into definitions somebody went back and deleted the code, and for this one nobody did. That line still runs, the tests are still green, and it simply has no reason to exist any more.

Every notch the declarative side moves forward turns a batch of things on the code side from necessary into residue.

The case study put it as what stays in code being partly what belonged there all along, and partly what has simply not been taken over yet. There is a third kind: already taken over, with nobody having gone back to remove it. Of the three that one is the hardest to find, because everything about it is fine. It is just written for nothing.


In closing

Of the four things left in the case study's code, only one is a rule this company set for itself. The other three are not business judgments, they are places where the declarative side failed to catch them. And that answer has a shelf life: the cross row half disappears once aggregate functions and evaluation ordering arrive, and only the half that has to ask the database is outside on purpose.

So which parts should be written as code is never a list, it is a line that moves. What a framework designer can do is not to draw it in the right place once, it is to make sure that when it does move forward, not one word of the hundreds of definitions already written has to change.

The next post, Data and Definitions Split Apart, So Forms Need No Types of Their Own, is about the data container that travels all the way across the layers: why a thousand tables do not each get a type of their own, and how the bill for losing compile time checking gets paid.


๐Ÿ“š Part of the thirty-post series ERP Architect's Notes: Designing a Definition-Driven Framework

๐Ÿ“˜ Original article on HackMD, in Traditional Chinese:
๐Ÿ‘‰ https://hackmd.io/@jeff377/day-08-declarative-rules

๐Ÿ“ข You are welcome to republish this, please credit the source.
๐Ÿ“ฌ Follow for more engineering notes:
Facebook ๏ฝœ HackMD ๏ฝœ GitHub ๏ฝœ NuGet

Top comments (0)