If I understand your problem correctly (you need to manage a large number of rules whose logic depends on a large number of object properties), it might make sense to extract the logic into some alternate format (a 'rules file') and use a code generator to auto-generate the dispatch and verification code.
As far as alternate formats go, you mention Excel, and that is one possibility. Personally, I'd go for YAML, JSON or XML, though, I think. Basically, you just need to pick some text-based format that is both human-readable and easy to parse in a code generation script.
Regarding code generation, my personal preference for the last several years has been to use Ned Batchelder's Cog utility. It's a Python script that processes little snippets of Python code embedded inline within your sources. If that sounds bizarre, trust me, it's absolutely life-changing once you get comfortable with the approach.
The only example I have handy doesn't exactly match your use case, but here's a snippet of a 'cogged' C++ header (Datum.h
) that contains Python code generation bits inside a 'magic comment':
class Datum
{
public:
Datum(
v1_0::acme::SocketCanStub& stub,
std::string const& name,
uint8_t offset,
uint8_t length,
bool bigEndian
)
: stub_(stub)
, name_(name)
, offset_(offset)
, length_(length)
, bigEndian_(bigEndian)
{ }
virtual ~Datum() = default;
void Process(struct can_frame const& msg);
virtual void Update(uint64_t rawValue) = 0;
boost::any const& Value() const { return value_; }
protected:
v1_0::acme::SocketCanStub& stub_;
std::string name_;
uint8_t offset_; // <-- in bits
uint8_t length_; // <-- in bits
bool bigEndian_;
boost::any value_;
};
/* [[[cog
import socketcan.generator
signals = socketcan.generator.parse_can_defs()
for s in signals:
cog.outl('class {}_Datum : public Datum'.format(s.name))
cog.outl('{')
cog.outl(' public:')
cog.outl(' {}_Datum('.format(s.name))
cog.outl(' v1_0::acme::SocketCanStub& stub,')
cog.outl(' std::string const& name,')
cog.outl(' uint8_t offset,')
cog.outl(' uint8_t length,')
cog.outl(' uint8_t bigEndian,')
cog.outl(' uint64_t initval')
cog.outl(' )')
cog.outl(' : Datum(stub, name, offset, length, bigEndian)')
cog.outl(' {')
cog.outl(' this->Update(initval);')
cog.outl(' }')
cog.outl()
cog.outl(' void Update(uint64_t rawValue) override;')
cog.outl('};')
cog.outl()
]]] */
When you run cog.py
on this file using:
$ cog.py -r Datum.h
the code in the magic comment is run, and its output is inserted inline into the file, right after the comment. The output looks something like this:
/* [[[cog
import socketcan.generator
signals = socketcan.generator.parse_can_defs()
for s in signals:
cog.outl('class {}_Datum : public Datum'.format(s.name))
cog.outl('{')
cog.outl(' public:')
cog.outl(' {}_Datum('.format(s.name))
cog.outl(' v1_0::acme::SocketCanStub& stub,')
cog.outl(' std::string const& name,')
cog.outl(' uint8_t offset,')
cog.outl(' uint8_t length,')
cog.outl(' uint8_t bigEndian,')
cog.outl(' uint64_t initval')
cog.outl(' )')
cog.outl(' : Datum(stub, name, offset, length, bigEndian)')
cog.outl(' {')
cog.outl(' this->Update(initval);')
cog.outl(' }')
cog.outl()
cog.outl(' void Update(uint64_t rawValue) override;')
cog.outl('};')
cog.outl()
]]] */
class VEH_SPEED_Datum : public Datum
{
public:
VEH_SPEED_Datum(
v1_0::acme::SocketCanStub& stub,
std::string const& name,
uint8_t offset,
uint8_t length,
uint8_t bigEndian,
uint64_t initval
)
: Datum(stub, name, offset, length, bigEndian)
{
this->Update(initval);
}
void Update(uint64_t rawValue) override;
};
class PRND_STAT_Datum : public Datum
{
public:
PRND_STAT_Datum(
v1_0::acme::SocketCanStub& stub,
std::string const& name,
uint8_t offset,
uint8_t length,
uint8_t bigEndian,
uint64_t initval
)
: Datum(stub, name, offset, length, bigEndian)
{
this->Update(initval);
}
void Update(uint64_t rawValue) override;
};
class TurnIndLvr_Stat_Datum : public Datum
{
public:
TurnIndLvr_Stat_Datum(
v1_0::acme::SocketCanStub& stub,
std::string const& name,
uint8_t offset,
uint8_t length,
uint8_t bigEndian,
uint64_t initval
)
: Datum(stub, name, offset, length, bigEndian)
{
this->Update(initval);
}
void Update(uint64_t rawValue) override;
};
// [[[end]]]
The goal is to generate a small Datum
subclass for each 'interesting' CAN signal defined in a large-ish (1.4 MB) kcd file. It's a little hard to explain, but.. here's the complete socketcan/generator.py
file referenced in the Cog comment:
#!/usr/bin/env python
import bz2
from collections import namedtuple
SIGNALS = {
'TurnIndLvr_Stat': (
'v1_0::acme::SocketCan::TurnSignalState', # <-- Value type
'getTurnSignalState', # <-- Name of getter method
'0' # <-- Initial (raw) value
),
'VEH_SPEED': (
'float',
'getVehicleSpeed',
'0'
),
'PRND_STAT': (
'v1_0::acme::SocketCan::PrndlState',
'getPrndlState',
'0'
),
}
Signal = namedtuple(
'Signal',
'name id length offset endianess type getter initval'
)
def parse_can_defs():
import os.path
from lxml import etree
kcdfile = os.path.join(os.path.dirname(__file__), 'can-db.kcd.bz2')
with bz2.BZ2File(kcdfile) as f:
tree = etree.parse(f)
signals = []
ns = dict(kcd="http://kayak.2codeornot2code.org/1.0")
for name in SIGNALS.keys():
elem = tree.xpath("//kcd:Signal[@name='{}']".format(name), namespaces=ns)[0]
s = Signal(
name,
int(elem.getparent().attrib['id'], 16),
elem.attrib['length'],
elem.attrib['offset'],
elem.attrib.get('endianess', 'little'),
SIGNALS[name][0],
SIGNALS[name][1],
SIGNALS[name][2],
)
signals.append(s)
return signals
if __name__ == '__main__':
import sys
sys.exit(main())
The 'alternate syntax' in this case is the pure Python data structure SIGNALS
, which defines the subset of CAN signals from the kcd
file that the application cares about. The script parses the kcd
file and loops over the signal names, populating the id
, length
and offset
fields for each one. The inlined code generation script in Datum.h
uses the returned data to write the code. If my boss informs me that another CAN signal has suddenly become 'interesting', I just add another entry to the SIGNALS
dictionary in socketcan/generator.py
and re-run cog.py
.
There may be more idiomatic ways to do this in C# (I mostly code in C++ and Python), but the basic idea of splitting the logic out to a separate file that's easier to work with, and generating code from that alternate representation, may be helpful. My example is pretty complex, and I had to elide some important details, but the overall approach is fundamentally simple: find a way to turn the error-prone, manual coding of tricky logic into an automated, data-driven process.