Mid Year Lookback - Design Patterns in Software

I just spent the first half of 2021 working on various finance applications despite being a full time 3rd year computer science student.

Mid Year Lookback - Design Patterns in Software

I just spent the first half of 2021 working on various finance applications despite being a full time 3rd year computer science student. And from that experience, I gained insights about design; not the design of user experience, but the architecture patterns and different styles of building applications.

What is bad design to me

Bad design inhibits an engineer's ability to produce good software for its intended business purposes. If the ability to create new features is handicapped by a monolith architecture, then it is bad design. If performance is impacted due to fragmented data sources arose from Micro Services, then it is bad design.

There exist an umbrella term for many sins committed in software engineering: technical debt. This single phrase applies from high-level architecture to low-level line-by-line details. These two words describe anything from the blasphemy of hard-coding variables to funky obtuse hacks.

So what is technical debt? Its profound meaning strongly resembles its literal definition. Instead of paying with cold hard cash, the loan, payback and interest come in the form of development effort and time. If you take a shortcut today, you have to pay it back with interest in the future.

Just like a financial loan, technical debt can be good when used properly. Software engineers can temporarily work around blockers. Software engineers can install a prototype solution while a better one is in the works.

That as it may be, we typically never address these temporary hacks before they rot and start giving code smells all over the place. Yes, just like a financial loan, you have to pay it back within a reasonable timeframe before it morphs into a rolling snowball and descends like an avalanche.

If you need an example, follow these steps
1. Open your 3rd latest project
2. Search for all references of
// TODO :
Now, how many of these flags did you actually address in the end?

What is good design to me

Good design, per contra, enhances the ability of an engineer to produce good software for its intended business purposes. As a software engineer, I am inherently lazy. I want to cover all of the current and potential future use cases for  business goals with minimal effort and allow my teammates and future-self  to understand my current thought process as much as possible.

To that end, we can follow these trains of thought (on top of the usual SOLID stuff)

  1. Abstract similar functioning code and making one extendable version = less code have to be written and easy to plug in new functionality across modules
  2. Single source of truth = able to control multiple parts of the application without much effort

Consistency. The underlying theme of these points is consistency. It is much easy to fix problems when all of your application is following a similar pattern. These problems would be easy to spot, fixes can be applied globally and context switching can be done with less effort.

1. Abstraction

When we repeat code, we are not just repeating it once in development. Quality Assurance  has to rest the duplicated code since it is not within the existing coverage. When a defect is flagged, we have to change the bug at two distinct segments and test each segment separately. By duplicating code snippets, we could easily snowball the problem.

By creating an abstraction for commonly used functionality, we only have to create the snippet once, let it be tested only once. Any changes could be done on that abstraction and the implementation automatically gets the benefits.

Imagine writing a simple event listener for a button to change a route.

header.homeButton.addEventListener('clicked', () => {
  history.push('/home')
})

header.settingsBtn.addEventListener('clicked', () => {
  history.push('/settings')
})

header.cartButton.addEventListener('clicked', () => {
  history.push('/cart')
})
This is an overly simplified example.

Now, let's say we also want to add a logger without modifying the history object. We would have to add a logger to every handler, resulting in 3 additional lines of code. Depending on the use case, we might want to abstract out the route changing logic. This way, plugging in new features is as simple as changing the abstraction.

const changeRoute = (route) => { 
  history.push(route)
  logger.record(route, Date.now())
}

header.homeButton.addEventListener('clicked', () => {
  changeRoute('/home')
})

header.settingsBtn.addEventListener('clicked', () => {
  changeRoute('/settings')
})

header.cartButton.addEventListener('clicked', () => {
  changeRoute('/cart')
})
Adding functionality at the abstraction

Additionally, when we have to swap out the routing logic, we would change it at the abstraction once. The rest of our components retains the same behavior within their units.

2. Single source of truth

I encountered this concept while reading up on the Flux Architecture. During this F8 demo, engineers at Facebook utilized this very pattern. By handing control of the state to a global centralized source, individual UI widgets only have to worry about looking pretty. This reduces their internal complexity while creating a consistent communication model between data and view.

One of my favorite libraries in recent months is Redux. When used together with React, Redux allows us to create very reactive software. By using Redux as a global state, all of the state changes can be recorded, stored, time travelled and debugged at a single location. The reducers essentially act as modifiers to the state, allowing us to poke at parts of the global state. UI components in React only have to react to modifications in the tree which they are interested in.

Why aim to create good design

As much as technology drives business, business drives technology. I mean, we need to feed engineers after all. It would be destructive to a business if they have to rewrite their software once every few years. With the increasing adoption of agile methodologies across sectors, software should help to nudge business by providing incremental upgrades. This means we cannot afford to rewrite entire systems once every few years (contrary to the jokes university students share with ourselves). And to create this, good design is the foundation. If we establish a strong foundation of good design, we can innovate much quicker later in the lifecycle of the product.


Cover Photo by Mikhail Nilov from Pexels