1. Introduction
There has been an open discussion if it is better to treat software as a design or as an engineering in the industry for many years, starting with the famous blog post “What Is Software Design” by Jack W. Reeves, 1992.
While the posts share similar topic, I believe the primary focus should be on the desired outcome of the process rather than the specific terminology used. The difference between "development" and "engineering" is ultimately secondary to the fundamental goal of projects.
In this post I will show some techniques where well designed code is something more than just a working program, making it communicative and understandable not only by the experts.
2. Design patterns
2.1. Builder pattern
Let us say the goal is to create a linechart composed of an X axis, a Y axis, and a line.
you can use this simple piece of code:
chartBuilder
.setXAxis()
.setYAxis()
.setLine()
.build();
The code is quite clear to understand – it creates two axes and a line, it consists of invoking functions that display geometric objects. Order of invoking does not matter here. What if you wanted to add two more lines and a grid?
chartBuilder
.setXAxis()
.setYAxis()
.setGrid()
.setLine()
.setLine()
.setLine()
.build();
Why do I find this example important? Because I suppose anyone can understand this code, what opens a way to make a code collaborative for people of different specializations.
It is also worth to mention, that there is an extra work for engineers to handle things that happen under each of the function (e.g setLine, setGrid) and expose them in a comprehensible way.
This example is one of many Design Patterns -- typical solutions to common problems
in software design. You can learn more about them here https://refactoring.guru/design-patterns
2.2. Adapter pattern
Consider a scenario where there is an old library that uses XML to store data, but your new application requires JSON data. You can use adapter pattern to convert the XML data to JSON before passing it to your application. The aim of adapter pattern is to allow two incompatible interfaces to work together.
Here is a short code snippets, where an adapter is handled by single function XMLToJSONAdapter.
After XML Data is fetched, it is passed as argument to XMLToJSONAdapter function (which is in charge of switching XML formant into JSON). The JSON formatted data is eventually passed to constant JSONData.
const XMLData = fetchData();
const JSONData = getJSONData(XMLToJSONAdapter(XMLData));
I believe that entry level to understand this code is pretty low.
Another, a bit more complicated example of adapter pattern in Object oriented way, making pretty much same work under getJsonData function:
class XmlToJsonAdapter {
constructor(xmlData) {
this.xmlData = xmlData;
}
getJsonData() {
// ... convert XML to JSON ...
return jsonData;
}
}
2.3. Facade pattern
Another design example is Facade patterns, the aim of which is to provide a simplified interface to a complex subsystem. This pattern defines a higher-level interface that makes the subsystem easier to use. In the code below the difficult part of coding, which is connecting to external system or any additional computation would be hidden in Weather class (getTemperature, getHumidity, getWindSpeed functions).
getCurrentWeather() {
const temperature = Weather.getTemperature();
const humidity = Weather.getHumidity();
const windSpeed = Weather.getWindSpeed();
return {
temperature,
humidity,
windSpeed,
};
}
getCurrentWeather function returns temperature, humidity and wind speed. Person who reads the code sees where those indicators come from and how they are returned together as a cohesive weather state. Getting the weather state, which we consider for this example more complicated is moved to getTemperature, getHumidity, getWindSpeed functions.
2.4. Example connecting different parts of code and multiple Design Patterns
Let us say there is an existing system that is no longer developed but some of its features will be utilized by a new system, that is being built.
In such case, a good solution might be creating a new piece of software that will translate between those systems and do not let the old system make direct impact on the new one. Such approach is called Anti-corruption layer.
Examples of usage of ACL in real life:
Core Banking and Mobile Banking: The ACL could be used to transform complex financial data from the core banking system into a simplified format suitable for mobile devices, ensuring a smooth user experience.
IoT Devices and Supply Chain Management: The ACL could be used to transform raw IoT data into meaningful insights that can be integrated into the supply chain management system, improving visibility and efficiency.
Anti-corruption layer is also a Design Pattern, but in example presented here it will operate on two different systems and consists of multiple parts of code.
The diagram below shows an overview for two exemplary systems connected with Anti-corruption layer. On the right hand side, there is a system that we want to connect with, in the middle there is ACL and on the left hand side there is your brand new system which you are proud of. In Anti-corruption layer you can see a Facade which role is to hide complexity of the system you want to connect to. There are also two Adapters, which role is to translate data format of previous system to the one that that will be currently used.
Creating a diagram, that connects different parts is a good idea, when you want to make your project more understandable by both, technical and non-technical people. A diagram might be treated as a map that we use to navigate in codebase. Thanks to its visual nature, anyone can see how things in your codebase are connected and if more detailed understanding is needed, people may read the code by themselves.
3. Infrastructure as code
This chapter will show yet another approach to utilize the code. I find it important to notice, because Infrastructure as Code is closer to physical world, as is design in its roots.
Infrastructure as Code is a practice that involves managing and provisioning infrastructure through code. This means that instead of manually configuring servers or networks they can be defined in configuration files. These files can be versioned, tested, and deployed just like any other software code.
Below diagram visualizes the process. User writes code, which is then version-controlled and mapped through Automation API or Server directly into an infrastructure.
As mentioned before, IaC is closer to the physical world than regular programming. The idea of defining and building products through precise instructions, rather than manual processes is also common for manufacturing industry and has a strong connection with design.
Key similarities between IaC and Manufacturing:
Blueprint-Driven Approach:
IaC: Engineers write code (blueprints) to define the desired infrastructure.
Manufacturing: Engineers create blueprints (CAD models, schematics) to design physical products.
Automation and Repeatability:
IaC: Automation tools execute the code to provision and configure infrastructure, ensuring consistency.
Manufacturing: Automated machinery follows precise instructions to produce identical products.
Version Control and Traceability:
IaC: Code is version-controlled, allowing tracking of changes, collaboration, and rollback.
Manufacturing: Product designs and manufacturing processes are version-controlled to maintain quality and consistency.
Continuous Improvement:
IaC: Infrastructure code is continuously refined and optimized to improve performance and reliability.
Manufacturing: Manufacturing processes are constantly analyzed and improved to increase efficiency and reduce costs.
At the end, I would like to show an example snippet of IaC, to show that some basic configurations may be clear for non-technical people. Although the configuration describes technical infrastructure, so it requires some vocabulary. Moreover the code describing more advanced infrastructure might be much more complex.
Bellow snippet defines a reusable configuration for a Google Compute Engine virtual machine with a Debian 11 boot disk. By providing values for the deployment_identifier variable, you can create multiple virtual machines with unique names based on this configuration. You can also modify the machine_type and network configuration to suit specific needs.
variable "machine_type" {
type = string
default = "n1-standard-1"
}
variable "zone" {
type = string
default = "us-central1-a"
}
variable "deployment_identifier" {
description = "The unique name for your instance"
type = string
}
resource "google_compute_instance" "default" {
name = "vm-${var.deployment_identifier}"
machine_type = var.machine_type
zone = var.zone
boot_disk {
device_name = "boot"
auto_delete = true
initialize_params {
image = "debian-cloud/debian-11"
}
}
network_interface {
network = "default"
access_config {
// Ephemeral IP
}
}
}
Source: https://cloud.google.com/service-catalog/docs/terraform-configuration
4. Summary
Code is a flexible tool, based on the latin alphabet (and special characters), which allows to articulate processes of varying complexity and give instructions that are understandable both to humans and machines, thereby making technology more inclusive. Beside a difficulties that comes with writing software, code has traits that make it comprehensible.
Bibliography
Design: The Whole Story by Elizabeth Wilhide
Domain-Driven Design: Tackling Complexity in the Heart of Software by Eric Evans
https://www.developerdotstar.com/mag/articles/reeves_design.html
Top comments (0)