C++ Refactoring Tip: Primitive Obsession

C++ code refactoring is difficult because the C++ language is large and complex with a hard-to-process syntax (tools like Lattix Architect can simplify the process). Given the importance of refactoring, here is a C++ refactoring tip for solving Primitive Obsession.

What is Primitive Obsession?

Primitive Obsession is using primitive data types (like integers, strings, doubles, etc.) to represent a more complicated entity such as share prices or temperature. Primitive types are generic because many people use them, but a class provides a simpler and more natural way to model things. When the data type becomes sufficiently complex (i.e. share prices can’t be negative), it might be time to replace the primitive data type with an object.

Why Refactor (C++ example)?

Often when you start writing code you use a primitive data type to represent a “simple” entity. Here is a C++ example:

class Stock
{
private:
	std::string company;
	int shares;
	double share_val;
public:
	Stock();         // default constructor
	Stock(const std::string & co, long n = 0, double pr = 0.0);
	void buy(long num, double price);
	void sell(long num, double price);
	void update(double price);
};

In this case, you are representing share_val as a double even though share_val should never be negative. To make this behavior work, you need to add type checking in the code. You will see four instances of type checking in the C++ code below:

// constructors
Stock::Stock()          // default constructor
{
	company = "no name";
	shares = 0;
	share_val = 0.0;
}

Stock::Stock(const std::string & co, long n, double pr)
{
	company = co;
	shares = n;

	if (pr < 0)
	{
		std::cout << "Share price can't be negative; "
			<< company << " share price set to 0.\n";
		share_val = 0;

	}
	else
	    share_val = pr;
}

// other methods
void Stock::buy(long num, double price)
{
	shares += num;
	
	if (price < 0)
	{
		std::cout << "The share price can't less than zero. "
			<< "Transaction is aborted.\n";
	}
	else
		share_val = price;
}

void Stock::sell(long num, double price)
{
	if (num > shares)
	{
		std::cout << "You can't sell more than you have! "
			<< "Transaction is aborted.\n";
	}
	else
	{
		shares -= num;
		if (price < 0)
		{
			std::cout << "Share price can't be less than 0"
				<< "Transaction is aborted.\n";
		}
		else
			share_val = price;
	}
}

void Stock::update(double price)
{
	if (price < 0)
	{
		std::cout << "Share price can't be less than 0"
			<< "Transaction is aborted.\n";
	}
	else
	    share_val = price;
}
In this case, we had to put four identical instances of validation logic to handle share_val (code smell: duplication). This breaks the "Don't repeat yourself principle which states “Every piece of knowledge must have a single, unambiguous, authoritative representation in the system.” The benefit to moving all this to a class is all the relevant behaviors will be in one place in the code.

How to Refactor (C++)

1. Create a new class for this field that contains all the validation logic that is currently spread across the application
class SharePrice
{
private:
	double shareVal;
public:
	SharePrice();
	SharePrice(double price);
	double getPrice() const { return shareVal; }
	void setPrice(double price, bool initial=false);
};
SharePrice::SharePrice()
{
	setPrice(0, true);
}
SharePrice::SharePrice(double price)
{
	setPrice(price, true);
}

void SharePrice::setPrice(double price, bool initial)
{
	if (price < 0)
	{
		if (initial)
		{
			std::cout << "Share price can't be negative "
				<< " share price set to 0.\n";
			shareVal = 0;
		}
		else
		{
			std::cout << "Share price can't be negative"
				<< "Transaction aborted.\n";
		}
	}
	else
		shareVal = price;
}
2. In the original class, change the field type(double share_val in this case) to the new class (SharePrice). Also, change the getters/setters in the original class to call the getters/setters in the new class (also may have to change constructor if initial values had been set to field values).
#include <string>
#include "shareprice.h"

class Stock
{
private:
	std::string company;
	int shares;
	SharePrice share_val;
public:
	Stock();         // default constructor
	Stock(const std::string & co, long n = 0, double pr = 0.0);
	void buy(long num, double price);
	void sell(long num, double price);
	void update(double price);
};
// constructors
Stock::Stock()          // default constructor
{
	company = "no name";
	shares = 0;
	share_val.setPrice(0.0, true);
}

Stock::Stock(const std::string & co, long n, double pr)
{
	company = co;
	shares = n;

	share_val.setPrice(pr, true);
}

// other methods
void Stock::buy(long num, double price)
{
	shares += num;

	share_val.setPrice(price, false);
}

void Stock::sell(long num, double price)
{
	if (num > shares)
	{
		std::cout << "You can't sell more than you have! "
			<< "Transaction is aborted.\n";
	}
	else
	{
		shares -= num;
		share_val.setPrice(price);
	}
}

void Stock::update(double price)
{
	share_val.setPrice(price);
}
3. Compile and test.

Summary

Refactoring in C++ is harder than C# or Java, but the reasons for refactoring (improved quality, better maintainability, etc.) make it worthwhile. As you can see in this example, the code in the C++ source file went from 72 lines to 46 lines. In most cases, duplicated code represents a failure to fully factor the design. Duplicate code makes modifying the code more difficult. Whenever you make a change in one place you need to remember all the other places where that code must be changed. As Martin Fowler said, the definition of refactoring is “a change made to the internal structure of the software to make it easier to understand and cheaper to modify without changing its observable behavior.” Check out our C++ Architectural Refactoring page for more information on how Lattix Architect can help with C++ refactoring.