Preventative Maintenance the solution to avoiding Monster applications

ARCHITECTING -️ January 10, 2023

Creating the monster

You are a company that has legacy projects, these projects have been coded by developers that have moved on. You asked them to create some documentation and over time it got dated.

This project is one massive 9000+ line PHP file that linearly runs. Any small change effects everything and its one of your main systems.

It is a monster and no developer wants to touch it because they are afraid they will lose their job.

What do you do? Every change makes it incrementally harder to make a new one. Code has been neglected and now there is a lot of tech debt.

How would you fix one or multiple flat files with over 9000 lines of code?

The right answer is to rewrite it or refactor it.

In a future post I'll write steps on how to approach that fixing an existing application, but for now how does one avoid it?

It takes a lot of thought to come up with a structure that leaves room for flexibility, and reduces redundancy.

Over the years there have been many advancements in software architecture paradigms, but one fact remains.

If a project is taken care of consistently over time in a similar mode of thinking then it will remain high quality.

Let's unpack that prior statement.

  • Consistently means every now and then it is reviewed,
  • In a similar "mode" of thinking is even if the main owner/editor of a project switches the style remains the same. -> In Javascript if the original owner likes Promises into Thens, if you start introducing await into it the project the breadth of the knowledge needed increases. -> Questions will be asked by future contributors, which one should I use? There is no consistency. Should I use camelCase or PascalCase ?

When you are working on enhancing developer experience you need to ensure that projects remain its own separate entities, and that they speak for themselves.

Changes to the project should not introduce a new way of doing things unless a plan to remove the old is fully removed.

Developers should ask, "Is there a way to do what I want that is already part of the existing code base?" Then if there isn't introduce a new concept, otherwise use the existing way, or make the case to refactor.

“Two of the most important characteristics of good design are discoverability and understanding.” ― Donald A. Norman

Contributors should be able to discover the styles of the project, understand and implement them on their own by just looking at the code.

Sometimes projects are built overtime, new features are added and even though these features are consistent with the style of the code, it still introduced redundancy and a lack of flexibility. This is very normal, and part of the building process, so how do you combat it?

You must think of code structure while developing applications

In his book, "How buildings learn, and what happens after they're built" Stewart Brant discusses a term called preventative maintenance, which means to every now and then look at your building and inspect it for things that can be repaired. He states that if you do preventative maintenance, small problems that lead to big problems are caught early and overall ends up costing less money in the long term.

For example you notice a little water on your ceiling, before the pipe bursts you can fix the pipe, strengthen it, and save yourself water damage, and having to rebuild the whole thing.

What is this SOLID you speak of?

I'm sure you've heard the acronym before, and wonder why is it so important?

SOLID is a set of principles that will help your code remain flexible, uncoupled, and have lower redundancy over time.

Typically as you build an application it starts off with little abstraction. Overtime it needs to be refactored and abstraction needs to be added.

Abstraction is a fancy way of saying interfaces/classes and design patterns.

Design patterns will be discussed in a future post, but they are a vernacular way of building software. Over plenty of refactors developers have come to consensus on how to build things. For instance the "Cache-Aside" design pattern describes adding an ephemeral key value lookup table and how to maintain it. Many companies have used and proven this concept. It uncouples the code, and adds increased availability.

Now let's get to the SOLID acronym.

It was introduced by Uncle Bob and Robert Martin with the goal of making code easier to maintain, and extend without breaking things.

Finally, let's go into detail of SOLID

Single Responsibility:

  • A class should have one, and only one, reason to change.
  • A clean and organized room. Everything should have its own place. A class should only be responsible for one thing, only do one thing. Multiple classes which are smaller and more precise. Very specific names, and responsibilities, as opposed to large classes.
  • Ex. EmployeePayroll, EmployeeLeave, instead of just Employee class.
  • A Controller class, main responsibility get input, give output. -> Main responsibility is the flow of data. -You can isolate its responsibility by doing validation before. Push logic like encrypting elsewhere deeper into the code execution.
  • It's much better to group methods of the same purpose in one file, and keeps things much more organized and readable.

Open/Closed:

  • Entity/Class should be open for extension but closed for modification.
  • You can add new code, instead of changing the existing one.
  • Add a completely new implementation if you like.
  • This is useful because you "Get the system to a point where its very stable, because its tested often and changed never."
  • Ex. Payment options. You have a if/else statement for a method. You would need to modify it every time you want to add a new method. This isn't ideal because if a new PR gets added that creates an error it will effect all the existing processes. Furthermore old tests might be effected. It is better to uncouple implementation methods.
  • Do bug fixes, deploy, then break somewhere else.
  • To combat this you can use the Factory pattern. Create class instances on runtime.
  • You can extend the factory, for example, then everything works.
  • No more modification only extending.

Liskov Substition Principle: To understand LSP, you need to understand the below. Which I will extrapolate what it means after.

"""Let theta(x) be a property provable about objects x of type T. Then theta(y) should be true for objects y of type S where S is a subtype of T."""

-> Derived classes should be able to substitute its parent class without the consumer knowing it.

  • Every class that implements an interface must be able to substitute any reference throughout the code that implements the same interface.
  • If it looks like a duck, quacks like a duck, but needs batteries - you probably have the wrong abstraction.
  • Interfaces for each method. Then implement only the ones you need.
  • Technically a square is a rectangle, but the issue is there are some methods that rectangles need to be able to implement that a square can't. So you shouldn't extend a rectangle when creating the implementation of a square. Why? Well lets say your width of a rectangle is 5, and height is 4. Calculate the area of it. Alright, area = A x B, 5x4 = 20. Now calculate the area of the square, okay, area = A x A, 5*5 = 25, wait that seems wrong..

Interface Segregation Principle:

  • No client should be forced to depend on methods it does not use.
  • Replace fat interfaces with many small, specific interfaces. Prefer object composition over inheritance. Makes things easier.
  • Segregate the interfaces, and tailor them to the individual needs of the client.
  • Changing one method in a class shouldn't affect classes that don't depend on it.
  • If a class is "forced" to implement a method then you're probably doing something wrong.
  • You create a coffee machine interface, it grinds coffee, brews coffee, and adds coffee. Everything coffee.
  • Now you want to re-use that same interface because plenty of other components in your code use it, so you make an Espresso Machine class that implements the CoffeeMachine. Here is the problem Espresso machines don't brew any coffee, so you need to throw an exception saying the method can't be implemented. Which breaks things. Instead use smaller interfaces.

Dependency Inversion Principle:

  • Interface should work the way it works no matter how its implemented. Don't depend on anything concrete, only depend on the lowest common abstraction.
  • Able to change an implementation easily without altering the high level code.
  • If you code things the proper way its really easy to change the background things, like the database you use.
  • You have code that relies on a base database interface instead of the driver itself so when you switch from MySQL to Snowflake its as easy as making a new implementation for snowflake and using all the same existing code.

Summary

Avoid the need to rebuild entire applications, keep track of projects and their states. Build with thought and refactor early. Doing this will reduce costs over the long run.

This has been a letter from a coder.

Best Regards, Tyler Farkas

Before you leave

We know, not another newsletter pitch! - hear us out. Most developer newsletters lack value. Our newsletter will help you build the mental resilience and emotional intelligence you need to succeed in your career. When’s the last time you enjoyed reading a newsletter? Even worse, when’s the last time you actually read one?

We call it Letters, but others call it their favorite newsletter.

Letters
Delivered to developers every end of the Month
Back to Blog

Initializing...