Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.

This is the fifth article in an ongoing series on Clarion embed points and how almost all Clarion developers use them in the wrong way. If you haven't read the first article in the series, then you might find that statement offensive. You might be thinking, "What does Harms know anyway? I'm a professional developer. I know how to write embed code."

...

I think it almost always looks like a class library, and I've taken some heat for saying that. In this article I'll defend my point of view, and I'll do it by beginning a transformation of the Invoice application which ships with Clarion.

The Invoice app

The example Invoice app isn't something you'd want to use in real life, but I've chosen it because we all have it, and because many of us have, at one time or another, had to deal with invoicing.

Figure 1 shows the customer order window, which has a child browse showing line items for each invoice.

Image Modified
Figure 1. Browsing orders for a customer

Figure 2 shows the order detail form. Yeah, I know, none of this is pretty. But that's okay - it's old example code, and Clarion devs sometimes have to deal with old code.

Image Modified
Figure 2. Changing a detail record

Here's the Detail file structure:

...

In Figure 3 I've added the calculated fields as display strings. I've also added a button to force a call to the CalcValues routine so I can see the results of several changes at one time.

Image Modified
Figure 3. Displaying the calculated fields

In Figure 3 I've added a new detail line, and I've selected an Aster, quantity two2. I've pressed the Recalculate Values button. You , and you can see that the values all appear to be correct.

In Figure 4 I've changed the tax rate to 5%.

Image Modified
Figure 4. Setting the tax rate

So far, so good.

In Figure 5 I've added a 10% discount.

Image Modified
Figure 5. Adding a discount

Whoops. The discount is calculated as $3.00, which is correct, at least for the total if not for the per-item price (the Invoice app has no formal notion of "extended" price). Why then is the savings only $1.65?

In Figure 6, I've set tax rate to 5% and the discount to 1%.

Image Modified
Figure 6. A 1% discount and a negative savings

According to Figure 6, offering a 1% discount to a customer results in a negative savings. Interesting.

In Figure 7, the wheels have well and truly come off.

Image Modified
Figure 7. Setting the tax rate to zero

I've set the discount rate and the tax rate to zero, but I'm still showing a tax paid of $1.50, a discount of 30 cents, and a savings of $-1.19.

...

The solution is to take the code out of the embed and put it somewhere it can be both reused and tested.

The beautiful black box

The important thing about business logic is that you should be able to use it without having to know exactly how it works. That may sound a bit counterintutive counter intuitive - after all, if it's your own code, shouldn't you know how it works? If you wrote it, yes, you should. But we all use black box code all the time. How many Clarion devs really know what's going on in Windows when a mouse is moved or a button clicked? How about the file driver system? Or even ABC? We don't need to know how these things all work; we just need to know how to use them.

...

  • Total price
  • Tax amount
  • Discount amount
  • Total cost
  • Savings

How to design a black box

Once you know the inputs and outputs, it's easy to design your black box. You just need to decide whether to create your black box as a procedure or a class.

Here's how you might declare a black box procedure to replace the CalcValues routine:

...

And here's how you could declare a similar black box class to replace the CalcValues routine:

InvoiceDetail       Class
Init                    Procedureprocedure(*decimal price,|
                            long quantity,|
                            *decimal discountRate, |
                            *decimal taxRate)
GetTotalCost            procedure(*decimal totalCost)
GetTaxAmount            procedure(*decimal taxAmount)
GetDiscountAmount       procedure(*decimal discountAmount)
GetSavings              procedure(*decimal savings)
                    End 

...

It's also easier to extend the InvoiceDetail class without breaking existing functionality. As written, the CalcValues routine calculates total cost including taxes but not extended price excluding taxes. Adding a method to get the extended price would have zero effect on code already using the class, whereas with the procedural solution you'd have to make the additonal additional parameter ommitableomittable.

In this very limited set of calculations and results, the procedural approach is already starting to show some strain. What happens when things get really interesting, as when you need to start tracking multiple taxes instead of a single tax?

In other words, the evolution of programming from procedural code to object-oriented code isn't without very good reason. OOP really does give you a greatly expanded toolbox for solving programming problems.

Testing

Whichever approach you take, procedural or class, your code needs to be testable. And I don't mean the kind of testing that involves someone running an app, entering values, and making sure the results are correct. Yes, you do still need to do that, but by the time your code gets to that point you should already have a high level of confidence in the underlying logic.

...

In the case of the CalcValues routine code, isolation is pretty easy to achieve. Although the routine code operates directly on the fields in the Detail record, this isn't a requirement. The logic can all be made internal to the class; that way the class can be used anywhere at all without modification, and the only embed code needed is whatever it takes to assign the values to and from the class. Presto! You've successfully removed the business logic from the embed point!

What's next

There are a couple of ways to approach unit testing. One is to take existing code, refactor it if necessary to make it testable, and then write a bunch of tests to see whether it's working as you expect. The other is to start by writing tests, and then create the code to make those tests compile and then pass. This latter approach is called test-driven development (TDD) and can be enormously useful. Although I do have some existing code to work with, it's broken enough that I'm going to start fresh, using the routine code only as a reference.

...