Good software is modularized. When you have related functions scattered throughout your project, you will miss good opportunities to improve reusability and remove duplication from your code. Not only that, but your code will be much harder to test because there’s no clear way to test related functions that are not grouped together.
One way to group these functions is just to put them under a single file—so they become in the same module. That would be a good choice if these functions don’t work on the same instance of data.
If these functions work on the same data, then it’s better to encapsulate this data in a class.
Let’s see an example where I have multiple functions that read the stats of child elements inside some container.
Example: before grouping
In this example, I have some container element, and I need to know some stats about its children—like the total number of elements, the number of hidden elements, and the number of visible elements.
function getChildrenCount(element) {
return element.children.length
}
function getHiddenChildrenCount(element) {
return Array.from(element.children).filter((child) => !child.offsetParent)
.length
}
function getVisibleChildrenCount(element) {
return Array.from(element.children).filter((child) => child.offsetParent)
.length
}
This is a simple example; in real projects you might have more functions with more complex logic. But even with this simple example, I’ll show you what benefits you can get by grouping them under a class.
Example: after grouping
I’ll group these functions under a class named NodeContainerStats
. In the first step, I’ll add the same functions to the class and convert them to getters. And then, I’ll show you what opportunities grouping gives you to improve that code.
class NodeContainerStats {
#container
constructor(element) {
this.#container = element
}
get childrenCount() {
return this.#container.children.length
}
get hiddenChildrenCount() {
return Array.from(this.#container.children).filter(
(child) => !child.offsetParent
).length
}
get visibleChildrenCount() {
return Array.from(this.#container.children).filter(
(child) => child.offsetParent
).length
}
}
The first benefit I can see after just doing this is that now I have a single, dedicated place for reading stats about a specific elements container.
To iterate over the container’s children, I need to convert it to an array using Array.from
. I can simplify this by extracting it to a private getter. And then I have to replace all Array.from(this.#container.children)
with just that getter.
class NodeContainerStats {
#container
constructor(element) {
this.#container = element
}
get #children() {
return Array.from(this.#container.children)
}
get childrenCount() {
return this.#children.length
}
get hiddenChildrenCount() {
return this.#children
.filter(child => !child.offsetParent)
.length
}
get visibleChildrenCount() {
return this.#children
.filter(child => child.offsetParent)
.length
}
}
This doesn’t only remove duplication, but it also makes it easy to change what children to return for that container. In this case I’m using .children
, which returns only element nodes. If I want it to include non-element nodes like text and comment, I can just switch it to .childNodes
—I just need to do that in one place: #children()
.
Next, I can see some duplication in hiddenChildrenCount
and visibleChildrenCount
getters. They both need to know if an element is hidden. So, this is a good opportunity to extract a function for that.
class NodeContainerStats {
#container
constructor(element) {
this.#container = element
}
#isHidden(element) {
return element.offsetParent === null
}
get #children() {
return Array.from(this.#container.children)
}
get childrenCount() {
return this.#children.length
}
get hiddenChildrenCount() {
return this.#children
.filter(this.#isHidden)
.length
}
get visibleChildrenCount() {
return this.#children
.filter(!this.#isHidden)
.length
}
}
The way I check if an element is hidden is by checking if its offsetParent
is null. If I want to change this to another check, I have a clear place for that—#isHidden
.
Another thing I can improve here is visibleChildrenCount
. This function does the exact opposite of hiddenChildrenCount
. This means I can keep the logic in one of them and calculate the other one based off of it—this might not seem a very useful refactoring, but this way I remove some code duplication, and I also can see that there is some kind of relationship between these two functions.
class NodeContainerStats {
#container
constructor(element) {
this.#container = element
}
#isHidden(element) {
return !element.offsetParent
}
get #children() {
return Array.from(this.#container.children)
}
get childrenCount() {
return this.#children.length
}
get hiddenChildrenCount() {
return this.#children
.filter(this.#isHidden)
.length
}
get visibleChildrenCount() {
return this.#children.length - this.hiddenChildrenCount
}
}
With that, I have a clear place for reading stats of container elements. It’s easy to test, change, and reuse.