11 min read

(For more resources on Microsoft products, see here.)

Larry Constantine is attributed with the creation of systematic measurement of software quality. In the mid-to-late seventies, Larry Constantine (and Ed Yourdon) attributed several things to the quality of software code. Under the umbrella of structured design, among those attributes of quality software code were cohesion and coupling. At the time they associated quality with generality, flexibility, and reliability. In this article series, we’re going to concentrate on generality and flexibility and how cohesion and coupling can be applied to increase code quality.

Larry Constantine:http://en.wikipedia.org/wiki/Larry_Constantine

Cohesion applies to many different disciplines. Cohesion in physics relates to the force that keeps molecules integrated and united and that makes the molecule what it is. A highly cohesive molecule is one that tends to remain autonomous and not adhere to or blend with other molecules. In geology, cohesion is the strength of substances to resist being broken apart. In linguistics, it’s the degree to which text is related. Text whose content relates to the same subject is cohesive.

Cohesion in software is very similar to cohesion elsewhere. A cohesive block of code is a block of code that relates well together and has the ability to remain together as a unit. Object-oriented programming brings distinct cohesive abilities. All programming languages have certain cohesive abilities, such as their ability to group code in modules, source files, functions, and so on. A programming language’s ability to define physical boundaries enables cohesiveness. A module, for example, defines a specific boundary for which some content is retained and other content is repelled. Code from one module can use code from another module, but only in specific and defined ways—usually independent of language syntax. All code within a module has innate cohesion: their relation amongst themselves as being contained within the module.

Any rule, principle, guideline, or practice needs to be implemented thoughtfully. This text isn’t a manual on how you must perform your refactoring; it’s a description of several types of refactorings and their impetus. By the same token, this text doesn’t set out to prove the benefits of any particular rule, principle, guideline, or practice. “Mileage may vary” and incorrect usage will often negate most, if not all, benefits. I’ll leave it as an exercise for the reader to find the research that “proves” the benefits of any particular rule, principle, guideline, or practice. This text assumes the generally accepted benefits of various principles and practices, including cohesion, as an indicator of quality. If you decide that the benefits aren’t outweighing the costs, it’s up to you to decide not to implement that principle.

Class cohesion

Object-orientation brings extra cohesive abilities to the programmer. The programmer has the ability to relate code together within a class. Other code can use the code within a class, but only through its defined boundaries (the class’s methods and properties).

In object-oriented design, cohesion is generally much more than simply code contained within a class. Object-oriented cohesiveness goes beyond the physical relation of code within a class and deals with the relation of meaning of the code within a class.

Object-oriented language syntax allows the programmer to freely relate code to other code through a class definition, but this doesn’t mean that code is cohesive. For example, let’s revisit our Invoice class so far.

/// <summary>
/// Invoice class to encapsulate invoice line items
/// and drawing
/// </summary>
public class Invoice
{
private IInvoiceGrandTotalStrategy
invoiceGrandTotalStrategy;
public Invoice(IEnumerable<InvoiceLineItem>
invoiceLineItems,
IInvoiceGrandTotalStrategy invoiceGrandTotalStrategy)
{
InvoiceLineItems = new
List<InvoiceLineItem>(invoiceLineItems);
this.invoiceGrandTotalStrategy =
invoiceGrandTotalStrategy;
}
private List<InvoiceLineItem> InvoiceLineItems
{
get;
set;
}
public void GenerateReadableInvoice(Graphics graphics)
{
graphics.DrawString(HeaderText,
HeaderFont,
HeaderBrush,
HeaderLocation);

float invoiceSubTotal = 0;
PointF currentLineItemLocation = LineItemLocation;
foreach (InvoiceLineItem invoiceLineItem in
InvoiceLineItems)
{
float lineItemSubTotal =
CalculateLineItemSubTotal(invoiceLineItem);
graphics.DrawString(invoiceLineItem.Description,
InvoiceBodyFont,
InvoiceBodyBrush,
currentLineItemLocation);

currentLineItemLocation.Y +=
InvoiceBodyFont.GetHeight(graphics);
invoiceSubTotal += lineItemSubTotal;
}
float invoiceTotalTax =
CalculateInvoiceTotalTax(invoiceSubTotal);
float invoiceGrandTotal =
invoiceGrandTotalStrategy.CalculateGrandTotal(
invoiceSubTotal,
invoiceTotalTax);
CalculateInvoiceGrandTotal(invoiceSubTotal,
invoiceTotalTax);
graphics.DrawString(String.Format(
"Invoice SubTotal: {0}",
invoiceGrandTotal - invoiceTotalTax),
InvoiceBodyFont, InvoiceBodyBrush,
InvoiceSubTotalLocation);
graphics.DrawString(String.Format("Total Tax: {0}",
invoiceTotalTax), InvoiceBodyFont,
InvoiceBodyBrush, InvoiceTaxLocation);
graphics.DrawString(String.Format(
"Invoice Grand Total: {0}",
invoiceGrandTotal), InvoiceBodyFont,
InvoiceBodyBrush, InvoiceGrandTotalLocation);
graphics.DrawString(FooterText,
FooterFont,
FooterBrush,
FooterLocation);
}

public static float CalculateInvoiceGrandTotal(
float invoiceSubTotal, float invoiceTotalTax)
{
float invoiceGrandTotal = invoiceTotalTax +
invoiceSubTotal;
return invoiceGrandTotal;
}

public float CalculateInvoiceTotalTax(
float invoiceSubTotal)
{
float invoiceTotalTax =
(float)((Decimal)invoiceSubTotal *
(Decimal)TaxRate);
return invoiceTotalTax;
}
public static float
CalculateLineItemSubTotal(
InvoiceLineItem invoiceLineItem)
{
float lineItemSubTotal =
(float)((decimal)(invoiceLineItem.Price
- invoiceLineItem.Discount)
* (decimal)invoiceLineItem.Quantity);
return lineItemSubTotal;
}
public string HeaderText { get; set; }
public Font HeaderFont { get; set; }
public Brush HeaderBrush { get; set; }
public RectangleF HeaderLocation { get; set; }
public string FooterText { get; set; }
public Font FooterFont { get; set; }
public Brush FooterBrush { get; set; }
public RectangleF FooterLocation { get; set; }
public float TaxRate { get; set; }
public Font InvoiceBodyFont { get; set; }
public Brush InvoiceBodyBrush { get; set; }
public Point LineItemLocation { get; set; }
public RectangleF InvoiceSubTotalLocation { get; set; }
public RectangleF InvoiceTaxLocation { get; set; }
public RectangleF InvoiceGrandTotalLocation { get; set; }
}

We have an operational Invoice class. It does some things, and they work. But, our Invoice class isn’t very cohesive. The Invoice class has distinct groups of fields. Some are for the state of an invoice and some are for generating a readable invoice. Methods that deal with the behavior and attributes of an invoice don’t use the fields that deal with generating a readable invoice.

Our Invoice class is implementing two distinct tasks: managing invoice state and generating a readable invoice. The data required to generate a readable invoice (over and above the data shown on an invoice) isn’t used by Invoice when not generating a readable invoice.

Our Invoice class can be said to have multiple responsibilities: the responsibility of managing state and the responsibility of generating a readable invoice. What makes an invoice an invoice may be fairly stable; we may occasionally need to add, remove, or change fields that store data contained in an invoice. But, the act of displaying a readable invoice may be less stable: it may change quite frequently. Worse still, the act of displaying a readable invoice may depend on the platform it is running on.

The Single Responsibility Principle

In terms of focusing refactoring efforts towards cohesiveness of a class, the Single Responsibility Principle (SRP) gives us guidance on what a particular class should or should not do. The Single Responsibility Principle states that “there should never be more than one reason for a class to change”. In the case of our invoice class there’s a couple of reasons why we’d need to change the class:

  • The way an invoice is displayed needs to change.
  • The data that is contained in an invoice needs to change.

The stability of each responsibility shouldn’t need to depend on the other. That is, any change to the Invoice class affects stability of the whole class. If I often need to change the way an invoice renders a displayable invoice, all of Invoice is instable—including its responsibility to the data an invoice contains.

The Single Responsibility Principle’s focus is fairly narrow: class responsibility. A novice may simply accept that scope and use it only to focus cohesion efforts at the class level. The fact is that the Single Responsibility Principle is applicable at almost all levels of software design, including method, namespace, module/assembly, and process; that is, a method could implement too much responsibility, the types within an assembly or namespace could have unrelated responsibilities, and the responsibilities of a given process might not be focused effectively.

Simply saying “single responsibility” or detailing that something has too many responsibilities is simple. But the actual act of defining what a responsibility is can be very subjective and subtle. Refactoring towards Single Responsibility can take some time and some work to get right. Let’s see how we can improve quality through better cohesion and the principle of single responsibility.

Refactoring classes with low-cohesion

Clearly single responsibility (and the heading of this section) suggests that we should refactor Invoiceinto multiple classes. But, how do we do that given our current scenario? The designer of this class has obviously thought that the functionality of Invoice included rendering a readable invoice, so how do we make a clean separation? Fortunately, the lack of cohesiveness gives us our separation points. What makes an invoice an invoice doesn’t include those fields that deal solely with rendering a readable invoice. Fields like HeaderFont, HeaderBrush, HeaderText, HeaderLocation, FooterFont, FooterBrush, FooterText, FooterLocation, InvoiceBodyFont, InvoiceBodyBrush, LineItemLocation, InvoiceBustTotalLocation, InvoiceTaxLocation, and InvoiceGrandTotalLocation all deal with just the rendering responsibility.

The Invoice class is modeling a real-world invoice. When you hold an invoice in your hand or view it on the screen, it’s already rendered. In the real-world we’d never think that a responsibility of rendering an invoice would be a responsibility of the invoice itself.

We know we want to retain our original Invoice class and we want to move the rendering responsibility to a new class. This new class will encapsulate the responsibility of rendering an invoice. Since this new class will take an Invoice object and help another class produce something useful, we can consider this an invoice rendering service.

In order to refactor our existing Invoice class to a new Invoice rendering service, we start with a new InvoiceRenderingService class and move the HeaderFont, HeaderBrush, HeaderText, HeaderLocation, FooterFont, FooterBrush, FooterText, FooterLocation, InvoiceBodyFont, InvoiceBodyBrush, LineItemLocation, InvoiceBustTotalLocation, InvoiceTaxLocation, and InvoiceGrandTotalLocation fields to the InvoiceRenderingService. Next, we move the GenerateReadableInvoice method to the InvoiceRenderingService. At this point, we basically have a functional class, but since the InvoiceRenderingService method was on the Invoice classes, the other properties that the GenerateReadableInvoice uses need an Invoice object reference—effectively changing it from “this” to a parameter to the GenerateReadableInvoice method. Since the original Invoice class was never expected to be used externally like this, we need to add a CalculateGrandTotal method that delegates to the invoiceGrandTotalStrategy object. The result is something like the following:

/// <summary>
/// Encapsulates a service to render an invoice
/// to a Graphics device.
/// </summary>
public class InvoiceRenderingService
{
public void GenerateReadableInvoice(Invoice invoice,
Graphics graphics)
{
graphics.DrawString(HeaderText,
HeaderFont,
HeaderBrush,
HeaderLocation);
float invoiceSubTotal = 0;
PointF currentLineItemLocation = LineItemLocation;
foreach (InvoiceLineItem invoiceLineItem in
invoice.InvoiceLineItems)
{

float lineItemSubTotal =
Invoice.CalculateLineItemSubTotal(
invoiceLineItem);
graphics.DrawString(invoiceLineItem.Description,
InvoiceBodyFont,
InvoiceBodyBrush,
currentLineItemLocation);

currentLineItemLocation.Y +=
InvoiceBodyFont.GetHeight(graphics);
invoiceSubTotal += lineItemSubTotal;
}
float invoiceTotalTax =
invoice.CalculateInvoiceTotalTax(
invoiceSubTotal);
float invoiceGrandTotal =
invoice.CalculateGrandTotal(
invoiceSubTotal,
invoiceTotalTax);
Invoice.CalculateInvoiceGrandTotal(invoiceSubTotal,
invoiceTotalTax);

graphics.DrawString(String.Format(
"Invoice SubTotal: {0}",
invoiceGrandTotal - invoiceTotalTax),
InvoiceBodyFont, InvoiceBodyBrush,
InvoiceSubTotalLocation);
graphics.DrawString(String.Format("Total Tax: {0}",
invoiceTotalTax), InvoiceBodyFont,
InvoiceBodyBrush, InvoiceTaxLocation);
graphics.DrawString(String.Format(
"Invoice Grand Total: {0}",
invoiceGrandTotal), InvoiceBodyFont,
InvoiceBodyBrush, InvoiceGrandTotalLocation);
graphics.DrawString(FooterText,
FooterFont,
FooterBrush,
FooterLocation);
}
public string HeaderText { get; set; }
public Font HeaderFont { get; set; }
public Brush HeaderBrush { get; set; }
public RectangleF HeaderLocation { get; set; }
public string FooterText { get; set; }
public Font FooterFont { get; set; }
public Brush FooterBrush { get; set; }
public RectangleF FooterLocation { get; set; }
public Font InvoiceBodyFont { get; set; }
public Brush InvoiceBodyBrush { get; set; }
public Point LineItemLocation { get; set; }
public RectangleF InvoiceSubTotalLocation { get; set; }
public RectangleF InvoiceTaxLocation { get; set; }
public RectangleF InvoiceGrandTotalLocation { get; set; }
}

Alternatively, the whole use of invoiceGrandTotalStrategy can be moved into the InvoiceRenderingService—which is a better design decision.

Detecting classes with low-cohesion

So, we’ve seen a fairly simple example of making a not-so-cohesive class into two more-cohesive classes; but, one of the tricky parts of refactoring away classes with low cohesion is finding them. How do we find classes with low-cohesion?

Fortunately, many people have put time and effort over the years into defining what it means for a class to be cohesive. There have been various metrics researched and created over the years to define cohesion in classes. The most popular metric is Lack of Cohesion of Methods (LCOM) . Lack of Cohesion of Methods measures the degree to which all methods use all fields. The more segregated field usage is amongst methods of a class, the higher the Lack of Cohesion of Methods metric of the class will be. Lack of Cohesion of Methods is a measure of the entire class, so it won’t point out where the class is not cohesive or indicate where the responsibilities can be separated.

Lack of Cohesion of Methods is a measurement of the degree to which fields are used by all methods of a class. Perfection as defined by Lack of Cohesion of Methods is that every method uses every field in the class. Clearly not every class will do this (and arguably this will hardly ever happen); so, Lack of Cohesion of Methods is a metric, as most metrics are, that requires analysis and thought before attempting to act upon its value. LCOM is a value between 0 and 1, inclusive. A measure of 0 means every method uses every field. The higher the value, the less cohesive the class; the lower the value, the more cohesive the class. A typical acceptable range is 0 to 0.8. But, there’s no hard-and-fast definition of specific value that represents cohesive; just because, for example, a class has an LCOM value of 0.9, that doesn’t mean it can or should be broken up into multiple classes. Lack of Cohesion of Methods values should be used as a method of prioritizing cohesion refactoring work by focusing on classes with higher LCOM values before other classes (with lower LCOM values).

In the case of our Invoice class, it’s apparent that its LCOM value does mean it can be split into multiple classes as we detailed in the previous section.

LEAVE A REPLY

Please enter your comment!
Please enter your name here