1

I have 20 entities and 50 user requests. And i have very complex business-logic (set of rules). How can i make my architecture so I'm not confused when adding new rules.

Now if i add property "InTheBattle" to fleet, i must consider this in 5 requestDispatchers and 3 methods in Verifications classes. What will happen when i will have 100 entities and 500 user requests?

Maybe i must use some pattern or tool like excel? How can i contain all rules in my head? :)

Entity = orm model class

Sample of one request dispatcher:

public class CreateShipRequestHandler : IRequestHandler
{
    private RepoContainer Repo;

    private int mFleetId;
    private int mShipDesignId;

    public void CreateShipHandler(RepoContainer repo, int fleetId, int shipDesignId)
    {
        this.Repo = repo;

        this.mFleetId = fleetId;
        this.mShipDesignId = shipDesignId;
    }

    public bool Dispatch(out string message)
    {
        //INTERESTING PLACE:

        message = string.Empty;

        var fleet = Repo.FleetRepo.GetById(mFleetId);
        var shipDesign = Repo.ShipDesignRepository.GetById(mShipDesignId);

        if(fleet == null || shipDesign == null)
        {
            message = "VerificationError";
            return false;
        }

        var userResearchedScience = Repo.UserResearchedScienceRepo.GetByUserAndScience(fleet.UserId, shipDesign.RequiredScienceId);

        //I am using ShipVerification on the client too (for activate buttons)
        if(!ShipVerification.IsCanCreateShip(fleet, userResearchedScience, out message))
        {
            return false;
        }

        var newShip = new Ship(mFleetId, mShipDesignId);
        Repo.ShipRepo.AddOrUpdate(newShip);

        return true;
    }
}

3 Answers 3

1

You are worried about the combinatorial explosion, that could make your design difficult to maintain.

Combinatorial explosion

It's difficult to maintain all the relationships and dependencies between all these entities. The mediator pattern can help to sort this out: Entities, Requests, perhaps even Rules are colleagues, and the mediator encapsulates the interactions.

Rule engine

You have many business rules, and rules evolve often. The best approach is to adopt an architecture implementing a rule engine.

There are different ways to implement this. One way could be to use a chain of responsibility . You can find some examples of implementation on wikipedia.

Another way to implement the rule engine could be to use an event processor. An event (e.g. new entity is created) is first inserted in an event queue. The rule engine fires all the rules relevant for this event, and removes the event. Each rule could fire other events that are inserted in the event loop. Once the event queue is empty, all rules and their consequences are processed. Of course, a rule engine requires a little bit more intelligence (for example identify if rules are cylcing, tigerring each other endlessly). But it's already a good start.

1

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.

1

Maybe i must use some pattern or tool like excel? How can i contain all rules in my head? :)

The core of this is a conceptual modelling problem; being able to capture rules (or at least rule meta-data) in a data structure such as a table can go a long way toward simplifying your solution.

The first step is to try to look at the individual areas of your conceptual model where complexity exists; for example

  • Processing updates at regular intervals, such as real time simulations of a model which is naturally complex in the real world (e.g. self-driving cars obeying the rules of the road)
  • Decisions made at the point of receiving inputs from an external source (e.g. predicting market trends based on large quantities of financial data).
  • Complex interactions between your entities (e.g. some A.I. controlled agents in a computer game)
  • Needing to process and interpret poorly-structured data such as natural language (e.g. personal assistant devices which accept human voice instructions)

Consider whether data structures are more appropriate than code for managing the complexity of your domain model. For example, natural language processors might use statistical language models or tree-like structures to decide on the relationships between words. On the other hand, transactions in financial markets used for spotting trends are often represented in relational structures. You may decide that you need a number of different structures to model different parts of your domain.

You need to make a judgement call how much of your conceptual model should be defined using data structures versus those which should be defined as algorithms. There aren't any hard rules about where to draw the line; although consider that complex data is often harder to debug than complex code.

The line between data and code is blurry; a data structure can be designed in such a way that it represents a whole series of branches and decisions which you might otherwise represent in code, but if doing so results in you spending a long time trying to debug that data then you haven't really gained anything.

Perhaps the best way to approach this is not to attempt to design a huge complex system up-front but to simply write something which works and solves a problem; when you're working with a complex set of requirements, the patterns in both your data and your code may not be entirely clear until you have started to write something which actually works; when the patterns begin to emerge it should be easier to see where to draw the line between code-vs-data.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.