Updated: 2/11/2002
The Object Mentor website has an example application that has been used to compare various OO design approaches. The application is based on a public power utility scenario and its various rate calculation strategies for different "types" or ranges of customers and the influence of season and location.
The original paper (PDF format) can be found here: http://www.objectmentor.com/publications/articlesByDate.html
Search for "Mellor" if do not immediately see it listed.
"By putting the entire rate policy in the same module, as part of the same algorithm, we risk interdependencies between them. A single change to one part of the policy has the potential to affect them all."
I am not sure if the author means "same module" or "same routine". I will address both; routines first.
There is no law that says that all procedural code has to be in one subroutine. Thus, I split it into multiple routines (see code sample below). Why the OO author did not consider this approach is very odd. (Note that I included routines not shown in the C version for completeness.)
I won't claim that all procedural languages allow easy routine-level recompilation/replacement/splitting, but there is no by-birth limit on the paradigm itself. It seems many readers of the Object Mentor use C as their guide to what procedural languages are or are not capable of.
Further, I probably could have put each routine in my example into its own module.
I am not sure if this is common in many business applications. Most businesses keep their IT department to a bear minimum, meaning that things have to be easy to get to, understand, change, and get out.
Excess layers may hamper this. I think safety and convenience are sometimes mutually exclusive. In addition, programmers may be encouraged to take "hacky" shortcuts if there is a lot of red-tape in their way of getting things done. Thus, extra "protection layers" may have the opposite effect if its original goal.
It is hard to say without actually seeing the pattern of growth. Will the needed details be in a few tables or lots of different tables? See Join Issues in the Business Modeling article for more on this issue.
If the code is kept in the case statement instead of calling a separate routine, then only one routine need be changed. However, some claim that this risks altering existing code (nearby case blocks). I would point out that sometimes one has to risk "bumping" nearby methods if only one method needs changing in a class.They also claim that keeping the code in a case statement makes the subroutine "too long". I view each case block as a separate unit the way an OO fan may view methods as separate units. Blocks is blocks. Case blocks are simply small blocks nested in larger blocks the same way that methods are usually nested in a class. It is a typical aspect tradeoff as far as what we allow to be "exposed" by having them together. The granularity of what can be compiled or changed as an independent unit is often a language-dependent and/or IDE issue.
If such was truly a problem, then a control table could perhaps be used, where fields in the Region table either directly contain or reference code (via a function name). Adding a new region would require no changes to existing code.
In some ways this is better than the OO approach if there is already a Region table being used. This is because a subclass would have to "register" itself somewhere to something to be known to the existing application The "registry" is probably some kind of table or list. IOW, such a design has a bit of Table Oriented Programming design to it.
But, if there is already a Region table, then the registry is duplicating the region list. In other words, a violation of the Single Choice principle. One probably cannot add other region-related attributes to this registry. It makes more sense to take advantage of a list (table) already in existence, or at least one that has the ability to accept new attributes.
Although I don't have a dedicated Region table in my design, if region becomes a central concept, then the formation of one is likely IMO.Someone pointed out that the "registry" may not have to be something that the programmer manages, and thus should not "count" as duplication. I will leave it to the reader to decide on this. It may depend on the programming environment and tool implementation.
See the countries tax example for similar issues, especially with regard to tablizing the divisions instead of using case statements. Also see the "change pattern" notes below.
Further, polymorphism can often degenerate into something else. Case statements are often more flexible under many of these degeneration scenarios because code does not have to be moved. Only the case or IF criteria need be changed in most cases. (Or, case blocks changed to IF blocks.)
We don't have enough information about the change patterns of this application to know whether region-based dispatching will expand, or change into something else. My approach is equally ready for both, while the OO design is too highly coupled to region divisions in my opinion. OO solutions often assume that the change pattern is "yet more of the same" with regard to adding more variations/subtypes/divisions.
I can imagine rules like, "if in region X and an approved member of the Conservation-B program, then ....". An OO version may further split the Region-X subclass into a conservation-B and non-conservation-B sub-sub-class (under region-X). This risks degenerating into the Mega-Name Pattern.
I suspect that in practice, the Mellor "sliding scale" formula would be defined under (translated to) a schedule. I suspect the author described it as a formula to save space or avoid producing a table, which is more effort and sometimes harder to electronically distribute than a table.
Although some of this kind of pattern could perhaps be represented by splitting customers into "types", or even calculation-specific (localized) types for the "sub-branches", I see no real advantage of such. The range, scope, and granularity of such types would be subject to unpredictable change. Perhaps one could say, "I think better when I turn things into subtypes". However, they should be careful about extrapolating such a mental preference into other individuals without some external metric for reducing code size, reducing change impact effects, etc.
So why doesn't the stated Mellor Problem follow this pattern? It could just be an exception. However, I suspect that many OOP authors and fans subconsciously exaggerate the presents and occurrence of sub-types. OOP textbooks and training materials point out the pattern so often, that peoples' minds start to see the pattern where it does not really exists, or remember its presence when it does occur, but not its absence. Reading the newspaper may give one the impression that plane crashes are common when in fact planes are more safe per-mile-traveled than cars. Mr. Martin may be more likely to collect examples that fit patterns OOP is optimized for or only present a view of the problem that allegedly fits OOP's strengths. (I have not independently verified the accuracy of the Mellor Problem, so I am only presenting speculation at this point.)
There are many other potential change-patterns that we must deal with. In business applications, usually there is an upper limit to the number of "levels" or mutually-exclusive divisions before things "degenerate" into other patterns. From the customer's perspective, the most flexible solutions are often independent features.
For example, some computer manufactures offer "levels" of PC's. They may have an entry-level PC, a mid-level, and high-end. Sometimes this is divided up into home and business users. A typical taxonomy might resemble:
PC CUSTOMER "LEVEL" TAXONOMY
Home User PC
Low End
Medium End
High End (serious game player)
Business User PC
Low End
Medium End
High End
However, this approach is often not satisfactory
for the customer. They might want only some
of the features of the high-end machines, but don't
want to pay for the rest of those features. I have
seen many ads by computer companies with quotation
forms that treat each feature independently.
QUOTE/ORDER FORM FOR CUSTOM PC Graphics card: [ ] Low [ ] Medium [ ] High RAM Amount: ______ Case: [ ] Flat [ ] Mini-tower [ ] Full-tower Disk Drive: [ ] 20g [ ] 50g [ ] 100g Other: _____ OS: [ ] Windows [ ] Red Hat [ ] OS/2 Other: _____ CPU: [ ] AMD-Y50 [ ] AMD-Y70 [ ] Intel ...... Etc......(The form is simplified for illustration purposes.)
The first approach bundles or couples features such as graphics card and case type. Although it may simplify manufacturing, it is often not the best way to satisfy customers.
I once wanted to purchase a vehicle with leather seats because leather is easier on family allergies than cloth seats. However, the dealer said we had to purchase the "high-end" model in order to get leather seats. Instead, we found a different dealer who would install leather seats in the mid-level model for a reasonable amount. Besides, there were styling features on the high-end model that we really did not want.
The given Mellor customer brake-down in many ways fits the "level" model rather than the feature-independent model. For example, the hours of notice could be made continuous. It might also grow independent of other power-related features. For example, there may be one plan for large service businesses, and another for manufacturers. Yet, both of these may still have hours-of-notice choices.
Thus, I find it not very change-friendly to couple features in software design unless there is some compelling force in the problem space that "glues" such features together. Perhaps State governments don't care about customer convenience and are stuck in their ways (arbitrary, inflexible categories). However, laws can still change in the blink of an eye, and we should prepare for them.
In extreme cases, independent feature implimentations tend to resemble the hyper-grid pattern. A study of hyper-grid issues may trigger some ideas about managing changes in a Mellor-like problem.
I could have even rolled up the consumer strategy flags into that one strategy field by having a "reg_consumer" and "lifeline" strategy name. However, I felt that it may be best to keep these separate without knowing more about typical change patterns for that organization. Also, the two consumer flags could possible be rolled into a single "ConsumerStrategy" field. This would allow more strategies to be added without changing the schema. In fact, they could all be rolled up into a single strategy, with possible dispatching codes such as reg_consumer, lifeline, reg_biz, indust, onehour, no_notice. These could even be used to dispatch routines without a case statement. Just something to ponder.Also, I use the Sites (location) table as the record (instance) source instead of the Billing table. In other words, it is by site instead of by customer, unlike the original. This has two advantages. First, it avoids having to implement a loop/iterator under the business/industrial section; and second, it allows one to break the bill out by site, which real customers would most likely be happy to see (if not demand it). Note that I have it save the calculated site value.
This approach may change how bulk discounts (multi-site) are applied. A volume discount would probably be calculated after all the site amounts are calculated.
I used a "RegionMap" table to map zip-codes to territories. This avoids the problem of assigning territory codes to every site when the area ranges change. If by chance zip-code is not fine-grained enough, or there are exceptions, then the "TerritoryCalc" routine can be altered to handle them. (The original did not directly show how the territory calculation/lookup was done.)
The Billing table, not shown, has fields "BillingID", "Name", and several typical address fields. See above analysis footnote about alternative schema design strategies.
I do agree with many of the points presented in the original. However, the reasoning seams to end when it comes to procedural/relational comparisons.
It would also be nice if a full code example was included with the original document, instead of just UML-like diagrams.
Still, there are places like the "SlidingScale" routine that could probably use some local factoring of rates and ranges so that one variable has to be changed instead of multiple copies of the number. Or, even make it more generic (not dependent on "rs" for example) if sliding scales are used for other parts of the system as well.
rs!amt = calculatedAmt
rs.update
Microsoft DAO often needs an ".edit" command also,
however, ADO does not. ADO was not available in
this version of the interpreter. It may have otherwise
solved the direct access problem also. The direct
access syntax is especially convenient when updating
many fields.
Option Compare Database
Option Explicit
' Mellor's Problem - version 1.0b
Dim stdDB As Database ' DB connection
'--------------------------
Sub main()
Set stdDB = CurrentDb
calcMany "1=1" ' true = all
MsgBox "Done!"
End Sub
'--------------------------
Sub calcMany(criteria)
' calculate multiple sites with a criteria expression
Dim sql, rs
sql = "SELECT * FROM Sites LEFT JOIN RegionMap "
sql = sql & " ON Sites.zipCode = RegionMap.zipCode "
sql = sql & " WHERE " & criteria
' Open the recordset and calc each record
Set rs = stdDB.OpenRecordset(sql, dbOpenDynaset)
Do While Not rs.EOF
CalcAmt (rs)
rs.MoveNext
Loop
rs.Close
End Sub
'--------------------------
Sub CalcAmt(rs) ' Calculate amount for a given record
Dim amt
amt = 0
If rs!isConsumer Then
amt = CalcConsumer(rs)
Else
amt = slidingScale(rs)
amt = amt * IndustrialDiscount(rs)
End If
'---- Now save it
Dim sql
sql = "UPDATE sites SET amt = " & amt & " WHERE siteID = " & rs!siteid
stdDB.Execute (sql) ' Edit command would not work, used SQL instead
End Sub
'-------------------------
Function CalcConsumer(rs)
Dim result, KWH
KWH = rs!KWH
If rs!isLifeline Then ' low-income discount
If KWH <= 100 Then
result = KWH * 0.03
ElseIf KWH <= 200 Then
result = 3 + (KWH - 100) * 0.05
Else
result = territoryCalc(rs)
End If
Else
result = territoryCalc(rs)
End If
CalcConsumer = result ' return result
End Function
'-------------------------
Function IndustrialDiscount(rs)
Dim result
Select Case LCase(rs!interruptStrategy)
Case "none"
result = 1
Case "indust"
result = 0.95
Case "onehour"
result = 0.9
Case "no_notice"
result = 0.8
Case Else
MsgBox "Error: missing Interrupt Strategy for site " & rs!siteid
result = 1
End Select
IndustrialDiscount = result ' return value
End Function
'---------------------
Function territoryCalc(rs)
Dim rate
Select Case rs!territID
Case 1, 2
rate = IIf(isWinter(), 0.07, 0.06)
Case 3
rate = 0.065
Case Else
MsgBox "Error: unknown region for site " & rs!siteid
End Select
' Insert any exceptions to zipCode-based lookup here
territoryCalc = rs!KWH * rate
End Function
'-----------------------
Function slidingScale(rs)
Dim slide, rate, KWH
KWH = rs!KWH
If KWH < 1000 Then
slide = (KWH - 1) / 999
rate = 0.09 - (0.04 * slide)
Else
rate = 0.05
End If
slidingScale = (KWH * rate)
End Function
'-----------------------
Function isWinter()
isWinter = False ' temp filler
End Function