Image by cookie_studio on Freepik
It smells because there are likely many instances where it could be edited or improved.
Most of these smells are just hints of something that might be wrong. They are not required fixed per se… (You should look into it though.)
Let's continue...
You need to validate strings. So you don't need strings at all
TL;DR: Search for missing domain objects when validating strings.
Serious software has lots of string validations.
Often, They are not in the correct places.
This leads to non-robust and corrupt software.
The simple solution is to build only real-world and valid abstractions.
<?
// First Example: Address Validation
class Address {
function __construct(string $emailAddress) {
// String validation on Address class violates SRP
$this->validateEmail($emailAddress);
// ...
}
private function validateEmail(string $emailAddress) {
$regex = "/[a-zA-Z0-9_-.+]+@[a-zA-Z0-9-]+.[a-zA-Z]+/";
// Regex is a sample / It might be wrong
// Emails and Urls should be first class objects
if (!preg_match($regex, $emailAddress))
{
throw new Exception('Invalid email address ' . emailAddress);
}
}
}
// Second Example: Wordle
class Wordle {
function validateWord(string $wordleword) {
// Wordle word should be a real world entity. Not a subset of strings
}
}
<?
//First Example: Address Validation
class Address {
function __construct(EmailAddress $emailAddress) {
// Email is always valid / Code is cleaner
// ...
}
}
class EmailAddress {
// We can reuse this object many times avoiding copy-pasting
string $address;
private function __construct(string $emailAddress) {
$regex = "/[a-zA-Z0-9_-.+]+@[a-zA-Z0-9-]+.[a-zA-Z]+/";
// Regex is a sample / It might be wrong
// Emails and Urls are first class objects
if (!preg_match($regex, $emailAddress))
{
throw new Exception('Invalid email address ' . emailAddress);
}
$this->address = $emailAddress;
}
}
// Second Example: Wordle
class Wordle {
function validateWord(WordleWord $wordleword) {
// Wordle word is a real world entity. Not a subset of string
}
}
class WordleWord {
function __construct(string $emailAddress) {
// Avoid building invalid world words
// For example length != 5
}
}
We can check all constructors validating strings and reify the missing concepts.
Small objects are hard to find.
Primitive obsessors always complain about this kind of indirections.
Creating these new small concepts keeps our model loyal to the bijection and ensures our models are always healthy.
Code Smell 41 - Regular Expression Abusers
Code Smell 04 - String Abusers
Photo by Brett Jordan on Unsplash
Less than 10% of the code has to do with the ostensible purpose of the system; the rest deals with input-output, data validation, data structure maintenance, and other housekeeping.
Mary Shaw
Software Engineering Great Quotes
Objects are there for the picking. Even the smallest ones.
TL;DR: Use small objects instead of primitive ones.
We are very lazy to create small objects.
We are also lazy to separate What and How.
We like very much to understand the internals of how things work.
We need to start thinking in a white box way and looking at the protocol and behavior of small components.
// Samples borrowed with permission from
// https://towardsdev.com/why-a-host-is-not-a-string-and-a-port-is-not-an-integer-595c182d817c
var port = 8080;
var in = open("example.org", port);
var uri = urifromPort("example.org", port);
var address = addressFromPort("example.org", port);
var path = pathFromPort("example.org", port);
// Samples borrowed with permission from
// https://towardsdev.com/why-a-host-is-not-a-string-and-a-port-is-not-an-integer-595c182d817c
const server = Port.parse(this, "www.kivakit.org:8080");
// Port is a smallobject with responsibilities and protocol
let in = port.open(this);
const uri = port.asUri(this);
const address = port.asInetSocketAddress();
const path = port.path(this, "/index.html");
We can automate checks on constructors for small objects missing opportunities.
We need to transform our strings, numbers, and arrays into small objects.
Code Smell 121 - String Validations
Code Smell 04 - String Abusers
Photo by K. Mitch Hodge on Unsplash
Iteration allows us to progressively approach some goal. We can discard the steps that take us further away and prefer the steps that move us nearer. This is in essence how evolution works. It is also at the heart of how modern machine learning (ML) works.
Dave Farley
Software Engineering Great Quotes
We love looking at the internal gears of the clock, but we need to start focusing on the hands.
TL;DR: Don't mess with implementation details. Be declarative. Not imperative.
Separating concerns is very difficult in the software industry.
Functional software survives ages.
Implementation software brings coupling and is harder to change.
Choosing wise declarative names is a daily challenge.
class Workflow {
moveToNextTransition() {
// We couple the business rule with the accidental implementation
if (this.stepWork.hasPendingTasks) {
throw new Exception('Preconditions are not met yet..');
} else {
this.moveToNextStep();
}
}
}
class Workflow {
moveToNextTransition() {
if (!this.canWeMoveOn()) {
throw new Exception('Preconditions are not met yet..');
} else {
this.moveToNextStep();
}
}
canWeMoveOn() {
// We hide accidental implementation 'the how'
// under the 'what'
return !this.stepWork.hasPendingTasks();
}
}
This is a semantic and naming smell.
We need to choose good names and add indirection layers when necessary.
Of course, premature optimizators will fight us, telling us we are wasting computational
resources and they need to know the insights we are hiding from them.
Code Smell 92 - Isolated Subclasses Names
Code Smell 05 - Comment Abusers
Photo by Josh Redd on Unsplash
The idea of this smell is here:
Code Smell 118 - Return False's comment
and here:
We are constantly interfacing with other people's code that might not live up to our high standards and dealing with inputs that may or may not be valid. So we are taught to code defensively. We use assertions to detect bad data and check for consistency.
Andrew Hunt
Software Engineering Great Quotes
You change something in a class. You change something unrelated in the same class
TL;DR: Classes should have just one responsibility and one reason to change.
We create classes to fulfill responsibilities.
If an object does too much, it might change in different directions.
class Webpage {
renderHTML() {
renderDocType();
renderTitle();
renderRssHeader();
renderRssTitle();
renderRssDescription();
// ...
}
// HTML render can change
renderRssDescription() {
// ...
}
renderRssTitle() {
// ...
}
renderRssPubDate() {
// ...
}
// RSS Format might change
}
class Webpage {
renderHTML() {
this.renderDocType();
this.renderTitle();
(new RSSFeed()).render();
this.renderRssTitle();
this.renderRssDescription();
// ...
}
// HTML render can change
}
class RSSFeed {
render() {
this.renderDescription();
this.renderTitle();
this.renderPubDate();
// ...
}
// RSS Format might change
// Might have unitary tests
// etc
}
We can automatically detect large classes or track changes.
Classes must follow the Single Responsibility Principle and have just one reason to change.
If they evolve in different ways, they are doing too much.
Code Smell 34 - Too Many Attributes
Code Smell 94 - Too Many imports
A design that doesn’t take change into account risks major redesign in the future.
Erich Gamma
Software Engineering Great Quotes
We learned at school that inheritance represents an 'is-a' relationship. It is not.
TL;DR: Think about protocol and behavior, forget inheritance
IS-A relation comes from the data world.
We learned ERDs with structured design and data modeling.
Now, we need to think in terms of behavior.
Behavior is essential, data is accidental.
// If you made Square derive from Rectangle,
// then a Square should be usable anywhere you expect a rectangle
#include <iostream>
Rectangle::Rectangle(const unsigned width, const unsigned height):
m_width(width),
m_height(height)
{
}
unsigned Rectangle::getWidth() const
{
return m_width;
}
void Rectangle::setWidth(const unsigned width)
{
/* Width and Height are independent */
m_width = width;
}
unsigned Rectangle::getHeight() const
{
return m_height;
}
void Rectangle::setHeight(const unsigned height)
{
m_height = height;
}
unsigned Rectangle::area() const
{
/*Valid for both Rectangles and Squares*/
return m_height * m_width;
}
Square::Square(const unsigned size)
: Rectangle(size, size)
{
}
// OK for squares, bad for rectangles
// Protocol is bad, width and height are not relevant on squares
void Square::setWidth(const unsigned size)
{
m_height = size;
m_width = size;
}
// OK for squares, bad for rectangles
// Protocol is bad, width and height are not relevant on squares
void Square::setHeight(const unsigned size)
{
m_height = size;
m_width = size;
}
void process(Rectangle& r)
{
unsigned h = 10;
auto w = r.getWidth();
r.setHeight(h);
std::cout << "Expected area: " << (w*h) << ", got " << r.area() << "\n";
// area is not well defined in squares
// every square IS-A rectangle, but does not behave-like a rectangle
}
int main()
{
Rectangle rectangle{3,4};
Square square{5};
process(rectangle);
process(square);
}
// If you made Square derive from Rectangle,
// then a Square should be usable anywhere you expect a rectangle
#include <iostream>
Rectangle::Rectangle(const unsigned width, const unsigned height):
m_width(width),
m_height(height)
{
}
void Rectangle:changeWidth(const unsigned width)
{
/* Width and Height are independant */
m_width = width;
// We should discuss if it is good to mutate
// and not create a new Figure
}
void Rectangle::changeHeight(const unsigned height)
{
m_height = height;
}
unsigned Rectangle::area() const
{
return m_height * m_width;
}
// No inheritance
Square::Square(const unsigned size):
m_size(size)
{
}
unsigned Square::area() const
{
return m_size * m_size;
}
void Square::changeSize(const unsigned size)
{
m_size = size;
}
void testRectangleChange(Rectangle& r)
{
unsigned h = 10;
auto w = r.getWidth();
r.setHeight(h);
std::cout << "Expected area: " << (w*h) << ", got " << r.area() << "\n";
// area is not well defined in squares
// every square IS-A rectangle, but does not behave-like a rectangle
}
int main()
{
Rectangle rectangle{3,4};
Square square{5};
testRectangleChange(rectangle);
testRectangleChange(square);
}
This is a semantic smell.
Real Number IS-A Complex number (according to math).
Integer IS-A Real number (according to math).
Real Number does not Behave-Like-A Complex number.
We cannot do real.setImaginaryPart() so it is not a Complex according to our Bijection
Code Smell 92 - Isolated Subclasses Names
Code Smell 11 - Subclassification for Code Reuse
Code Smell 37 - Protected Attributes
Photo by Joshua Rondeau on Unsplash
DRY - Don't Repeat Yourself - Every piece of knowledge must have a single, unambiguous, authoritative representation within a system.
Andy Hunt
Software Engineering Great Quotes
And that’s all for now…
The next article will explain 5 more code smells!