According to www.lizard.ws the original's function cyclomatic complexity is 29 and for the second version is 22. Both numbers are usually considered high, and teams aim for much lower values (debatable what the good range is though and will see within this answer why).
In order to reduce it, one way is to encapsulate the if
statements, among with removing the code duplication and separate the responsibilities.
The next
calls seem duplicate, so let's reduce them first.
providerSchema.pre('save', function (next) {
var valid = true;
if (this.level === 4) {
if (this.description.length >= 80 || this.certifications.length > 5) {
valid = false;
}
} else if (this.level === 3) {
if (this.description.length >= 50 || this.certifications.length > 3) {
valid = false;
}
} else if (this.level === 2) {
if (this.description.length >= 30 || this.certifications.length > 0 || this.teaser || this.social.length > 0) {
valid = false;
}
} else if (this.level === 1) {
if (this.description || this.certifications.length > 0 || this.teaser || this.social.length > 0 || this.locationLat || this.locationLong || this.workingHourEnd || this.workingHourStart) {
valid = false;
}
} else {
if (this.description || this.certifications.length > 0 || this.teaser || this.social.length > 0 || this.locationLat || this.locationLong || this.workingHourEnd || this.workingHourStart) {
valid = false;
}
}
if (valid) {
next();
} else {
next(new Error('your current plan does not have this feature'));
}
});
With this first refactoring we didn't gain much in terms of lowering CC, in fact it increased to 30, because of the added if
statement. However this can let us to split the validation responsibility from enabling the actual feature (as @Margon mentioned).
providerSchema.pre('save', function (next) {
if (isValidFeatureRequest()) {
next();
} else {
next(new Error('your current plan does not have this feature'));
}
function isValidFeatureRequest() {
if (this.level === 4) {
if (this.description.length >= 80 || this.certifications.length > 5) {
return false;
}
} else if (this.level === 3) {
if (this.description.length >= 50 || this.certifications.length > 3) {
return false;
}
} else if (this.level === 2) {
if (this.description.length >= 30 || this.certifications.length > 0 || this.teaser || this.social.length > 0) {
return false;
}
} else if (this.level === 1) {
if (this.description || this.certifications.length > 0 || this.teaser || this.social.length > 0 || this.locationLat || this.locationLong || this.workingHourEnd || this.workingHourStart) {
return false;
}
} else {
if (this.description || this.certifications.length > 0 || this.teaser || this.social.length > 0 || this.locationLat || this.locationLong || this.workingHourEnd || this.workingHourStart) {
return false;
}
}
return true;
}
});
The isValidFeatureRequest
function is at 29 and providerSchema
is at 2. We still need to work on.
Checking again the code duplication, I noticed the last two blocks have the the same checks for other levels than 2, 3 or 4, so let's merge them.
providerSchema.pre('save', function (next) {
if (isValidFeatureRequest()) {
next();
} else {
next(new Error('your current plan does not have this feature'));
}
function isValidFeatureRequest() {
if (this.level === 4) {
if (this.description.length >= 80 || this.certifications.length > 5) {
return false;
}
} else if (this.level === 3) {
if (this.description.length >= 50 || this.certifications.length > 3) {
return false;
}
} else if (this.level === 2) {
if (this.description.length >= 30 || this.certifications.length > 0 || this.teaser || this.social.length > 0) {
return false;
}
} else {
if (this.description || this.certifications.length > 0 || this.teaser || this.social.length > 0 || this.locationLat || this.locationLong || this.workingHourEnd || this.workingHourStart) {
return false;
}
}
return true;
}
});
We gained the following figures
function | CC
-------------------------------------------
providerSchema | 2
isValidFeatureRequest | 20
The CC for isValidFeatureRequest
is now at 20, which is an improvement.
The check for description
and certifications
also seems to vary, so I can encapsulate it into a function.
providerSchema.pre('save', function (next) {
if (isValidFeatureRequest()) {
next();
} else {
next(new Error('your current plan does not have this feature'));
}
function isValidFeatureRequest() {
if (this.level === 4) {
if (descriptionOrCertificationsOffLimits(80, 5)) {
return false;
}
} else if (this.level === 3) {
if (descriptionOrCertificationsOffLimits(40, 3)) {
return false;
}
} else if (this.level === 2) {
if (descriptionOrCertificationsOffLimits(30, 0) || this.teaser || this.social.length > 0) {
return false;
}
} else {
if (this.description || this.certifications.length > 0 || this.teaser || this.social.length > 0 || this.locationLat || this.locationLong || this.workingHourEnd || this.workingHourStart) {
return false;
}
}
return true;
}
function descriptionOrCertificationsOffLimits(descriptionLimit, certificationsLimit) {
return this.description.length >= descriptionLimit || this.certifications.length > certificationsLimit;
}
});
function | CC
-------------------------------------------
providerSchema | 2
isValidFeatureRequest | 17
descriptionOrCertificationsOffLimits | 2
CC is now at 17, slightly better.
There is lot to check in the last branch, so let's extract it into his own function.
providerSchema.pre('save', function (next) {
if (isValidFeatureRequest()) {
next();
} else {
next(new Error('your current plan does not have this feature'));
}
function isValidFeatureRequest() {
if (this.level === 4) {
if (descriptionOrCertificationsOffLimits(80, 5)) {
return false;
}
} else if (this.level === 3) {
if (descriptionOrCertificationsOffLimits(40, 3)) {
return false;
}
} else if (this.level === 2) {
if (descriptionOrCertificationsOffLimits(30, 0) || this.teaser || this.social.length > 0) {
return false;
}
} else if (hasAny()) {
return false;
}
return true;
}
function descriptionOrCertificationsOffLimits(descriptionLimit, certificationsLimit) {
return this.description.length >= descriptionLimit || this.certifications.length > certificationsLimit;
}
function hasAny() {
return this.description || this.certifications.length > 0 || this.teaser || this.social.length > 0 || this.locationLat || this.locationLong || this.workingHourEnd || this.workingHourStart;
}
});
Which results into
function | CC
-------------------------------------------
providerSchema | 2
isValidFeatureRequest | 10
descriptionOrCertificationsOffLimits | 2
hasAny | 8
We have now 4 functions with manageable complexities.
The hasAny
function seems to have a large CC, compared to what it does. What we can do here is to improve its readability, by displaying one condition per line. This is also the moment when I think we can't do anything about this function, and is the time not to look at an arbitrary CC limit in order to squize the code just to pass the analyzer.
function hasAny() {
return this.description ||
this.certifications.length > 0 ||
this.teaser ||
this.social.length > 0 ||
this.locationLat ||
this.locationLong ||
this.workingHourEnd ||
this.workingHourStart;
}
Let's extract more, to check if it has a teaser or social data
providerSchema.pre('save', function (next) {
if (isValidFeatureRequest()) {
next();
} else {
next(new Error('your current plan does not have this feature'));
}
function isValidFeatureRequest() {
if (this.level === 4) {
if (descriptionOrCertificationsOffLimits(80, 5)) {
return false;
}
} else if (this.level === 3) {
if (descriptionOrCertificationsOffLimits(40, 3)) {
return false;
}
} else if (this.level === 2) {
if (descriptionOrCertificationsOffLimits(30, 0) || hasTeaserOrSocial()) {
return false;
}
} else if (hasAny()) {
return false;
}
return true;
}
function descriptionOrCertificationsOffLimits(descriptionLimit, certificationsLimit) {
return this.description.length >= descriptionLimit || this.certifications.length > certificationsLimit;
}
function hasTeaserOrSocial() {
return this.teaser || this.social.length > 0;
}
function hasAny() {
return this.description ||
this.certifications.length > 0 ||
this.teaser ||
this.social.length > 0 ||
this.locationLat ||
this.locationLong ||
this.workingHourEnd ||
this.workingHourStart;
}
});
Which results into
function | CC
-------------------------------------------
providerSchema | 2
isValidFeatureRequest | 9
descriptionOrCertificationsOffLimits | 2
hasTeaserOrSocial | 2
hasAny | 8
The if
followed by an inner if
can be combined into and and
operation so we can have this
function isValidFeatureRequest() {
if (this.level === 4 && descriptionOrCertificationsOffLimits(80, 5)) {
return false;
} else if (this.level === 3 && descriptionOrCertificationsOffLimits(40, 3)) {
return false;
} else if (this.level === 2 && descriptionOrCertificationsOffLimits(30, 0) || hasTeaserOrSocial()) {
return false;
} else if ((this.level === 1 || this.level === 0) && hasAny()) {
return false;
}
return true;
}
The CC doesn't change, but it enables me to extract validation for every level, so I gain smaller functions, with smaller complexity.
Edit to fix a bug here - This step introduced a bug, as @Roland Illig mentioned in the comments (the story of my life when I refactor even a simple if
).
After fixing it the CC actually increased with 2, to 11, as I introduced two new checks and I also had to add a new function. end of edit
function isValidFeatureRequest() {
if (isLevel4AndNotValid()) {
return false;
} else if (isLevel3AndNotValid()) {
return false;
} else if (isLevel2AndNotValid()) {
return false;
} else if (isBellowLevel2AndNotValid()) {
return false;
}
return true;
}
function isLevel4AndNotValid() {
return this.level === 4 && descriptionOrCertificationsOffLimits(80, 5);
}
function isLevel3AndNotValid() {
return this.level === 3 && descriptionOrCertificationsOffLimits(40, 3);
}
function isLevel2AndNotValid() {
return this.level === 2 && (descriptionOrCertificationsOffLimits(30, 0) || hasTeaserOrSocial());
}
function isBellowLevel2AndNotValid() {
return (this.level === 1 || this.level === 0) && hasAny();
}
Which are
function | CC
-------------------------------------------
providerSchema | 2
isValidFeatureRequest | 5
isLevel4AndNotValid | 2
isLevel3AndNotValid | 2
isLevel2AndNotValid | 3
isBellowLevel2AndNotValid | 3
descriptionOrCertificationsOffLimits | 2
hasTeaserOrSocial | 2
hasAny | 8
The isValidFeatureRequest
still looks odd, I can remove the else statements and I can convert the last call into a return statement, which decrease the complexity with one point.
function isValidFeatureRequest() {
if (isLevel4AndNotValid()) {
return false;
}
if (isLevel3AndNotValid()) {
return false;
}
if (isLevel2AndNotValid()) {
return false;
}
if (isBellowLevel2AndNotValid()) {
return false;
}
return true;
}
My final attempt is this:
providerSchema.pre('save', function (next) {
if (isValidFeatureRequest()) {
next();
} else {
next(new Error('your current plan does not have this feature'));
}
function isValidFeatureRequest() {
if (isLevel4AndNotValid()) {
return false;
}
if (isLevel3AndNotValid()) {
return false;
}
if (isLevel2AndNotValid()) {
return false;
}
if (isBellowLevel2AndNotValid()) {
return false;
}
return true;
}
function isLevel4AndNotValid() {
return this.level === 4 && descriptionOrCertificationsOffLimits(80, 5);
}
function isLevel3AndNotValid() {
return this.level === 3 && descriptionOrCertificationsOffLimits(40, 3);
}
function isLevel2AndNotValid() {
this.level === 2 && (descriptionOrCertificationsOffLimits(30, 0) || hasTeaserOrSocial());
}
function isBellowLevel2AndNotValid() {
return (this.level === 1 || this.level === 0) && hasAny();
}
function descriptionOrCertificationsOffLimits(descriptionLimit, certificationsLimit) {
return this.description.length >= descriptionLimit || this.certifications.length > certificationsLimit;
}
function hasTeaserOrSocial() {
return this.teaser || this.social.length > 0;
}
function hasAny() {
return this.description ||
this.certifications.length > 0 ||
this.teaser ||
this.social.length > 0 ||
this.locationLat ||
this.locationLong ||
this.workingHourEnd ||
this.workingHourStart;
}
});
With the following resuts:
function | CC
-------------------------------------------
providerSchema | 2
isValidFeatureRequest | 5
isLevel4AndNotValid | 2
isLevel3AndNotValid | 2
isLevel2AndNotValid | 3
isBellowLvel2AndNotValid | 3
descriptionOrCertificationsOffLimits | 2
hasTeaserOrSocial | 2
hasAny | 8