I wonder what the best practice is for handling optional calls, special cases etc. within procedural code (PL/SQL without OOP). We have a program with lots of configuration and things that will or will not happen because of database state etc. In terms of splitting my code, most of the times I begin in splitting it by domain concepts, so in the following example someGoodNameAction
within main
whould be one of these.
Sometimes those routines grow and there will be many special cases latter on. I wonder if those should be handled within such a procedure or if that procedure again should be splitted into n procedures per case. This would keep those procedures pretty easy in clean but would lead in many, many, many procedures with names like someGoodNameActionSpecialCaseN
. So one domain concept could easily explode into 8, 16 or 32 procedures without those conditions and there would be hundreds of procedures and of course a lot of duplication, although just duplication in terms of function/procedure calls not in terms of knowledge.
Those procedures aren't that hard to maintain but they get pretty ugly and have a lot of s.. inside them that won't be called for many cases etc. On the other hand a lot of ultraspecific procedures for special cases don't seem quite right either.
Simplified pseudo code:
function someGoodNameAction1(row) {
if (someGuard1(row)) {
log('sh.. hit the fan!');
return;
}
if (someGuard2(row)) {
log('sh.. hit the fan.. again!');
return;
}
someSubRoutine(row);
maybeModifiedRow = row;
if (specialCase2(row)) {
maybeModifiedRow = enrichRow(row);
}
if (specialCase3(maybeModifiedRow)) {
someOtherSubRoutine(maybeModifiedRow);
}
if (specialCase3(maybeModifiedRow)) {
someData = getSomeData(maybeModifiedRow);
if (someData.field > maybeModifiedRow.field) {
log('sh....');
return;
} else if (someData.field2 == maybeModifiedRow.field2) {
log('oh nooo..');
return;
}
}
someOtherOtherSubroutine(maybeModifiedRow);
someOtherData = getSomeOtherData(maybeModifiedRow);
if (someCondition(someOtherData)) {
someFinalSubRoutine1(maybeModifiedRow);
} else {
someFinalSubRoutine2(maybeModifiedRow);
}
}
function main() {
rows = getSomeRecords();
for (row in rows) {
if (isGoodName1()) {
someGoodNameAction1(row);
} else if (isGoodName2()) {
someGoodNameAction2(row);
}
}
}
Edit: Some clarification. Yes I know about packages in PL/SQL and those functions and procedures would be inside of one. And there would be a lot of calls to other packages etc.
To have a more realistic example, the thing I was thinking of was the code of a material flow controller, or some procedure inside that. The main function would loop over some records inside a database which are provided by an automated warehouse and contain conveyors informations. Which pallet is on the conveyor, the operating mode, maybe something like weight, height, length of the pallet etc.
So the main loop would seperate those rows into different types of conveyors. I was thinkin about a scale/profile control, which would be someGoodNameAction1
. Lets call it shapeControl or checkPoint
.
Inside that there might be conditional code. If the pallet is going into the highbay rack the height must be checked and maybe the pallet won't get in. Same for maximum weight.
But the weight still have to be checked to make sure the pallet data is ok and the right item is on there or the excepted weight is at least close to the one from the scale.
And if the pallet already have a transport that could be kept if not it will get one into the highbay rack or maybe it is a pallet for some other destination etc. Or it is a specific pallet type or some special case for a special item.
I could keep those conditions and optional calls inside the "checkPoint" routine like in the previous example which makes it kinda messy. Or that checkPoint routine would have a lot of specific subroutines (exaggerated names just to be clear), e.g.:
checkPoint_PalletType1_ForHighbayRack_NonSpecialItem
checkPoint_PalletType1_ForHighbayRack_SpecialItem
checkPoint_PalletType1_ForOtherDestination_SpecialItem
checkPoint_PalletType1_ForOtherDestination_NonSpecialItem
checkPoint_PalletType1_TransportAlreadyExists_SpecialItem
checkPoint_PalletType1_TransportAlreadyExists_NonSpecialItem
checkPoint_PalletType2_ForHighbayRack_NonSpecialItem
checkPoint_PalletType2_ForHighbayRack_SpecialItem
checkPoint_PalletType2_ForOtherDestination_SpecialItem
checkPoint_PalletType2_ForOtherDestination_NonSpecialItem
checkPoint_PalletType2_TransportAlreadyExists_SpecialItem
checkPoint_PalletType2_TransportAlreadyExists_NonSpecialItem
checkPoint_PalletType3_ForHighbayRack_NonSpecialItem
checkPoint_PalletType3_ForHighbayRack_SpecialItem
checkPoint_PalletType3_ForOtherDestination_SpecialItem
checkPoint_PalletType3_ForOtherDestination_NonSpecialItem
checkPoint_PalletType3_TransportAlreadyExists_SpecialItem
checkPoint_PalletType3_TransportAlreadyExists_NonSpecialItem
Those would be highly specific for a single case and wouldn't have any conditions. Some might even never happen. But at the end that are a lot of routines and there are of course some more conveyor types inside the main loop too. So those top level routines would reach the hundreds and of course there would be some integration code to decide which of those functions to call, so there might be a isCheckPoint_PalletType3_TransportAlreadyExists_NonSpecialItem
function too, to check if that procedure should be called. And any new case would lead to an explosion of new functions and procedures.
Oh and there is of course some code which actually does something useful..