Clean Code

How to identify a Data Class using NDepend

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

In this article we’ll see how to identify the Data Class code smell.

Data Class Detection Strategy

Data Classes are classes that expose their data directly and have few functional methods. Object-Oriented Metrics in Practice, by Michele Lanza and Radu Marinescu, proposes the following detection strategy for Data Classes:

(WOC < One Thrid) AND
                  (((NOPA + NOAM > Few) AND (WMC < High)) OR
                    ((NOPA + NOAM > Many) AND (WMC < Very High)))

This detection strategy uses four metrics:

  • WOC – Weight Of a Class – to measure the relative number of functional members compared to all members of a class
  • NOAM – Number of Accessor Methods
  • NOPA – Number of Public Attributes
  • WMC – Weighted Method Count – to measure class complexity

This detection strategy uses three types of thresholds:

  • NOPA and NOAM use Generally-Accepted Meaning Thresholds. Few is defined between 2 and 5. Many is the short term memory capacity, so it’s 7 or 8.
  • WOC uses a Common Fraction Threshold. One Third is 0.33.
  • WMC uses a Statistics-Based Threshold. 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 High threshold for WMC is 31 and Very High is 47.

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.

WOC – Weight Of a Class

This metric measure the relative number of functional members compared to all members of a class. It’s computed by dividing the number of public functional members by the total number of public members.

// <Name>WOC</Name>
// ** Helper Functions **
let isProperty = new Func<ICodeElement, bool>(member => 
 member.IsMethod && 
 (member.AsMethod.IsPropertyGetter || member.AsMethod.IsPropertySetter)) 

// ** Metric Functions **
let allPublicFunctionalMembersFor = new Func<IType, IEnumerable<IMember>>(t =>
 t.Methods.Where(m => m.IsPublic && !isProperty(m) && !m.IsAbstract))

let allPublicMembersFor = new Func<IType, IEnumerable<IMember>>(t =>
 t.Members.Where(m => 
     m.IsPublic && 
     (!m.IsMethod || (!m.AsMethod.IsClassConstructor && !m.AsMethod.IsConstructor))))

let wocFor = new Func<IType, double>(t =>
 (double) allPublicFunctionalMembersFor(t).Count() / allPublicMembersFor(t).Count())

// ** Sample Usage **
from t in JustMyCode.Types.Where(type => !type.IsEnumeration)
let functionalPublicMembers = allPublicFunctionalMembersFor(t)
let allPublicMembers = allPublicMembersFor(t)
let woc = wocFor(t)
orderby woc ascending 
select new { t, woc, functionalPublicMembers , allPublicMembers }

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 }

NOAM – Number of Accessor Methods

This metric counts the number of accessors (getters and setters) of a class.

// <Name>NOAM</Name>
// ** Helper Functions **
let isProperty = new Func<ICodeElement, bool>(member => 
 member.IsMethod && 
 (member.AsMethod.IsPropertyGetter || member.AsMethod.IsPropertySetter)) 

// ** Metric Functions **
let accessorsFor = new Func<IType, IEnumerable<IMember>>(t =>
 t.Methods.Where(m => m.IsPublic &&
                      isProperty(m) && 
                      !m.IsAbstract && 
                      !m.IsStatic))

let noamFor = new Func<IType, int>(t => 
 accessorsFor(t).Count())

// ** Sample Usage **
from t in JustMyCode.Types
let accessors = accessorsFor(t)
let noam = noamFor(t)
orderby noam ascending 
select new { t, noam, accessors }

NOPA – Number of Public Attributes

This metric counts the number of public attributes of a class.

// <Name>NOPA</Name>
// ** Metric Functions **
let publicAttributesFor = new Func<IType, IEnumerable<IMember>>(t => 
 t.Fields.Where(f => f.IsPublic && !f.IsInitOnly && !f.IsStatic))

let nopaFor = new Func<IType, int>(t => publicAttributesFor(t).Count())

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

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>Data Class</Name>
warnif count > 0
// *** WOC ***
// ** Helper Functions **
let isProperty = new Func<ICodeElement, bool>(member => 
 member.IsMethod && 
 (member.AsMethod.IsPropertyGetter || member.AsMethod.IsPropertySetter)) 

// ** Metric Functions **
let allPublicFunctionalMembersFor = new Func<IType, IEnumerable<IMember>>(t =>
 t.Methods.Where(m => m.IsPublic && !isProperty(m) && !m.IsAbstract))

let allPublicMembersFor = new Func<IType, IEnumerable<IMember>>(t =>
 t.Members.Where(m => 
      m.IsPublic && 
      (!m.IsMethod || (!m.AsMethod.IsClassConstructor && !m.AsMethod.IsConstructor))))

let wocFor = new Func<IType, double>(t =>
 (double) allPublicFunctionalMembersFor(t).Count() / allPublicMembersFor(t).Count())

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

// *** NOAM ***
let accessorsFor = new Func<IType, IEnumerable<IMember>>(t =>
 t.Methods.Where(m => m.IsPublic && 
                      isProperty(m) && 
                      !m.IsAbstract && 
                      !m.IsStatic))

let noamFor = new Func<IType, int>(t => 
 accessorsFor(t).Count())

// *** NOPA ***
// ** Metric Functions **
let publicAttributesFor = new Func<IType, IEnumerable<IMember>>(t => 
 t.Fields.Where(f => f.IsPublic && !f.IsInitOnly && !f.IsStatic))

let nopaFor = new Func<IType, int>(t => publicAttributesFor(t).Count())

// ** Thresholds **
let Few = 5
let Many = 8
let OneThird = 0.33
let wmcVeryHigh = 47
let wmcHigh = 31

// ** Detection Strategy **
from t in JustMyCode.Types
let woc = wocFor(t)
let wmc = wmcFor(t)
let nopa = nopaFor(t)
let noam = noamFor(t)

where 
 // Interface of class reveals data rather than offering services
 (woc < OneThird) && 
  (
   (
    // More than a few public data
    (nopa + noam) > Few && 
    // Complexity of the class is not high
    (wmc < wmcHigh)
   ) ||
   (
    // Class has many public data
    (nopa + noam) > Many &&
    // Complexity of the class is not very high 
    (wmc < wmcVeryHigh)
   )
 )

select new { t, woc, wmc, nopa, noam }

Conclusion

Implementing the Data Class detection strategy with NDepend was easy. After running the detection strategy on a bigger project, I noticed that it (rightfully) picks up many DTO classes. What I ended up doing was adding another Where clause in the detection strategy. This way I can ignore certain namespaces and assemblies from triggering false positives.