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.