0

I've got a method called UpdateUserDevices (UserModel). The UserModel contains a List<DeviceModel> which associates a list of devices with a specific user. (One-to-many).

When I call the method, everything is working as excpected, however it's quite complex with nested loops and if statements.

What would be a good pattern to reduce the cyclomatic complexity on this? I thought about "CoR" but that might be overkill.

private void UpdateUserDevices( UserModel model )
{
    // Get users current devices from database
    var currentDevicesFromDatabase = _deviceRepository.FindByUserId( model.Id ).ToList();

    // if both the model devices and the datbase devices have records
    // compare them and run creates, deletes, and updates
    if( model.Devices.Any() && currentDevicesFromDatabase.Any() )
    {
        var devicesToAdd = model.Devices.Exclude( currentDevicesFromDatabase, d => d.Id ).ToList();
        var devicesToDelete = currentDevicesFromDatabase.Exclude( model.Devices, d => d.Id ).ToList();
        var workingDevices = model.Devices.Union( currentDevicesFromDatabase );

        foreach( var device in workingDevices )
        {
            // Add devices
            if( devicesToAdd.Contains( device ) )
            {
                _deviceRepository.Create( device );
                continue;
            }

            // delete devices
            if( devicesToDelete.Contains( device ) )
            {
                _deviceRepository.Delete( device );
                continue;
            }

            // update the rest
            _deviceRepository.Update( device );
        }
        return;
    }

    // model.Devices doesn't have any records in it.
    // delete all records from the database
    if( !model.Devices.Any() )
    {
        foreach( var device in currentDevicesFromDatabase )
        {
            _deviceRepository.Delete( device );
        }
    }

    // database doesn't have any records in it
    // create all new records
    if( !currentDevicesFromDatabase.Any() )
    {
        foreach( var device in currentDevicesFromDatabase )
        {
            _deviceRepository.Create( device );
        }
    }
}
Chase Florell
  • 46,378
  • 57
  • 186
  • 376
  • You don't need `continue`. – Sam Leach Oct 14 '13 at 20:18
  • rather than using `if(...)...continue;` why not just use `if(){}else if(){} else{}`? – Servy Oct 14 '13 at 20:18
  • continue helps the code move along without evaluation more else if's. I'm more-so looking for a solid pattern to deal with one-to-many – Chase Florell Oct 14 '13 at 20:20
  • @ChaseFlorell you don't have any else ifs. – Sam Leach Oct 14 '13 at 20:21
  • ok, so let's say I remove the continues and create `if {} else if {} else {}`... the complexity is still quite high with three different loops and three if statements. – Chase Florell Oct 14 '13 at 20:25
  • Well you tagged SOLID principles, so why not just try to apply them? Start with SRP, your method clearly have way too much responsibility and at different levels of abstraction. Reorganize it so each method do one and only one thing (at the correct level of abstraction) and you'll end up with a bunch of simple methods instead of a big complex one. That would be a good start. – Pierre-Luc Pineault Oct 14 '13 at 20:53

3 Answers3

1

It might be that I don't understand exactly what happens, but it seems to me like you could simplify it a lot by removing all your outermost if statements and just performing the topmost codeblock.

private void UpdateUserDevices ( UserModel model )
{
    // Get users current devices from database
    var currentDevicesFromDatabase = _deviceRepository.FindByUserId( model.Id );

    var devicesToAdd = model.Devices.Exclude( currentDevicesFromDatabase, d => d.Id ).ToList();
    var devicesToDelete = currentDevicesFromDatabase.Exclude( model.Devices, d => d.Id ).ToList();
    var workingDevices = model.Devices.Union( currentDevicesFromDatabase ).ToList();

    foreach ( var device in workingDevices )
    {
        if ( devicesToAdd.Contains( device ) )
        {
            // Add devices
            _deviceRepository.Create( device );

        }
        else if ( devicesToDelete.Contains( device ) )
        {
            // Delete devices
            _deviceRepository.Delete( device );

        }
        else
        {
            // Update the rest
            _deviceRepository.Update( device );
        }
    }

}

Actually the foreach could be split into three separate with no nested ifs.

private void UpdateUserDevices ( UserModel model )
{

    var currentDevicesFromDatabase = _deviceRepository.FindByUserId( model.Id );

    // Take the current model and remove all items from the database... This leaves us with only records that need to be added.
    var devicesToAdd = model.Devices.Exclude( currentDevicesFromDatabase, d => d.Id ).ToList();

    // Take the database and remove all items from the model... this leaves us with only records that need to be deleted
    var devicesToDelete = currentDevicesFromDatabase.Exclude( model.Devices, d => d.Id ).ToList();

    // Take the current model and remove all of the items that needed to be added... this leaves us with only updateable recoreds
    var devicesToUpdate = model.Devices.Exclude(devicesToAdd, d => d.Id).ToList();

    foreach ( var device in devicesToAdd )
        _deviceRepository.Create( device );

    foreach ( var device in devicesToDelete )
        _deviceRepository.Delete( device );

    foreach ( var device in devicesToUpdate )
        _deviceRepository.Update( device );

}
Chase Florell
  • 46,378
  • 57
  • 186
  • 376
rasmusgreve
  • 131
  • 1
  • 4
  • the problem there is the `union` call.. if either list is `null`, it blows up.... `var workingDevices = model.Devices.Union( currentDevicesFromDatabase );` – Chase Florell Oct 14 '13 at 20:30
  • But isn't it safe to assume that neither is null since the original code doesn't check for null either and would blow up just as much? – rasmusgreve Oct 14 '13 at 20:35
  • that's what this line is doing... `if( model.Devices.Any() && currentDevicesFromDatabase.Any() )` – Chase Florell Oct 14 '13 at 20:40
  • No, that only checks if they both aren't empty? – rasmusgreve Oct 14 '13 at 20:41
  • it appears as though you're on to something. When I first wrote the code, I didn't have any `ToList()` calls. Now that they're in there it appears as though the `Any` check is redundant.. I've removed the lower if's and the Any checks, and the tests are still passing. – Chase Florell Oct 14 '13 at 21:02
  • Great :) Hope it helps! You might be able to remove the if's and instead of 1 foreach have 3. One foreach devicesToAdd, one foreach devicesToDelete and then instead of workingDevices where you union the two sets together you use Intersect to know which ones to update. Of course this requires an implementation of Equals that gives the desired result. – rasmusgreve Oct 14 '13 at 22:05
  • one quick question about this. in your `devicesToUpdate`, isn't that just a reference comparison? I think we'd rather do a value comparison on the `Id` field, no? – Chase Florell Oct 15 '13 at 14:57
0

The continue statemant is the cause of the high cyclomatic complexity

Replace

if( devicesToAdd.Contains( device ) )
{
   _deviceRepository.Create( device );
   continue;
}

// delete devices
if( devicesToDelete.Contains( device ) )
{
   _deviceRepository.Delete( device );
  continue;
}

with something similar to

 if( devicesToAdd.Contains( device ) ) {
    _deviceRepository.Create( device );
 } else if( devicesToDelete.Contains( device ) ) {
      // delete devices
    _deviceRepository.Delete( device );
 }

and then you also could remove the continues, which are a bit of a code smell.

With else-if there are less combinations possible (less paths) to go through your method, at least from the view of the cyclomatic complexity analyser sw.

A simpler example:

if (debugLevel.equals("1") {
    debugDetail = 1;
}
if (debugLevel.equals("2") {
    debugDetail = 2;
}

This code has 4 paths, each addional if multiplies the complexity by 2. From human intelligence this example looks simple.

For the complexity analyser this is better:

if (debugLevel.equals("1") {
    debugDetail = 1;
} else if (debugLevel.equals("2") {
    debugDetail = 2;
}

This now has only 2 possible paths! You, using human intelligence see, that both conditions are mutually exclusive, but the complexity analyser sees that only in the second example using the else-if clause.

AlexWien
  • 28,470
  • 6
  • 53
  • 83
  • 1) your example also includes `continue`, 2) how does a `continue` increase complexity? I'd rather exit early than evaluate more useless if's. – Chase Florell Oct 14 '13 at 20:23
  • @ChaseFlorell removed continues Once you have updated your code please tell if that lowerded the cyclomatic complexity. I expect yes. With else-if there are less combinations possible (less paths) to go through your method, at least from the bview of the cyclom. analyser sw. – AlexWien Oct 14 '13 at 20:27
0

What I would do involves a breakdown of the method. You are trying to do a few things so we can separate them out. Moved loop conditionals into variables (readability). Checked Edge case, of either empty, first. Moved the delete to its own method (readability/maintainability). Moved main (complex logic) to its own method (readability/maintainability). The UpdateUserDevices now looks clean.

Main Method:

private void UpdateUserDevices( UserModel model )
{
    // Get users current devices from database
    var currentDevicesFromDatabase = _deviceRepository.FindByUserId( model.Id ).ToList();

    $isUserDevicesEmpty = !model.Devices.Any();
    $isRepositoryEmpty = !currentDevicesFromDatabase.Any();

    if($isRepositoryEmpty || $isUserEmpty){
        // Missing One
        if($isRepositoryEmpty){
            this.deleteEmpty(currentDevicesFromDatabase);
        }

        if($isUserDevicesEmpty){
            this.deleteEmpty(model.Devices);
        }
    }
    else{
        this.mergeRepositories(currentDevicesFromDatabase, model);
    }

    return;
}

Merge Repos Method: The meat and potatoes of the original method now has its own method. Things that happened here. Removed workingDevice to add in devicesToUpdate ( Direct vs Indirect logic => taking the union then doing contains for all elements is the same as doing intersect once). Now we can have 3 separate foreach loops to process the changes. ( You are avoiding doing the contains for every device, maybe other efficiency gains).

private void mergeRepositories(UserModel model, List currentDevicesFromDatabase)
{
    // if both the model devices and the datbase devices have records
    // compare them and run creates, deletes, and updates

    var devicesToAdd = model.Devices.Exclude( currentDevicesFromDatabase, d => d.Id ).ToList();
    var devicesToDelete = currentDevicesFromDatabase.Exclude( model.Devices, d => d.Id ).ToList();
    var devicesToUpdate = model.Devices.Intersect( currentDevicesFromDatabase, d => d.Id ).ToList();

    foreach( device in devicesToAdd ){
        _deviceRepository.Create( device );
    }

    foreach( device in devicesToDelete ){
        _deviceRepository.Delete( device );
    }

    foreach( device in devicesToUpdate){
        _deviceRepository.Update( device );
    }

    return;
}

Delete Empty method: Nothing to see here

private void deleteEmpty(List devices){
    foreach( var device in devices )
    {
        _deviceRepository.Delete( device );
    }

    return
}

Now its nice and simple. Or so I think anyway. ( I had to do something similar for listing Items on Ebay. What I actually wrote was a slightly different version without using Intersect on the whole set, but that's neither here nor there )

SH-
  • 1,642
  • 10
  • 13