Best Practices: Debunking the myth of good and bad around objective software code design and engineering

Ifeora Okechukwu
36 min readJul 19, 2023

--

There are a good number of things in software engineering that are truly subjective. For example, the debate about: tabs vs. spaces (code formatting) or vim vs. vscode (code editors) or prefixing interface names with an “I” (code variable naming) or methods of estimation (code task planning) and so on. These debates are usually dictated by personal preference and that’s fine. But, there are more things about software engineering that are truly objective and reducing them to a set of mere opinionated arguments that are inherently flawed (because they don’t always apply to all and every use case, scale or context) is doing a great disservice to the ecosystem and software industry at large. Some now even insist that all of software engineering is highly subjective. But is it ?

By the end of this article, one should be able to correct conclude that best practices do in fact exist under certain contexts (objectively not subjectively) and like invariants, as long as the pre-conditions in such contexts hold true, these best practices will always apply.

I think the major reason why we struggle with the idea that certain aspects of software engineering are objective is because most of knowledge within and about coding or software engineering is tacit or implicit. This means most people (even experts) cannot articulate it properly. It sucks but it’s just the way it is. Best practices for any discipline (especially for example: architecture) are never fully formed in one or two generations of endeavour or blind trial-and-error. Times, tools, practices and people change and evolve! Some of what was impossible in the 1970s are very possible today. What was correct in the 1980s may not be correct today too. Back in the late 1970s, there was smaller memory and larger and heavier computing hardware. This isn’t the case today.

Sarcastic tweet mirroring wild takes on Twitter — No group of engineers have been able to submit actual evidence that unit tests are a waste of time

There seems to be 2 extremes when it comes to personal views or opinions on best practices:

  1. People who don’t believe in best practices due to ugly personal experiences with misapplication/abuse of said best practices and hence avoid them totally.
  2. People who believe in best practices but are too fanatical about them.

A lot of software engineers today loathe clean code, coding patterns & object-oriented programming for a plethora of reasons but mainly due to misapplication.

Many say “Inheritance is completely bad and should be avoided!” Some say “Singletons are an anti-pattern!”. Others say “Polymorphism is a drag!” or “Unit tests are a waste of time! or “Readability is myth — always subjective!” or “Getters and Setters in OOP are totally evil!”. Some others even say things like “Micro-services are evil!” or “TDD is dead, long live testing!. The list goes on and on. Few times, these statements contain some valid criticism.

Most times, these are all just myths and misguided opinions borne out of a gross misunderstanding or frustration with flagrant abuse of said practice(s). If you dig deeper below the surface, you find that none of these things are inherently as bad or as awful as most would think — they simply work until they don’t or they are abused by bad programmers. It is simply the way we use (or abuse) these ideas that makes most of the difference. I put together a thread on Twitter to make this point. I would also like to buttress a few more points:

Firstly, inheritance is at its’ core about “code reuse”. This is very similar to composition (used most and mainly in Functional programming languages). The saying: “Prefer Composition over Inheritance” is a bold statement which after much experience and consideration, i have come to believe is not correct/plausible. I have heard many software practitioners insist on preferring composition which seems to makes wonderful sense in practice until you run into problems with composition itself. Because, if the saying is to be believed, why does this RFC proposal exist on the Rust Github page (opened since 2014) ?

Rust prefers composition over inheritance (obvious at least in the language primitives: impl = struct + trait). That means when coding in rust, we should use composition for all cases of code reuse. However, this RFC proposal seems to be evidence that though it can be done, it comes at some terrible cost. I implore you to study the discussions in this RFC (especially if you’re a Rust fan like myself). Golang seems to share a similar RFC to the one for Rust

So, my question is: which is better since both can have issues of their own ? The reality is that both work well until they don’t. You have to realise this and use them in the appropriate case. Inheritance is strictly used for creating hierarchies of specialization (a less specific type of behavior as you move up the inheritance chain and vice versa) while Composition is strictly used for creating hierarchies of enhancements (a less enhanced copy of behavior as you move down the composition chain and vice versa). As you already know, Inheritance embodies a IS-A relationship while Composition embodies a HAS-A relationship. You can read more about it here.

Also, the revered Allen Holub in 2003 made the statement that setters and getters in OOP are evil. It so turns out that they aren’t evil, what makes them evil is the way they are used to return private class members without any offensive programming or data validation thereby eviscerating encapsulation. Excessive use of getters/setters usually reveal a design flaw and are more or less useless much after the flaw has been rectified. One of such flaws has to do with the Tell Don’t Ask guideline. Once behavior (logic) is properly collocated with data, getters especially become unnecessary. Sometimes getters/setters can be necessary for reformatting private class member data without returning a reference to the private class member itself. Martin Fowler does share a similar conclusion to mine here in his article.

A practical example of these (properly using inheritance and getters/setters) is creating point vector objects used in simple computer graphics software or a 2D game loop. A 2D Point has similarities with a 3D Point but they are not the same thing exactly. A 2D Point IS-A Point and a 3D Point is also a Point. Yet, a 3D Point is not a 2D Point.

Below is some source code that captures these ideas:

/* A POINT - Class Definition */
/* The base class - Point ("abstract") class */

class Point {
/* @HINT: Private class members ES13 (ES 2022) */
/* @CHECK: https://www.javascripttutorial.net/javascript-private-fields/ */
#dimension;
#vector;

constructor (vector = [], dimension = 0) {
if (!Array.isArray(vector)) {
throw new Error("Not a point vector");
}

if (vector.length !== dimension) {
throw new Error(`Not a "${dimension}D" point vector`);
}

const total = vector.reduce((sum, radix) => {
return sum + radix;
}, 0);

if (!total || isNaN(total) || Number.isNaN(total)) {
throw new Error("Not a valid point vector");
}

this.#dimension = dimension;
this.#vector = vector;
}

get coordinates () {
throw new Error("Implementation needed");
}

set modifyAxisAt ({ mark = 0, value = -1 }) {
if ((!mark || !value)
|| (Number.isNaN(mark) || Number.isNaN(value))) {
throw new Error("invalid `mark` and/or `axis` sentinel");
}

if (typeof value === "number"
&& typeof mark === "number"
&& (-1 < mark && mark < this.#dimension)) {
const vector = this.#vector;
vector[mark] = value;
this.#vector = vector;
}
}

magnitude () {
const sumOfSquares = this.#vector.reduce((sum, vectorAxis) => {
return sum + (vectorAxis ** 2);
}, 0);

return Math.sqrt(
sumOfSquares
);
}

translate (linearDirection = []) {
throw new Error("Implementation needed");
}

rotate (angleInDegrees = 0) {
throw new Error("Implementation needed");
}

clone () {
throw new Error("Implementation needed");
}
}
/* A 2D POINT - Class Definition - INCORRECT VERSION */
/* Extends the Point ("abstract") class - can't be instantiated!! */
/* Implements all abstract methods: `translate()`, `clone()`, `rotate()` ... */

class Point2D extends Point {
constructor (vector = [0, 0]) {
super(vector, 2);
}

get coordinates () {
/* @HINT: Correct use of a getter/setter */
/* @NOTE: getters/setters can be necessary for
reformatting private class member without
exposing the reference to those class members
*/
const [ x, y ] = this.#vector;
return {
x,
y
};
}

translate (linearDirection = [0, 0]) {
if (!Array.isArray(linearDirection)) {
return this.clone();
}

const total = linearDirection.reduce((sum, radix) => {
return sum + radix;
}, 0);

if (isNaN(total) || Number.isNaN(total)) {
throw new Error(
"Argument: [linearDirection]; not a valid vector"
);
}

const [ translateX, translateY ] = linearDirection.length === this.#dimension
? linearDirection
: [0, 0];

const { x, y } = this.coordinates;

return new Point2D(
[
x + translateX,
y + translateY
]
);
}

rotate (angleInDegrees = 0) {
const { x, y } = this.coordinates;
const cosThetha = Math.cos(angleInDegrees);
const sinThetha = Math.sin(angleInDegrees);

const newXAxis = Math.ceil((x * cosThetha) - (y * sinThetha));
const newYAxis = Math.ceil((x * sinThetha) + (y * cosThetha));

return new Point2D([newXAxis, newYAxis]);
}

clone () {
const coordinates2D = this.coordinates;
return new Point2D(
[
coordinates2D.x,
coordinates2D.y
]
);
}
}

Now, we have a dilemma! How do we efficiently build a 3D Point class ? Do we make a 3D Point class extend a 2D Point class or a Point class ? You might opt to extend a 2D Point but remember that in doing so we will be breaking the Liskov Substitution principle slightly. You see the rotate() method takes one additional parameter (the axis of rotation) for a 3D transform while the rotate() method of a 2D transform doesn’t. This means if you treat a 3D Point as a 2D Point, you won’t pass the second argument when calling the rotate() method. Hmmm, how do we remedy this ?

/*
class Point3D extends Point2D {

BAD!!!! 🤨😖

- A 3D point IS-NOT-A 2D point

}
*/

We can try to use both composition an inheritance (mix and match them), where they each apply! Here goes:

/* 3D POINT - Class Definition - INCORRECT VERSION */
/* Extends the Point ("abstract") class */
/* Implements all abstract methods: `translate()`, `clone()`, `rotate()` ... */


/* !! Inheritance !! */
class Point3D extends Point {
#point2D;
constructor (vector = [0, 0, 0]) {
super(vector, 3);
/* !! Composition !! */
this.#point2D = new Point2D(vector.slice(0, 2));
}

get coordinates () {
/* @HINT: Correct use of a getter/setter */
/* @NOTE: getters/setters can be necessary for
reformatting private class member without
exposing the reference to those class members
*/
const [ x, y, z ] = this.#vector;
return {
x,
y,
z
};
}

translate (linearDirection = [0, 0, 0]) {
const coordinates3D = this.coordinates;

this.#point2D.modifyAxisAt = { mark: 0, value: coordinates3D.x };
this.#point2D.modifyAxisAt = { mark: 1, value: coordinates3D.y };

const translatedPoint2D = this.#point2D.translate(
linearDirection.slice(0, -1)
);
const coordinates2D = translatedPoint2D.coordinates;

this.#point2D.modifyAxisAt = { mark: 0, value: 0 };
this.#point2D.modifyAxisAt = { mark: 1, value: 0 };

return new Point3D(
[
coordinates2D.x,
coordinates2D.y,
coordinates3D.z + linearDirection[this.#dimension - 1]
]
);
}

rotate (angleInDegrees = 0, axis = "Z") {
const coordinates3D = this.coordinates;

switch (axis) {
case "Z":
this.#point2D.modifyAxisAt = { mark: 0, value: coordinates3D.x };
this.#point2D.modifyAxisAt = { mark: 1, value: coordinates3D.y };

const rotatedPoint2D = this.#point2D.rotate(
angleInDegrees
);
const coordinates2D = rotatedPoint2D.coordinates;

this.#point2D.modifyAxisAt = { mark: 0, value: 0 };
this.#point2D.modifyAxisAt = { mark: 1, value: 0 };

return new Point3D(
[
coordinates2D.x,
coordinates2D.y,
coordinates3D.z
]
);
break;
case "X":
this.#point2D.modifyAxisAt = { mark: 0, value: coordinates3D.y };
this.#point2D.modifyAxisAt = { mark: 1, value: coordinates3D.z };

const rotatedPoint2D = this.#point2D.rotate(
angleInDegrees
);
const coordinates2D = rotatedPoint2D.coordinates;

this.#point2D.modifyAxisAt = { mark: 0, value: 0 };
this.#point2D.modifyAxisAt = { mark: 0, value: 0 };

return new Point3D(
[
coordinates3D.x,
coordinates2D.x,
coordinates2D.y
]
);
break;
case "Y":
this.point2D.modifyAxisAt = { mark: 0, value: coordinates3D.x };
this.point2D.modifyAxisAt = { mark: 1, value: coordinates3D.z };

const rotatedPoint2D = this.#point2D.rotate(
angleInDegrees
);
const coordinates2D = rotatedPoint2D.coordinates;

this.#point2D.modifyAxisAt = { mark: 0, value: 0 };
this.#point2D.modifyAxisAt = { mark: 0, value: 0 };

return new Point3D(
[
coordinates2D.y,
coordinates3D.y,
coordinates2D.x
]
);
break;
}
}

clone () {
const coordinates3D = this.coordinates;
return new Point3D(
[
coordinates3D.x,
coordinates3D.y,
coordinates3D.z
]
);
}
}

The solution for the 3D Point class above 👆🏾👆🏾👆🏾👆🏾 is a fair one but not a great one. We still have the problem with the rotate() method. If JavaScript supported interfaces (like Golang), i could maybe try to use the Interface Segregation principle to solve the problem but that will only work to an extent.

Now, there’s another problem here: The cross product of a 2D vector is a scaler but the cross product of a 3D vector is another 3D (not a scaler). Therefore, from a Liskov Substitution Principle (LSP) perspective, this is bad news! This effectively tells us that A 2D Point cannot be a Point in the same way that a 3D Point cannot also be a Point. We are now beginning to see ways in which a Point base class cannot apply to both the Point2D and Point3D classes respectively. This means we have modelled the problem wrongly using inheritance by have prematurely adopted inheritance here.

So, therefore what we can do here is ditch inheritance only and simply use composition because it makes the most sense.

/* A 2D POINT - Class Definition - CORRECT VERSION */

class Point2D {
/* @HINT: Private class members ES13 (ES 2022) */
/* @CHECK: https://www.javascripttutorial.net/javascript-private-fields/ */
#dimension;
#vector;

constructor (vector = [0, 0]) {
if (!Array.isArray(vector)) {
throw new Error("Not a point vector");
}

if (vector.length !== 2) {
throw new Error(`Not a "2D" point vector`);
}

const total = vector.reduce((sum, radix) => {
return sum + radix;
}, 0);

if (!total || isNaN(total) || Number.isNaN(total)) {
throw new Error("Not a valid point vector");
}

this.#dimension = 2;
this.#vector = vector;
}

get coordinates () {
/* @HINT: Correct use of a getter/setter */
/* @NOTE: getters/setters can be necessary for
reformatting private class member without
exposing the reference to those class members
*/
const [ x, y ] = this.#vector;
return {
x,
y
};
}

set modifyAxisAt ({ mark = 0, value = -1 }) {
if ((!mark || !value)
|| (Number.isNaN(mark) || Number.isNaN(value))) {
throw new Error("invalid `mark` and/or `axis` sentinel");
}

if (typeof value === "number"
&& typeof mark === "number"
&& (-1 < mark && mark < this.#dimension)) {
const vector = this.#vector;
vector[mark] = value;
this.#vector = vector;
}
}

magnitude () {
const sumOfSquares = this.#vector.reduce((sum, vectorAxis) => {
return sum + (vectorAxis ** 2);
}, 0);

return Math.sqrt(
sumOfSquares
);
}

translate (linearDirection = [0, 0]) {
if (!Array.isArray(linearDirection)) {
return this.clone();
}

const total = linearDirection.reduce((sum, radix) => {
return sum + radix;
}, 0);

if (isNaN(total) || Number.isNaN(total)) {
throw new Error(
"Argument: [linearDirection]; not a valid vector"
);
}

const [ translateX, translateY ] = linearDirection.length === this.#dimension
? linearDirection
: [0, 0];

const { x, y } = this.coordinates;

return new Point2D(
[
x + translateX,
y + translateY
]
);
}

rotate (angleInDegrees = 0) {
const { x, y } = this.coordinates;
const cosThetha = Math.cos(angleInDegrees);
const sinThetha = Math.sin(angleInDegrees);

const newXAxis = Math.ceil((x * cosThetha) - (y * sinThetha));
const newYAxis = Math.ceil((x * sinThetha) + (y * cosThetha));

return new Point2D([newXAxis, newYAxis]);
}

clone () {
const coordinates2D = this.coordinates;
return new Point2D(
[
coordinates2D.x,
coordinates2D.y
]
);
}
}

So, in other words, if you need to move boilerplate around cheaply amongst two or more very related software components and/or extract commonality of implementation (not behavior) among two or more very related things, then use inheritance. Else, if you need to reuse code among two or more very related things and also change implementation on the fly then use composition. They each have their uses. In fact you can mix and match both. Inheritance is strictly for composing polymorphic entities and subtypes. Hence, polymorphic entities are strictly created by Interfaces/Abstract classes. Composition doesn’t enforce polymorphic outcomes or subtypes!

At this point, many readers might say: “You see ? Composition is more flexible than Inheritance”. This statement is 100% true! However, not every time in every case. Languages like PHP and Java grossly limited inheritance by making it such that only one class could inherit from another and restricted the richness of implementation however, these languages could use composition as a workaround and an extra tool. It wasn’t the paradigm (OOP) that placed a restriction for inheritance to be from a single parent only. It was the language(s) that placed that restriction. Another issue with OOP languages practitioners regarding wrongly applying Inheritance is believing that all parent classes had to be concrete (can be instantiated). This ought not to be so all the time because when you do Inheritance correctly, most parent classes are never a fully-formed idea or blueprint to create anything — just bits and pieces (abstract classes). In my opinion, these are what led to the widespread abuse of inheritance.

Another major reason software engineers run into so many issues with (OOP) inheritance is they start by writing change-resistant, premature code too early. It is always easier for you to let the code tell you what the hierarchy (or levels of inheritance) should be over time than for you to dictate to the code what it should be at the start. For instance:

class Bird:
# Can we sufficiently define how all kinds of birds fly here ? NO
# Are we sure all birds can fly ? NO
# What then happens if we need a bird to fly differently ? OVERRIDE FLYING?🤨

# The answer to the first two questions mean that this is a premature effort.
# It means that `fly()` belongs in a concrete class initially.
# It also means that the parent `Bird` super class is not needed initially.
def fly():
print("flying")

class Ostrich(Bird):
# This is now a problem because Ostriches don't/can't fly
pass

Defining a Bird class with a fly method and inheriting from it is premature. The fly method should be defined on a concrete class that is final (can’t be inherited or subclassed) initially until there’s a pertinent need to extract the fly method to some parent class. As you can see below, there is no such need. However, rather prematurely, the fly method is on a (python super class) not on a base class.

Let’s correct that: 👇🏾👇🏾

"""
There should be no Bird class to start with.
A Bird class should only exist as a result of
necessity and not empty "convention".

Much later, when we learn more about the domain and
problem (i.e. 'Birds in general'), we can then extract
the Bird class from the existing Ostrich class to enable
re-use.
"""

class Ostrich:
def fly():
print("flying")

Much Better! How do we know that this is a situation that can benefit from inheritance ? Well, we don’t know at the start until we have written enough code and understood enough of the domain (i.e Birds).

So, let’s assume a lot of time has gone by. This is “Bird” situation seems to be appropriate for inheritance after all. As we can see below, composition isn’t necessary here. After many iterations, the final result (codebase) is as follows:

from final_class import final


# Base Class (This base class is an abstract class)

class Bird:
# all birds are warm-blooded
endothermic = True
# all birds have 2 legs
legsCount = 2

def __init__(self, gender, stage, family):
# all birds have gender
self.gender = gender
# all birds have stages of development
self.stage = stage
# all birds have a family they belong to in a taxonomy
self.family = family

def name(self):
# all birds have a name
return self.__class__.__name__

def sex(self):
return self.gender

def eat():
raise NotImplementedError("subclasss should implement")

def canFly(self):
# birds of the `STRUTHIONIDAE` family cannot fly (e.g. Kiwis, Ostriches)
if self.family != "STRUTHIONIDAE":
# only `ADULT` birds can fly
return self.stage == "ADULT"
else:
return False

def canLayEggs(self):
# birds who are `MALE` cannot lay eggs
if self.gender != "MALE":
# only `ADULT` birds can lay eggs
return self.stage == "ADULT"
else:
return False











# "Interfaces" (All constructors for abstract classes should respect LSP)

class FeatheredWinger:
# Any living thing that has feathers on its' wings
def __init__(self, winged):
self.hasWings = winged

def spreadWings():
raise NotImplementedError("subclasss should implement")

def flapWingsInSync():
raise NotImplementedError("subclasss should implement")



class FeatheredFlyer(FeatheredWinger):
# Any living thing that can fly/perch and has feathers on its' wings
def __init__(self):
self.isFlying = False
super().__init__(True)

# There might be several different ways to fly so we shouln't preempt
# Let the sub class decide how flying is done
def fly(self):
raise NotImplementedError("subclasss should implement")

def perch(self):
raise NotImplementedError("subclasss should implement")

def isNowFlying(self):
return self.isFlying










class EggLayer:
# all egg layers are themselves hatched by incubation
hatchedByIncubation = True

# Any living thing that lays eggs
def __init__(self, incubationPeriodDaysRange, hatched, hasEggs):
self.incbationPeriodDaysRnage = incubationPeriodDaysRange
self.hatched = hatched
self.hasEggsToFertilize = False
self.hasEggs = hasEggs

# Let the sub class decide how flying is done
def layEggs():
raise NotImplementedError("subclasss should implement")















# Exceptions (All exceptions should be thrown explicitly with a message)

class CannotFlyError(Exception):
pass


class CannotLayEggsError(Exception):
pass













# This `Raven` class cannot be subclassed (all methods should respect LSP)
@final
class Raven(Bird, EggLayer, FeatheredFlyer):
def __init__(self, gender, stage):
birdFamily = "CORVIDAE"
# a raven is a bird
Bird.__init__(self, gender, stage, birdFamily)
# a raven lays egss
EggLayer.__init__(
self,
[20, 25], # egg incubation period (20 - 25 days)
stage != "EGG", # Is egg-layer an egg or not ?
gender == "FEMALE" # Can the egg-layer lay eggs ?
)
# a raven has feathers on it's wings and can fly
FeatheredFlyer.__init__(self)

def flapWingsInSync(self):
print(f'this {self.name()} has started flapping its wings...')

def spreadWings(self):
print(f'this {self.name()} has spread its wings...')
return True

def eat(self):
print(f'this {self.name()} is eating')

def layEggs(self):
if self.canLayEggs():
print(f'this {self.name()} is laying eggs')
else:
raise CannotLayEggsError(
f'this {self.name()} cannot lay eggs'
)

def perch(self):
if not self.isNowFlying():
return

self.isFlying = False
print(f'this {self.name()} has perched!')

def fly(self):
if self.isNowFlying():
return

if self.hasWings and self.canFly():
wingsNowSpread = self.spreadWings()
if wingsNowSpread:
self.flapWingsInSync()
self.isFlying = True
print(f'this {self.name()} is flying!')
else:
raise CannotFlyError(
f'this {self.name()} cannot fly'
)






# This `Ostrich` class cannot be sub-classed (all methods should respect LSP)
@final
class Ostrich(Bird, EggLayer, FeatheredWinger):
def __init__(self, gender, stage):
birdFamily = "STRUTHIONIDAE"
# an ostrich is a bird
Bird.__init__(self, gender, stage, birdFamily)
# an ostrich lays eggs
EggLayer.__init__(
self,
[42, 58], # egg incubation period (42 - 58 days)
stage != "EGG", # Is egg-layer an egg or not ?
gender == "FEMALE" # Can the egg-layer lay eggs ?
)
# an ostrich has feathers on it's wings
FeatheredWinger.__init__(self, True)

def flapWingsInSync(self):
print(f'this {self.name()} has started flapping its wings...')

def spreadWings(self):
print(f'this {self.name()} has spread its wings...')
return True

def eat(self):
print(f'this {self.name()} is eating')

def layEggs(self):
if self.canLayEggs():
print(f'this {self.name()} is laying eggs')
else:
raise CannotLayEggsError(
f'this {self.name()} cannot lay eggs'
)




# USAGE:
raven = Raven("MALE", "ADULT")
hatchling = Ostrich("FEMALE", "BABY")


raven.fly()

if raven.isNowFlying():
raven.perch()

print(Raven.endothermic)
print(Raven.hatchedByIncubation)

raven.layEggs() # throws an error because the raven is MALE

hatchling.canFly() # returns false (No errors)

hatchling.layEggs() # throws an error because the ostrich is a BABY

As we continue to code along for a good amount of time into the future, we discover that truly all birds can lay eggs. Now, do we do away with the EggLayer “interface” (i.e. python “abstract class”) completely or make the Bird class inherit or “implement” it ? It’s tempting to do away with the EggLayer “interface” but think: Isn’t it possible for other subclasses like a Reptile class to depend on it ?

Let’s see below: 👇🏾

from final_class import final

# Exceptions (All exceptions should be thrown explicitly with a message)

class CannotFlyError(Exception):
pass

class CannotLayEggsError(Exception):
pass











# Helpers (This helper class is a concrete class)

class Gonads:
# A sexual organ of any living thing
def __init__(self, gender)
self.gender = gender
self.isTouched = False

def type(self):
return "M" if self.gender == "MALE" else "F"

def recieve(self, gonad):
if isinstance(gonad, Gonad):
gonad.isTouched = True

if not gonad.isTouched:
gonad.recieve(self)





# Base Class (This base class is an abstract class)

class EggLayer:
# all egg layers are themselves hatched by incubation
hatchedByIncubation = True

# Any living thing that lays eggs
def __init__(self, incubationPeriodDaysRange, stage, gender):
# all animals who are egg layers have gender
self.gender = gender
# all animals who are egg layers have stages of development
self.stage = stage
# all animals who are egg layers have an incubation period
self.incbationPeriodDaysRnage = incubationPeriodDaysRange
self.hasEggsToFertilize = False

def canLayEggs(self):
# animals who are egg layers and `MALE` cannot lay eggs
if (self.gender != "MALE"):
# only `ADULT` birds can lay eggs
return self.hatched() and self.stage == "ADULT"
else:
return False

def attemptLayingEggs(self):
cannotLayEggs = not self.canLayEggs()
if cannotLayEggs:
raise CannotLayEggsError(
f'this {self.name()} cannot lay eggs'
)

def hatched(self):
return self.stage != "EGG"

def layEggs():
raise NotImplementedError("subclasss should implement")












# Sub Class (This sub class is also an abstract class)

"""
Example:

class Reptile(EggLayer):
pass
"""

class Bird(EggLayer):
# all birds are warm-blooded
endothermic = True
# all birds have 2 legs
legsCount = 2

def __init__(self, gender, stage, family, incubationPeriodDaysRange=[8,60]):
# all birds lay eggs
super().__init__(
incubationPeriodDaysRange,
stage,
gender
)
# all birds have a family they belong to in a taxonomy
self.family = family

def genitalia(self):
# [Composition] Tight coupling here; absolutely necessary and correct
# Why ? well because a gonad is needed to create a genitalia!
"""
If the public interface for `Gonad` class changes the
client code has to change with it. No other way...
"""
return Gonad(self.gender)

def name(self):
# all birds have a name
return self.__class__.__name__

def eat():
raise NotImplementedError("subclasss should implement")

def attemptMatingWith(self, bird):
if not isinstance(bird, Bird):
raise TypeError(f'Cannot mate with a {type(bird).__name__}')

def canFly(self):
# birds of the `STRUTHIONIDAE` family cannot fly (e.g. Kiwis, Ostriches)
if self.family != "STRUTHIONIDAE":
# only `ADULT` birds can fly
return self.stage == "ADULT"
else:
return False










# "Interfaces" (All constructors for abstract classes should respect LSP)

class FeatheredWinger:
# Any living thing that has feathers on its' wings
def __init__(self, winged):
self.hasWings = winged

def spreadWings():
raise NotImplementedError("subclasss should implement")

def flapWingsInSync():
raise NotImplementedError("subclasss should implement")

class FeatheredFlyer(FeatheredWinger):
# Any living thing that can fly/perch and has feathers on its' wings
def __init__(self):
self.isFlying = False
super().__init__(True)

# There might be several different ways to fly so we shouln't preempt
# Let the sub class decide how flying is done
def fly(self):
raise NotImplementedError("subclasss should implement")

def perch(self):
raise NotImplementedError("subclasss should implement")

def isNowFlying(self):
return self.isFlying











# This `Raven` class cannot be subclassed (all methods should respect LSP)
@final
class Raven(Bird, FeatheredFlyer):
def __init__(self, gender, stage):
birdFamily = "CORVIDAE"
# a raven is a bird
Bird.__init__(self, gender, stage, birdFamily, [20, 25])
# a raven has feathers on it's wings and can fly
FeatheredFlyer.__init__(self, True)

def flapWingsInSync(self):
print(f'this {self.name()} has started flapping its wings...')

def spreadWings(self):
print(f'this {self.name()} has spread its wings...')
return True

def layEggs(self):
Bird.attemptLayingEggs(self)
print(f'this {self.name()} is laying eggs')

def eat(self):
print(f'this {self.name()} is eating')

def perch(self):
if not self.isNowFlying():
return

self.isFlying = False
print(f'this {self.name()} has perched!')

def mateWith(self, anotherRaven):
self.attemptMatingWith(self, anotherRaven)
if (not isinstance(anotherRaven, Raven):
raise TypeError("Cannot mate with a non-raven")

selfGenitalia = self.genitalia()
ostrichGenitalia = ostrich.genitalia()

# sexual intercourse
ostrichGenitalia.recieve(selfGenitalia)

if (not selfGenitalia.isTouched and not ostrichGenitalia.isTouched):
raise TypeError("Mating unsucessful")
else:
print("Mating successful")

def fly(self):
if self.isNowFlying():
return

if (self.hasWings and self.canFly()):
wingsNowSpread = self.spreadWings()
if (wingsNowSpread):
self.flapWingsInSync()
self.isFlying = True
print(f'this {self.name()} is flying!')
else:
raise CannotFlyError(
f'this {self.name()} cannot fly'
)

# This `Ostrich` class cannot be subclassed (all methods should respect LSP)
@final
class Ostrich(Bird, FeatheredWinger):
def __init__(self, gender, stage):
birdFamily = "STRUTHIONIDAE"
# an ostrich is a bird
Bird.__init__(self, gender, stage, birdFamily, [42, 58])
# an ostrich has feathers on it's wings
FeatheredWinger.__init__(self, True)

def flapWingsInSync(self):
print(f'this {self.name()} has started flapping its wings...')

def spreadWings(self):
print(f'this {self.name()} has spread its wings...')
return True

def eat(self):
print(f'this {self.name()} is eating')

def mateWith(self, anotherOstrich):
self.attemptMatingWith(self, anotherOstrich)
if not isinstance(anotherOstrich, Ostrich):
raise TypeError("Cannot mate with a non-ostrich")

selfGenitalia = self.genitalia()
ostrichGenitalia = anotherOstrich.genitalia()

# sexual intercourse
ostrichGenitalia.recieve(selfGenitalia)

if not selfGenitalia.isTouched and not ostrichGenitalia.isTouched:
raise TypeError("Mating unsucessful")
else:
print("Mating successful")

def layEggs(self):
self.attemptLayingEggs()
print(f'this {self.name()} is laying eggs')


# USAGE:
raven = Raven("FEMALE", "ADULT")
egg = Ostrich("MALE", "EGG")

raven.layEggs()

egg.layEggs() # throws an error because the ostrich is at the EGG stage

As we can see above 👆🏾👆🏾, Inheritance done right does work! You just have to be careful how you use it and how far you push it. Please, don’t create premature parent classes. Let the patterns that form in the codebase reveal themselves over time. Create more of concrete final classes in the beginning. Hence, make use of inheritance where it makes sense and make use of composition where it makes sense. I personally do not believe in “Composition over Inheritance”. Composition is not a direct substitute for Inheritance and vice versa. However, they both have an overlap of qualities (e.g. code reuse) but they are not the same nor interchangable.

Secondly, Microservices are not about improving software performance or promoting separation of concerns. It’s more about the speed and flexibility of delivery to market and the structure and size of teams in relation to the growing size (feature-set size) of your digital product. Generally, if you have a very small team, you have no business building or managing several micro-services — keep it simple! Micro-services also cost a lot more to maintain and writing tests for Micro-services is hard. Remember that micro-services a just modules but at a higher level of organization. In the end, micro-services work! You just have to be careful how you use it.

Another issue is how folks over-split micro-services (or Bounded Contexts). It’s very unproductive! For example: I do not believe that a:

  • Billing service
  • Order service
  • Shipment Tracking service
  • Checkout service

All of these need not be different distinct services. They all depend on each other in a very direct way such that if the Billing service changes how it receives payment data, the Checkout service might have to change to conform to that. Similar with Order service and Shipment tracking service. In fact, they all share data belonging to the customer who is shopping. This means they would change for the same reasons (based on the Single Responsibility principle).

It follows that it would be better to merge them all into one wholesome service called:

  • Order Fulfilment service

In the same vein, I do not believe in separating:

  • API Gateway (Edge Micro-service)
  • Authentication service

API Gateways are used for access security and access control as well as routing (authorization, authentication, RBAC, rate limiting and protocol translation). So, why separate user authentication to it’s own distinct service ?

Thirdly, Test-Driven Development (TDD) is one of the most misunderstood tools out there. It’s often said to be counter-intuitive but is it ? TDD is a top-down approach to build software but your tests are used to drive the process. TDD is not about testing. TDD is about code design. You cannot learn and appreciate TDD properly if you haven’t learnt Testing throughly by itself first. Testing in TDD is just a means to an end. Should you use TDD everywhere ? Certainly not! In fact, one cannot use TDD for software projects that are open-ended where the requirements and output change frequently.

It’s not easy or possible to build user interfaces using TDD as you never know it’s done until after several iteration of usability testing. Best to use Integration tests for UI-specific work. TDD doesn’t work well with unknown unknowns and unknown knowns.

TDD only works well with known knowns and known unknowns. When it comes to known knowns, it means that there are no gaps in our knowledge of how to create the behavior we want/need in our software. As for known unknowns, it means there are known gaps (or in our knowledge but by doing some concrete experimentation (or spikes) and creating some throw-away code to fill the gaps in our knowledge, we can get back to using TDD afterwards. This point is important since TDD can lock-in design decisions which is a good thing but can be a bad thing for badly written code.

See below: 👇🏾👇🏾

If there’s anything i have learnt and learnt well in my many years as software engineer is that it’s never a straight line between theory and practice but you already knew this right ? Computer science theory suggests a lot of things of which some are helpful in practice and others aren’t pragmatic at all. However, to treat resistance or dissent to certain widespread and long-lived practices as proof that these practices are flat-out wrong and evolved from narrow-minded personal opinions is at best half right and at worst detrimental to career growth. We are all old enough to know that we cannot always throw away the baby and the bathwater. They weren’t wrong for a time in the past until things changes and made them wrong or obsolete or both.

Furthermore, all advice (or proposed ideas or practices) should never be followed blindly. The rest of us shouldn’t be too busy or lazy to raise questions and throughly inspect ideas. However, vehemently judging the proposed ideas or practices (often from a place of bias) of the past with the standards of the present is a bad way of looking at the entire topic of best practices that really helps no one. These days, nobody seems too interested in peer-reviewing pronouncements made by so-called industry leaders and tech trend influencers (a.k.a marketers for digital dev-tooling products). These days we even try to wholly discredit people who have a lot of skin in the game and have helped us with usable knowledge albeit partially flawed. Are we now too indifferent ? Have we now outsourced our agency to just a few people in the industry who we believe should tell us what practices to follow or discard without question ? This reminds me of the left-pad situation on NPM a couple of years ago.

Best practices — a completely flawed concept and nothing more ?

Over a decade ago (2008), Robert. C. Martin suggested the “Clean Code” and it became the holy grail of writing readable code and building great software for a long time until about half a decade ago when the face of computing changed significantly. When this happened a lot of people who didn’t know better then (but knew enough now) began to see cracks and faults with “Clean Code”. In the past (2017), I had also applied myself to this holy grail.

In fact, i took it as gospel when i was mostly naive about software best practices and inexperienced. Today i only agree with certain portions of it. Why ? Well, because things have become clearer with experience. Also, we now have better systems, better tools (well this is slightly subjective) and larger scale (software that serves millions and billions of people didn’t exist in the late 1990s and early 2000s) and much different software delivery environments.

Unfortunately, there are now a lot of harsh criticisms for the “Clean Code” practices. It’s just sad to see. Certain software practitioners just want to throw away the baby and the bath water. Does this mean that everything in the “Clean Code” isn’t worth learning about ? Well, the truth is more nuanced than that. It means, however, that not all practices in the “Clean Code” are best or correct or always applicable. I believe that Robert C. Martin isn’t above mistakes as he’s human. Also, remember that he built software in a different time than now. I think we should cut him some slack.

Yes, granted, ideas like “function definitions should always be small” is unrealistic (Chapter 3 — Clean Code). Usually, for functions or classes, size is always about 2 things (not 1 thing):

  1. Lines of Code
  2. Unrelated tasks

The number of lines of code should be small but not too small that it becomes unreadable or incomprehensible! The number of unrelated tasks should effectively be zero in a function or class. A function or class method should only do either on thing and/or a set of related things (Single Responsibility principle). This is in conflict with the “Clean Code” idea as it were. In fact, you can find code like the one below (in the “Clean Code” book):

/**
* THIS IS CLEARLY HORRIBLE CODE EVEN BY ANY STANDARD BECAUSE
* IT IS VERY STRESSFUL TO READ AND UNDERSTAND
* ALSO, the `includeSetupAndTeardownPages` does one thing extra
* which is `updatePageContent()` which violates Uncle Bobs' mantra of
* let a function do one thing.
*
* This is a mess!
*/
private void includeSetupAndTeardownPages() throws Exception {
includeSetupPages();
includePageContent();
includeTeardownPages();
updatePageContent();
}

/**
* ALSO, THIS IS CLEARLY HORRIBLE CODE EVEN BY ANY STANDARD BECAUSE
* IT IS VERY STRESSFUL TO READ AND UNDERSTAND
*/
protected static int[] generate(int n) {
primes = new int[n];
multiplesOfPrimeFactors = new ArrayList<Integer>();
set2AsFirstPrime();
checkOddNumbersForSubsequentPrimes();
return primes;
}

A function or class methods or class should never do half or three-quarters of one whole thing. Why ? Well, because chances are high that you will only use it in one place and it is always never going to be clear about what it does.

class FeatheredWinger:
# Anything that has feathers on its' wings
def __init__(self):
self.hasWings = True

def startSpreadWings():
# This function/method is too small
print("start to spread wings")

def finishSpreadWings():
# This function/method is too small
print("finish spreading wings at maximum stretch")

def spreadWings():
# This code is hard/stressful to read
# each function/method is doing half of one whole coherent thing

# Better to write code about spreading wings within `spreadWings()` method

startSpreadWings() # (one half) wrong!
finishSpreadWing() # (another half) also wrong!

Also, looking through the code example in the “Clean Code” book: Chapter 3, there are glaring inconsistencies; For example, Robert C. Martin goes again his own advice about meaningful names and readable code.

See here: 👇🏾👇🏾

private void includeSuiteSetupPage() throws Exception {
/**
* Here, Uncle Bob uses a constant static field `SUITE_SETUP_NAME` of
* a final class `SuiteResponder` for:
*/

// Yep! doing half of one thing
include(SuiteResponder.SUITE_SETUP_NAME, "-setup");
}

private void includeSetupPage() throws Exception {
/**
* Here, Uncle Bob uses a string literal which is one of the biggest
* sources of unwanted coupling and are also hard to understand when
* reading a codebase
*
* For instance: What is "Setup" or "-setup" and what does it mean ?
* It would have been better if he used an Enum or constatnts Data class
*/

// Also, doing half of one thing
include("SetUp", "-setup");
}

Below is a much better version of the complete code above (from Chapter 3 of the Clean Code book) written by someone else and presented to Uncle Bob himself:

👇🏾👇🏾

However, in spite of all of these flaws with “Clean Code”, it also has many good parts (and i dare say great parts too) that can be considered as thoroughly objective and useful:

  1. Prefer throwing/returning Exceptions to returning error codes (Not subjective)
  2. Command Query separation (Not subjective)
  3. Use descriptive names (Not subjective)
  4. Explain Yourself with syntax more and less with comments (Not subjective)
  5. Organizing for Change (Not subjective)
  6. Use Intention-revealing names (Not subjective)
  7. Don’t Repeat Yourself (but you can at the early stages of software project and where ever necessary — especially when writing tests!)
  8. Law of Demeter (but you can violate this law where applicable)
  9. Provide Context with Exceptions (Not subjective — A very good habit to form)
  10. Don’t return null (Not subjective — A jewel of the “Null object pattern”; but check for them (Balanced defensive programming)).

Then, there are other parts of the “Clean Code” that are too strict, nonsensical or outdated:

  1. One level of abstraction per Function (needless indirection or abstraction is not a virtue in computer programming)
  2. Have no Side Effects (Errrm… not realistic; even functional languages require side effects e.g. collateral effects)
  3. Uncle Bob’s formatting rules (highly subjective and seriously outdated)
  4. The 3 laws of TDD (The third law is very subjective)
  5. Write Your Try-Catch-Finally Statement First (Too defensive — I prefer Crash early first before I add a try-catch)

As you can see above 👆🏾👆🏾, the “Clean Code” mantra isn’t as horrible as many practitioners paint it out to be. It is clear that out of 15 points taken by me at random, 10 of them form solid best practices while only 5 are too strict or outdated to be use continually. What does that tell you ?

There are indeed actual “best practices” and fewer “best opinions”

What is a best practice ? How do we define one ? Software engineering is a little different from science. Scientific ideas can be tested by numerous scientists and replicated in experiments that form the basis of scientific laws (e.g. the laws of physics and chemistry). However, ideas in software engineering (or engineering in general) are more difficult to test and replicate among numerous engineers because engineering doesn’t have an exact ‘scientific method’ of its’ own. Yet, ideas do emerge as a standard reference solution to a problem space. These ideas can be evaluated and further promoted as fundamental principles by applying a system of rigorous tests when similar patterns emerge or are found within familiar contexts of a given problem space. This is known as the engineering design process or the engineering method (the use of heuristics to cause the best change in a poorly understood situation within the available resources). This is also how best practices are formulated and established.

You see i believe that simply stating that there are only opinions (the subjective inclination) and no actual best practices has no fundamental empirical value to begin with. Why ? well because it doesn’t take a lot of things into consideration like context of a problem space and adaptability to a solution space. Also, opinions are just that because they haven’t been tested or evaluated rigorously (say by peer-review). Yes, it’s true that there are no rules in software engineering — only principles because everything we do, every decision we make is heavily dependent on context. The context surrounding you makes your situation unique, and the tools you select must also be adapted to your circumstances. However, is this context solely unique to just you and your tech organisation and does it never ever change with time to resemble/mimic the context of others or other organisations ?

Most people believe that every best practice has to work for everyone and every team at any time for it to be deemed an actual best practice. This is false! How many times have you heard someone say: “We/I tried that and it didn’t work!” ? I bet a lot of times. This is because best practices aren’t always a one-size-fits-all thing and not because there are no best practices. A best practice may work for a given team, with a certain skill level at a certain time. However, as time goes on, that same best practices will no longer work for that same team especially if their context and circumstance has changed over time.

Now, it doesn’t mean that it ceases to be best practice when it no longer works. The problem is that context and circumstance is/are seen as a static concept that is always different for everyone and that doesn’t change with time. What worked for you or your tech department many years ago may not work today. Why is that ? Well, for one you are growing (and possibly scaling). The management style for a 5-man team is not the same for a 500-man team of sub-teams. The dynamics are different. As an industry, I don’t think we can afford to build software anyhow we like and based on our own personal opinions. That will be troublesome!

As software changes or evolves and as the people who work on it improve their knowledge and skill, decisions need to go through a much rigorous process of analysis. Software engineers must evolve novel ways of managing the process used to continually build it as well as manage the costs associated with building it. This means our favourite way, practice or tool may not cut it anymore. It’s up to us to do the work of upgrading the methods we apply gracefully and appropriately.

Moreover, effective engineering ultimately requires human understanding of many software details at once but always with low and manageable cognitive load. The engineering of medium to large software systems is likely to remain a hard problem to solve. Many advances have been made since the days of assembly language and Fortran programs. These advances have been both in programming language design and in software engineering tools as i highlighted earlier. But experience has shown time and time again that generic or “magic bullets” solutions are often double-edged swords best tucked away for good or used sparingly for very unique cases.

Yet, in recent times, our industry has continued to embrace these generic and/or “magic bullet” solutions as all-purpose solutions that over time degrade performance and hurt readability and reliability. Furthermore, these “magic bullet” solutions are hardly peer-reviewed and those who “market” them are often not very critical of them and resist any outside in-depth criticism of these solutions.

Employing design patterns ONLY when necessary and avoiding code smells are a great example of practices that aren’t magic bullets or generic. You have to apply them systematically to unique context. However, this application shouldn’t be dogmatic. In addition, the applications should adapt them to the context in which they are applied. Most of the criticisms for most best practices are simply people problems.

Let’s talk a bit about readability and maintainability of software code. A lot of people say this is all subjective. They say; “Great readability and maintainability is subject to individual interpretation.”. But is it really ? The way i see it, readability is often confused with familiarity. Maintainability is often confused with how often/quickly software can be “remodelled” or repurposed. However, it is measured by the ease of adding/removing features, quickly making sense of logic, fixing annoying bugs

Take a look at the code below:

<?php 

namespace Core\Banking;

use System;
use Http\Request;
use Http\Response;

class Payments {
public function transferFunds(Request $httpRequest, Response $httpResponse) {

$senderId = $httpRequest->getFromURLQuery('sender_id');
$recieverId = $httpRequest->getFromURLQuery('reciever_id');

$amount = intval($httpRequest->getFromEntityBody('transfer_amount'));

$sourceBankAccount = System::requestBankAccountFor($senderId);
$destinationBankAccount = System::requestBankAccountFor($recieverId);

$transaction = NULL;

try {

$transaction = System::establishPayoutIntentUsing(
array(
"reciever_bank_account" => $destinationBankAccount,
"sender_bank_account" => $sourceBankAccount
)
)->createTransaction("type:local")->valuedAt($amount);

System::scheduleOneTimePayoutUsing($transaction);

System::scheduleNotificationViaEmail(
array(
"sender_person" => $sourceBankAccount->owner,
"reciever_person" => $destinationBankAccount->owner,
"transaction_reference" => $transaction->reference,
"amount" => $amount
),
array(
"email_template_id" => "Transaction: Funds Transfer",
"email_no_reply" => true,
"email_attachments" => NULL
)
);

$httpResponse->reportServiceSuccess(
202,
"Accpeted: Funds transfer in progress",
);
} catch (Exception $error) {

$httpResponse->reportServiceError($error);
}
}
}

It seems to be very readable right ? Why is that ? Well, for one, i think that is because the code design and naming of class methods and variables is done with a decent amount of effort and care.

Whenever i hear people insist that codebase readability is only subjectively assessed and cannot be objectively assessed. I assume that they will reach the same conclusion for articles, novels, essays or news items written in a natural language like English. However, i don’t find this to be so. In fact, people often insist that certain articles or books are easier to understand because of clear writing. There are even tips or guidelines for such writing. Yet somehow, when it comes to programming languages (which differ very little from natural languages), we insist that readability is all too subjective.

I disagree.

Which of the two verions of code below would you consider is more readable ?

👇🏾👇🏾

// Version 1

function isUserEligible (user) {

if (user !== null) {
if (user.age >= 30) {

const today = new Date()
if (user.dob.getMonth() === today.getMonth()
&& user.dob.getDate() === today.getDate()) {
return true;
}
} else {
return false;
}
} else {
throw new Error("user unavailable")
}
}
// Version 2

function isUserEligible (user) {
if (user === null) {
throw new Error("user unavailable")
}

if (user.age < 30) {
return false;
}

const today = new Date()

if (user.dob.getMonth() === today.getMonth()
&& user.dob.getDate() === today.getDate()) {
return true;
}
}

In my experience, i have found that good readability of any codebase is possible by applying the tips for writing clearly in the programming language the codebase is written in. This includes organizing/grouping related points/thoughts (high locality of behavior) when writing and using simple (or idiomatic) vocabulary in said programming language.

I have always felt that high locality of behavior is very much connected to readability and maintainability but not in the way most people think. Most people insist that high locality of behavior has to do with assembling every piece of logic in one place or one source file. This is far from being useful especially at scale. The right way to setup a high locality of behavior is to separate intention from implementation. All codebases that utilise this idea have a high locality of behavior e.g. HTMX, Laravel e.t.c. Conversely, any codebase that doesn’t utilize this idea has a low locality of behavior because the code is hardly intuitive.

The ultimate test of any true best practice is scale

How does a best practice fair at scale ? When we have a lot of moving parts to manage and keep track of and fix, how does the said best practice fair ? Does it fair better or worse ? There are 3 areas of scale for measuring the effectiveness of a best practice and they are:

  1. Scale of the amount of usage it can tolerate
  2. Scale of the amount of complexity it can handle
  3. Scale of the amount of evolution it can foster (how well can it be adapted over time ?)

I remember back in 2014/2015 when Facebook touted the Flux Architecture as the next big thing in web frontend software development and before you could say jack, Redux was more popular than anything that came before it. A couple of events followed quickly. For instance, a plethora of open-source creations like immutability libraries with data structures that sprang up to solve speed problems that no one will ever have or ever had.

However, these days both Redux and the Flux Architecture have been some what retired as being too opinionated (most app state isn’t always global right ?). As Redux matured in the years past, almost everything about it eventually reeked of premature convolutions and might ultimately have led to this tweet (i guess):

In fact, to make matters worse, the Redux documentation site shares misleading information like this one. The point here is that Redux doesn’t scale well over time as a web application and its’ data grows bigger in size and complexity. You might only use Redux for all of a semi-simple small web application or for part of a complex large web application. In addition, its’ evolution or that of any of its derivatives have been limited in usage by this. ReduxToolkit is a friendlier adaptation of Redux but still suffers a bit from the limits of its’ parent. There are also parts of it that are difficult to defend. For example, the Redux middleware is a convoluted mess that never should have existed in the first place as it is a source of needless tight coupling. Also, combining reducers is a premature idea that serves no real purpose other than conforming to Reduxs’ own design. However, it may seem that the likes of Zustand are towing a similar path.

Still, Redux works and it works well! It works very well for quite simple and small web applications (e.g. a forms-only web app) that are not very chatty with a server and have very limited frequency of client-side interactivity and limited data sources to sync to. However, as said web application begins to get more and more chatty with a server as features are added, Redux begins to perform poorly. For now, other state primitives (like atoms, signals — other than the stores or state containers) are being experimented with at scale. Redux was tested (in situations where managing lots of server state and lots of client state is a must) and failed that test!

Similarly, we must test every “best practice” at scale to determine its’ durability when under heavy load or mildly extreme conditions. We must debate the different outcomes from several peer-reviewers and come to a consensus. This is how we sustainably grow the body of knowledge for best practices.

Again, Does Redux work ? Yes, it works very well. Is Redux a best practice ? No it’s not because it doesn’t scale well with usage, complexity or evolution of a software system even under the same pre-condition and context.

Best practices don’t work for every context of a problem but they should work for all under similar pre-conditions and context. Finally, great best practices evolve well over time and age well too!

Conclusion

True best practices should be rigorously tested and peer-reviewed under similar conditions and until they are, they cannot be deemed as true best practices.

Best practices will not always cover every context or situation but they will always work in every situation where the pre-condition and context are the same or very similar.

Most times, software engineers like to be viciously tribal or vehemently dismissive about languages, paradigms, design patterns and methodologies. This inadvertently hinders us from seeing the bigger picture. The reality is that you can write bad or great code in any programming language/paradigm or abuse any design pattern. These thing are merely tools and it’s up to us to wield them intelligently and with skill.

Consequently, the a reckless application of best practices when using said tools has led to toxic presentism that has unfortunately permeated our industry in recent times. This is beginning to undermine progress in our industry. We should be critical of any idea/pronouncement by a software practitioner that is seemingly to aid our task of building systems. However, in doing so, our job is to offer balanced and constructive criticism of the entire topic and only exorcise the bad parts so we can build something better.

As an industry, we don’t need more stern or harsh criticisms of past ideas of best practice. What we need more of are materials and work on standardizing best practices and literature (content) on avoiding the abuse of best practices which always lands us in very uncomfortable dilemma. We should build upon the work of people like Kent Beck, Martin Fowler, Micheal Feathers & Robert C. Martin and not tear all of their work down for cheap applause and hot takes. It’s disgusting!

Mistakes will unavoidably be made by inexperienced persons or bleeding-edge addicts and after a while a chorus of subjective ideas will emerge. However, we must continue to offer guidance using mostly non-trivial or real-world examples and teachings and together we can learn quickly to avoid the mistakes and get better at our craft. I myself do not claim to not have made mistakes or abused certain practices in the past but i have learned quickly and will continue to do so. I urge you to do the same!

Lastly, if you are not already, please get better at code design. 🙏🏾

--

--

Ifeora Okechukwu
Ifeora Okechukwu

Written by Ifeora Okechukwu

I like puzzles, mel-phleg, software engineer. Very involved in building useful web applications of now and the future.

No responses yet