Reducing cyclomatic complexity in your Objective C code

Contenido del Bootcamp Dirigido por: | Última modificación: 13 de marzo de 2024 | Tiempo de Lectura: 3 minutos

Algunos de nuestros reconocimientos:

Premios KeepCoding

Cyclomatic complexity

The need for control structures is greatly diminished in Object Oriented languages if compared to other languages. Usually polymorphism provides a better and more scalable alternative to complex and nested conditionals. As a matter of fact, Smalltalk doesn’t even have a switch statement or method. In Objective C we have mostly two techniques to reduce control structures and branching:
  • Sending messages to nil
  • Polymorphism
  • Abusing NSDictionary with blocks

What’s so bad about conditionals?

Before we even begin reling off our list of alternatives, let’s pause to think why should conditionals be avoided in the first place?
The use and abuse of control structures adds to the complexity of your code, specifically cyclomatic complexity. The more branches your code has, the more complex it will be, and the more difficult to understand and maintain.
The more moveable parts a machine has, the greater the chances are it will break.
 Before we introduce any techniques, lets take a look at a particular example:
[crayon lang="Objective C" ]
if ([self.card isValid]) {
if ([self.card.owner isValid]) {
if ([self.card.owner hasCreditFor: 300]) {
// vend
// debit
}else{
// notify
}
}else{
[self eatCard: self.card];
}
}else{
[self eatCard: self.card];
}
[/crayon]
Easy to read? How easy is it to add new cases?

Using nil to remove conditionals

In Cocoa, nil is the universal Null Object Pattern and should be used to reduce unnecessary checks. For instance in our example, the first few conditionals could be reduced to
[crayon lang="Objective C"]
if ([self.card.owner hasCreditFor: 300]) {
//vend
//debit
}else{
[self eatCard:self.card];
}
[/crayon]
as long as card and owner are set to nil if invalid (for whatever reason). Using nil as a Null Object can greatly reduce the amount of code you write and the complexity of your code, but can also mask errors as normal program execution.

Using polymorphism to remove the need for conditionals

Whenever you use a switch or nested ifs to choose behavior depending on the type of object received, the gods of Object Orientation kill a puppy. Suppose  you have a class hierarchy of shapes (Rectangle, Circle, Triangle, etc…) that are painted on a UIView subclass called Canvas. A common but essentially wrong implementation is to have all the painting code on a Canvas method called paintShape:
[crayon lang="Objective C"]
-(void) paintShape: (Shape *) aShape{
if ([[aShape class] isEqual: [Rectangle class]]) {
// draw a rect
}else if([[aShape class] isEqual: [Circle class]]) {
// draw a rect
} // etc...

}

[/crayon]
This solution not only relies on a complex conditional, but also turns the Shape classes into nothing more than structures without any behavior and intelligence. All the intelligence, and complexity, of the system is concentrated in the Canvas class, turning it into a God Class (as described by Arthur Riel in Object oriented design heuristics). This code is based on a real case I saw on my Cocoa Touch course. A better solution is to have the painting code in each Shape subclass. Each Shape knows how to paint itself, and only needs the Canvas instance as a parameter. All the code in paintShape: can be reduced to a double dispatch call to paintOn:
[crayon lang="Objective C"]
-(void) paintShape: (Shape *) aShape{
[aShape paintOn:self];
}

[/crayon]
Whenever Canvas receives the paintShape: message, it replies back to the sender with a paintOn: message. This results in the appropriate paintOn: being called without having to explicitly check the class of the sender.
The bottom line is: if you are using a switch, you’re probably doing something wrong. If you are using a switch to check the class of some object, you are doing something wrong.

 Switching on a NSString

Sometimes resorting to polymorphism is overkill and a switch is an acceptable solution. However, a common need is to  switch on strings, not integers. Unfortunately the C case statement won’t accept NSStrings (or any object). A reasonable solution is to use NSDictionary. All objects stored in NSDictionary are placed based on the hash, and can be looked up very efficiently, possibly faster than a bunch of nested ifs using isEqualTo:. This method shows how to use NSDictionary to replace nested if statements.
[crayon lang="Objective C"]
-(void) dispatchOn: (NSString *) aString{
NSDictionary *cases = [NSDictionary dictionaryWithObjectsAndKeys:
[FooCase class], @"foo",
[BarCase class], @"bar",
nil];
[[[[[cases objectForKey:aString] alloc] init] autorelease] process: aString];
}
[/crayon]
We start by populating the NSDictionary (ideally it should be a static variable to avoid recreating it) with pairs of strings and classes. The appropriate class is then used to create an instance of the object and the object is sent the process: method. A better solution would be to create a new CaseHandler class wich contains a NSMutableDictionary. This class can then add (even at runtime) new cases. Each case would be a block to be processed and a key object to identify the case. Ideally, the class should also handle «default» cases (when the key doesn’t match any existing keys). I might implement it and upload it to GitHub.

More Information on reducing branching

See the excellent «» article and «Thinking Forth» from Leo Brodie. The latter is a magnificent read on software design even if your don’t like or care for Forth. It’s amazing to read about «refactoring» in the early 80s. Don’t miss out on any of our posts! Subscribe here!

Posts más leídos