Вопрос

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 );
        }
    }
}
Это было полезно?

Решение

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 );

}

Другие советы

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.

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 )

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top