paint-brush
What Is Programming's Worst Enemy?by@davidmavrodiev
546 reads
546 reads

What Is Programming's Worst Enemy?

by David M.February 17th, 2024
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

It is a written set of instructions (programs) that a machine will execute. So, we can say that the programmer is the “creator” of the code, but who is the “user” of it? Maybe the machine?
featured image - What Is Programming's Worst Enemy?
David M. HackerNoon profile picture

Image created by AI tool DALL·E 3 — the author has the provenance and copyright.


What is programming? It is a written set of instructions (programs) that a machine will execute. So, we can say that the programmer is the “creator” of the code, but who is the “user” of it? Maybe the machine? No, it is not the machine.


The machine does not understand modern high-level programming languages; in standard cases, programs written in such languages are “translated” to low-level instructions using compilers. The “users” of the code are the other programmers. Programming is a group sport.


Alone, you can write simple or even complex programs, but if you want to create cutting-edge enterprise-level software that you can reliably develop and use, then you need more programmers who will work together in teams.


So, in most cases, many programmers organized in teams actively modify the code throughout the development cycle. Because of that, the modern programming approach is to write your code more for your colleagues, and not so much for the machine. This shift in perspective makes programming more about writing understandable code that should work as expected.


The code should be easy to understand for humans. What is the biggest enemy of understanding? It is complexity. Keeping that in mind, let’s examine some of the famous programming principles/rules that promise to deal with complexity, as follows:


Programming best practices and principles are guidelines and rules that help developers write code that is efficient, readable, maintainable, and scalable. These practices are essential for individual developers and teams to produce high-quality software.

Here are some of the most widely recognized best practices and principles in programming:


  • Write Readable Code: Use meaningful variable and function names. Follow a consistent coding style and conventions. Comment your code where necessary to explain “why” something is done, not “what” is done, as the latter should be evident from the code itself.


  • Keep It Simple (KISS): Aim for simplicity in your designs. Avoid over-engineering and implementing features that are not needed.


  • Don’t Repeat Yourself (DRY): Avoid duplication of code. Use functions, classes, and modules to encapsulate reusable logic.


  • Refactor Regularly: Continuously improve the codebase by refactoring old or inefficient code.


  • Use Design Patterns Wisely: Design patterns provide solutions to common problems. Understand and apply them where appropriate, but don’t force their use.


  • Document Your Code and Use: Write documentation for your code and how to use your software, which is crucial for users and future developers.


  • SOLID Principles:
    • Single Responsibility Principle (SRP): A class should have one, and only one, reason to change.


    • Open/Closed Principle (OCP): Software entities should be open for extension but closed for modification.


    • Liskov Substitution Principle (LSP): Objects in a program should be replaceable with instances of their subtypes without altering the correctness of the program.


    • Interface Segregation Principle (ISP): Many client-specific interfaces are better than one general-purpose interface.


    • Dependency Inversion Principle (DIP): Depend upon abstractions, not concretions.


  • Don’t Make Me Think: Make the code as intuitive as possible, reducing the effort needed to understand it.


  • YAGNI (You Ain’t Gonna Need It): Avoid adding functionality until it is necessary.


These principles sound good on paper, and if used wisely, they can indeed slow down the rate at which complexity grows in your code. Remember, complexity is always your master. In software development, as the project grows and more and more features are added, you can slow the complexity but never stop it.


Most developers learn that the hard way. Based on my practice, such principles/tips/rules should be applied case by case and based on the context.


I have seen many senior programmers applying such practices regardless of the context and case, and the result is adding more complexity to an already complex codebase.


For example, let’s take a closer look at our unnecessarily complicated calculator application.

OverEngineeredCalculator/
│
├── src/
│   ├── main/
│   │   ├── java/
│   │   │   ├── com/
│   │   │   │   ├── overengineeredcalculator/
│   │   │   │   │   ├── calculator/
│   │   │   │   │   │   ├── core/
│   │   │   │   │   │   │   ├── CalculatorEngineInterface.java
│   │   │   │   │   │   │   ├── CalculatorEngineImpl.java
│   │   │   │   │   │   │   ├── OperationStrategyInterface.java
│   │   │   │   │   │   │   ├── OperationFactoryInterface.java
│   │   │   │   │   │   │   ├── OperationFactoryImpl.java
│   │   │   │   │   │   │   ├── CalculatorContextInterface.java
│   │   │   │   │   │   │   ├── CalculatorContextHolder.java
│   │   │   │   │   │   ├── operations/
│   │   │   │   │   │   │   ├── base/
│   │   │   │   │   │   │   │   ├── Operation.java
│   │   │   │   │   │   │   │   ├── OperationImpl.java
│   │   │   │   │   │   │   │   ├── ValidatorInterface.java
│   │   │   │   │   │   │   │   ├── ValidatorImpl.java
│   │   │   │   │   │   │   ├── addition/
│   │   │   │   │   │   │   │   ├── AdditionStrategyInterface.java
│   │   │   │   │   │   │   │   ├── AdditionStrategyImpl.java
│   │   │   │   │   │   │   │   ├── AdditionValidatorInterface.java
│   │   │   │   │   │   │   │   ├── AdditionValidatorImpl.java
│   │   │   │   │   │   │   ├── subtraction/
│   │   │   │   │   │   │   │   ├── SubtractionStrategyInterface.java
│   │   │   │   │   │   │   │   ├── SubtractionStrategyImpl.java
│   │   │   │   │   │   │   │   ├── SubtractionValidatorInterface.java
│   │   │   │   │   │   │   │   ├── SubtractionValidatorImpl.java
│   │   │   │   │   │   │   ├── multiplication/
│   │   │   │   │   │   │   │   ├── MultiplicationStrategyInterface.java
│   │   │   │   │   │   │   │   ├── MultiplicationStrategyImpl.java
│   │   │   │   │   │   │   │   ├── MultiplicationValidatorInterface.java
│   │   │   │   │   │   │   │   ├── MultiplicationValidatorImpl.java
│   │   │   │   │   │   │   ├── division/
│   │   │   │   │   │   │   │   ├── DivisionStrategyInterface.java
│   │   │   │   │   │   │   │   ├── DivisionStrategyImpl.java
│   │   │   │   │   │   │   │   ├── DivisionValidatorInterface.java
│   │   │   │   │   │   │   │   ├── DivisionValidatorImpl.java
│   │   │   │   │   ├── ui/
│   │   │   │   │   │   ├── framework/
│   │   │   │   │   │   │   ├── UIFrameworkInterface.java
│   │   │   │   │   │   │   ├── UIFrameworkImpl.java
│   │   │   │   │   │   │   ├── UIElementFactoryInterface.java
│   │   │   │   │   │   │   ├── UIElementFactoryImpl.java
│   │   │   │   │   │   ├── console/
│   │   │   │   │   │   │   ├── ConsoleUIInterface.java
│   │   │   │   │   │   │   ├── ConsoleUIImpl.java
│   │   │   │   │   │   ├── gui/
│   │   │   │   │   │   │   ├── SwingUIInterface.java
│   │   │   │   │   │   │   ├── SwingUIImpl.java
│   │   │   │   │   │   │   ├── JavaFXUIInterface.java
│   │   │   │   │   │   │   ├── JavaFXUIImpl.java
│   │   │   │   │   │   │   ├── GUIFactoryInterface.java
│   │   │   │   │   │   │   ├── GUIFactoryImpl.java
│   │   │   │   │   ├── utils/
│   │   │   │   │   │   ├── LoggerInterface.java
│   │   │   │   │   │   ├── LoggerImpl.java
│   │   │   │   │   │   ├── ConfigLoaderInterface.java
│   │   │   │   │   │   ├── ConfigLoaderImpl.java
│   │   │   │   │   │   ├── ValidatorInterface.java
│   │   │   │   │   │   ├── ValidatorImpl.java
│   │   │   │   │   ├── exceptions/
│   │   │   │   │   │   ├── CustomExceptionBase.java
│   │   │   │   │   │   ├── InvalidInputException.java
│   │   │   │   │   │   ├── OperationNotSupportedException.java
│   │   │   │   │   ├── integration/
│   │   │   │   │   │   ├── ExternalServiceAdapterInterface.java
│   │   │   │   │   │   ├── ExternalServiceAdapterImpl.java
│   │   │   │   │   │   ├── HistoryRecorderInterface.java
│   │   │   │   │   │   ├── HistoryRecorderImpl.java
│   │   ├── resources/
│   │   │   ├── config.properties
│   │   │   ├── logging.properties
│   ├── test/
│   │   ├── java/
│   │   │   ├── com/
│   │   │   │   ├── overengineeredcalculator/
│   │   │   │   │   ├── calculator/
│   │   │   │   │   │   ├── CalculatorEngineTest.java
│   │   │   │   │   │   ├── OperationFactoryTest.java
│   │   │   │   │   ├── operations/
│   │   │   │   │   │   ├── addition/
│   │   │   │   │   │   │   ├── AdditionStrategyTest.java
│   │   │   │   │   │   ├── subtraction/
│   │   │   │   │   │   │   ├── SubtractionStrategyTest.java
│   │   │   │   │   │   ├── multiplication/
│   │   │   │   │   │   │   ├── MultiplicationStrategyTest.java
│   │   │   │   │   │   ├── division/
│   │   │   │   │   │   │   ├── DivisionStrategyTest.java

This application serves as a prime example of over-engineering, where the application of principles like SRP and ISP, along with an excessive number of interfaces and implementations for simple operations, has led to a bloated structure that obscures the core functionality rather than enhancing it.


To combat complexity effectively:


Keeps the number of project files/modules/packages as small as possible. It’s essential to critically assess the necessity of each component within your project.


Consolidating functionalities where it makes sense can significantly reduce the cognitive load for new developers joining the project.


Think about removing code, not adding more of it.

Embrace refactoring with the aim of simplification. Before adding a new feature or refactoring an existing one, consider the overall impact on the project’s complexity. Sometimes, the best code is the code not written.


Always break the “best practice” if it will make your code simpler.

Context is king. If adhering to a particular principle or practice does not make sense in your specific situation, it’s wise to deviate. The goal should always be to maintain simplicity and clarity, even if it means breaking away from conventional wisdom.


By critically examining the calculator example, we can learn valuable lessons about the pitfalls of over-engineering. Such a mindset will help you eliminate dead code and reward you with a slim and easier-to-understand codebase.


Great software engineers strive for simplicity rather than complexity, understanding that the most elegant solutions are often the most straightforward.


Also published here