Clean Code

How to identify a Tradition Breaker using NDepend

In the previous articles in this series we’ve seen:

In this article we’ll see how to identify the Tradition Breaker code smell.

Tradition Breaker Detection Strategy

A class suffers from Tradition Breaker when it doesn’t use the protected members of its parent. Object-Oriented Metrics in Practice, by Michele Lanza and Radu Marinescu, proposes the following detection strategy for Tradition Breaker:

((NAS >= Average NOM per class) AND (PNAS >= Two Thirds)) AND
(((AMW > Average) OR (WMC >= Very High)) AND (NOM >= High)) AND
((Parent’s AMW > Average) AND (Parent’s NOM > High/2) AND (Parent’s WMC >= Very High/2))

This might seem complex on a first look. After we go over the definition for each metric, we’ll break this detection strategy in three distinct parts. This way we’ll see why the authors picked these conditions and it will make more sense.

This detection strategy uses five metrics:

  • NAS – Number of Added Services – to measure the number of methods that are not overridden from a class’s ancestors
  • PNAS – Percentage of Newly Added Services – to measure the relative number of newly added services compared to all services
  • AMW – Average Method Weight – to measure the average complexity of all methods of a class
  • WMC – Weighted Method Count – to measure class complexity
  • NOM – Number of Methods

This detection strategy uses three types of thresholds:

  • PNAS uses a Common Fraction Threshold. Two Thirds is 0.66.
  • NAS, AMW, WMC and NOM use Statistics-Based Thresholds. For these types of thresholds, a large number of projects needs to be analyzed. The authors of Object-Oriented Metrics in Practice analyzed 45 Java projects and extracted Low, Average, High and Very High thresholds for some basic metrics. The Average threshold for AMW is 2. The Very High threshold for WMC is 47. The Average threshold for NOM is 7 and the High threshold is 10.

Metrics Definitions

Let’s go over the definitions for the used metrics and how to implement them with NDepend. For a more detailed definition, be sure to check Appendix A.2 of Object-Oriented Metrics in Practice. If you’re not familiar with CQLinq, check out the NDepend documentation or my blog post on how to query your code base.

NAS – Number of Added Services

This metric measures the number of methods that are not overridden from a class’s ancestors.

// <Name>NAS</Name>
let isPublicNonStaticMethod = new Func<IMethod, bool>(m =>
 m.IsPublic &&
 !m.IsClassConstructor && 
 !m.IsConstructor && 
 !m.IsStatic)

let newlyAddedMethodsFor = new Func<IType, IEnumerable<IMethod>>(c =>
 c.Methods.Where(m => 
  isPublicNonStaticMethod(m) &&
  m.OverriddensBase.Count() == 0)) 

from c in JustMyCode.Types
let newlyAddedMethods = newlyAddedMethodsFor(c)
let nas = newlyAddedMethods.Count()
orderby nas descending 
select new { c, nas, newlyAddedMethods }

PNAS – Percentage of Newly Added Services

This metric measures the relative number of newly added services compared to all services. It’s the number of public methods that are not overridden from the ancestors, divided by the total number of public methods.

// <Name>PNAS</Name>
let isPublicNonStaticMethod = new Func<IMethod, bool>(m =>
 m.IsPublic &&
 !m.IsClassConstructor && 
 !m.IsConstructor && 
 !m.IsStatic)

let publicMethodsFor = new Func<IType, IEnumerable<IMethod>>(c =>
 c.Methods.Where(m => isPublicNonStaticMethod(m))) 

let newlyAddedMethodsFor = new Func<IType, IEnumerable<IMethod>>(c =>
 c.Methods.Where(m => 
  isPublicNonStaticMethod(m) &&
  m.OverriddensBase.Count() == 0)) 

from c in JustMyCode.Types
let newlyAddedMethods = newlyAddedMethodsFor(c)
let publicMethodsCount = publicMethodsFor(c).Count()
let pnas = publicMethodsCount == 0 ? 
 null : 
 (double?) newlyAddedMethods.Count()/publicMethodsCount
orderby pnas
select new { c, pnas, newlyAddedMethods }

AMW – Average Method Weight

This metrics measures the average complexity of all methods of a class. McCabe’s cyclomatic number is used to quantify a method’s complexity.

// <Name>AMW</Name>
let amwFor = new Func<IType, double?>(c =>
 (double?) c.CyclomaticComplexity / c.NbMethods)

from c in JustMyCode.Types
let amw = amwFor(c)
orderby amw descending
select new { c, amw }

WMC – Weighted Method Count

This metric measures the complexity of a class. This is done by summing the complexity of all methods of a class. McCabe’s Cyclomatic Complexity is used to measure the complexity of a method.

// <Name>WMC</Name>
let wmcFor = new Func<IType, int>(t => 
 t.MethodsAndContructors
  .Select(m => (int) m.CyclomaticComplexity.GetValueOrDefault())
  .Sum())

// ** Sample Usage **
from t in JustMyCode.Types
let wmc = wmcFor(t)
orderby wmc descending 
select new { t, wmc }

NOM – Number of Methods

This metric counts the number of methods in a class. This metric is computed out of the box by NDepend:

// <Name>NOM</Name>
from c in JustMyCode.Types
let nom = c.NbMethods
orderby nom descending
select new { c, nom }

Putting it all together

Now that we know how to compute each of the required metrics, let’s see how the detection strategy looks like:

// <Name>Tradition Breaker</Name>
warnif count > 0

// ** Helper Functions **
// ** NAS & PNAS **
let isPublicNonStaticMethod = new Func<IMethod, bool>(m =>
 m.IsPublic &&
 !m.IsClassConstructor && 
 !m.IsConstructor && 
 !m.IsStatic)

let publicMethodsFor = new Func<IType, IEnumerable<IMethod>>(c =>
 c.Methods.Where(m => isPublicNonStaticMethod(m))) 

let newlyAddedMethodsFor = new Func<IType, IEnumerable<IMethod>>(c =>
 c.Methods.Where(m => 
   isPublicNonStaticMethod(m) &&
   m.OverriddensBase.Count() == 0)) 

// ** WMC **
let wmcFor = new Func<IType, int>(t => 
 t.MethodsAndContructors
  .Select(m => (int) m.CyclomaticComplexity.GetValueOrDefault())
  .Sum())

// ** AMW **
let amwFor = new Func<IType, double?>(c =>
 (double?) c.CyclomaticComplexity / c.NbMethods)

// ** Thresholds **
let TwoThirds = 0.66
let amwAverage = 2
let wmcVeryHigh = 47
let nomAverage = 7
let nomHigh = 10

// ** Detection Strategy **
from c in JustMyCode.Types.Where(t => 
 t.IsClass && t.DepthOfDeriveFrom("System.Object") > 1)

// ** NAS & PNAS **
let newlyAddedMethods = newlyAddedMethodsFor(c)
let publicMethodsCount = publicMethodsFor(c).Count()
let nas = newlyAddedMethods.Count()
let pnas = publicMethodsCount == 0 ? 
 null : 
 (double?) nas/publicMethodsCount

// ** WMC **
let wmc = wmcFor(c)
let wmcParent = wmcFor(c.BaseClass)

// ** NOM **
let nom = c.NbMethods
let nomParent = c.BaseClass.NbMethods

// ** AMW **
let amw = amwFor(c)
let amwParent = amwFor(c.BaseClass)

// ** Componenets **
let excessiveIncreaseOfChildClassInterface =
 // More newly added services than average NOM per class
 nas > nomAverage &&
 // Newly added services are dominant in child class
 pnas > TwoThirds

let childClassHasSubstantialSizeAndComplexity = 
 (
  // Method complexity in child class above average
  amw > amwAverage ||
  // Functional complexity of child class is very high
  wmc >= wmcVeryHigh
 ) &&
 // Class has substantial number of methods
 nom >= nomHigh

let parentClassIsNeitherSmallNorDumb = 
// Parent functional complexity above average
amwParent > amwAverage &&
// Parent has more than half of child's methods
nomParent > (nomHigh / 2) &&
// Parent's complexity more than half of child'
wmcParent > (wmcVeryHigh / 2)
 
where excessiveIncreaseOfChildClassInterface && 
 childClassHasSubstantialSizeAndComplexity &&
 parentClassIsNeitherSmallNorDumb 

select new { c, 
 nas, pnas,
 amw, wmc, nom,
 amwParent, wmcParent, nomParent }