Why is software architecture so important and how can Lattix help?
Lattix Blog | Software Architecture, DevOps, Systems Engineering
Sean is Director of Sales and Marketing at Lattix.
It is always hard to work on code that you did not originally build. It is even harder when the original developers who worked on the code are all gone, there is no documentation, and no one is exactly sure how it is implemented. Unfortunately, this is an all-too-common problem for businesses with legacy software applications. These legacy applications typically have been a driver of revenue and profits for the business for a long time, so they are vital. This means that as the business changes, these legacy applications need to be updated as well. But because of architectural drift and erosion, updating and fixing problems is hard and becoming harder all the time.
We write software to make our lives easier; to reduce the complexity and chaos. The need to reduce complexity is at the very center of software development. Unfortunately, we tend to mirror the complexity of the real world in our software and this leads to many problems. As Bruce Schneier in his essay A Plea for Simplicity: You can’t secure what you don’t understand said, “the worst enemy of software security is complexity.” In this blog post, we will talk about the importance of managing complexity and how to reduce complexity in software.
The Importance of Managing Complexity
Fred Brooks, in his paper “No Silver Bullet: Essence of Accidents of Software Engineering”, explains there are two types of issues that introduce complexity, the essential and the accidental properties. Essential properties are the properties that a thing must have to be that thing. Accidental properties are the properties a thing happens to have, but they are not really necessary to make the thing what it is. Accidental properties could also be called incidental, discretionary, or optional properties. Brooks argues that difficulties from accidental properties have been addressed with higher-level languages, the introduction of IDEs, hardware enhancements, etc.
This leaves the problem of essential difficulties, and essential difficulties are harder to solve. You are now interfacing with the complex, chaotic real world where you must identify all the dependencies and edge cases for a problem. As software addresses larger and larger problems, interactions between entities become more complex, thus increasing the difficulty of creating new software solutions. The root of all essential difficulties is complexity.
Conceptual errors (specifications, design, and testing) are much more damaging than syntax errors. C.A.R Hoare, the inventor of the quicksort algorithm, stated in the talk The Emperor’s Old Clothes, “there are two ways of constructing a software design: one way is to make it so simple that there are obviously no deficiencies, and the other is to make it so complicated that there are not obvious deficiencies.” Steve McConnell in Code Complete states that software’s primary technical imperative is to manage complexity. When software projects fail for technical reasons, one reason is often uncontrolled complexity (i.e. things are so complex that no one really knows what is going on). Edsger Dijkstra pointed out that no one’s skull can contain a whole program at once; we should try to organize our programs in such a way that we can safely focus on one part at a time.
Brooks paints a pessimistic picture of the future stating there is no “silver bullet” in either technology or management technique that promises even an order of magnitude improvement in productivity, reliability, or simplicity. While there is no silver bullet, there are things that can be done to reduce complexity.
How to Reduce Complexity
When designs are inefficient and complicated they can be broken down into three categories:
- A complex solution to a simple problem
- A simple, incorrect solution to a complex problem
- An inappropriate, complex solution to a complex problem
As stated above, the most important technical goal is managing complexity. In terms of software architecture, complexity can be reduced by dividing a system into a series of subsystems. The goal is to break a complicated problem into simpler, easier to manage pieces. The less subsystems are dependent on each other the safer it is to focus on one area of complexity at a time. McConnell gives 14 recommendations on how to conquer complexity:
- Dividing a system into subsystems at the architecture level so your brain can focus on a smaller section of the system at one time
- Carefully defining class interfaces so you can ignore the internal workings of the class
- Preserving the abstraction represented by the class interface so your brain doesn’t have to remember arbitrary details
- Avoiding global data, because global data vastly increases the percentage of the code you need to juggle in your brain at any one time
- Avoiding deep inheritance hierarchies because they are intellectually demanding
- Avoiding deep nesting loops and conditionals because they can be replaced by simpler control structures.
- Avoiding gotos because they introduce nonlinearity that has been found to be difficult for most people to follow
- Carefully defining your approach to error handling rather than using an arbitrary number of different error-handling techniques
- Being systematic about the use of built-in exception mechanisms, which can become nonlinear control structures that are about as hard to understand as gotos if not used with discipline
- Not allowing classes to grow into monster classes that amount to whole programs in themselves (Lattix Architect has a number of metrics that can help with this)
- Keeping routines short
- Using clear, self-explanatory variable names so your brain doesn’t have to waste cycles remembering details like “i stands for the account index, and j stands for the customer index, or was it the other way around?”
- Minimizing the number of parameters passed to a routine, or, more importantly, passing only the parameters needed to preserve the routine interface’s abstractions
- Using conventions to spare your brain the challenge of remembering arbitrary accidental differences between different sections of code
The importance of managing complexity can be seen when we start looking at other people’s source code. Depending on the complexity of the software, this can be a daunting task. As stated in Code for the Maintainer, “always code and comment in such a way that if someone a few notches junior picks up the code, they will take pleasure in reading and learning from it.” Lattix Architect has helped hundreds of companies reduce the complexity of their source code by visualizing, optimizing, and controlling their software architecture.
CodeClinic, a provider of innovative solutions for software organizations in safety-critical embedded systems, received the Embedded World Award 2022 in the Safety & Security Category for Traci. This tracing solution is a traceability matrix used to identify gaps in the design and verification of requirements as required by standards such as ISO 26262, DO-178 B/C, IEC 62304, IEC 61508, and more. With Traci, functional safety teams can eliminate the manual error-prone process of conducting tracing analysis.
Out of the box, Android Studio provides one module: the app module. Because of this, most developers write their entire application in this one module. This is fine for small teams and small applications. But, as an application grows, more team members are added and the application becomes more complex, build times can increase – with Gradle builds sometimes taking up to 10-15 minutes – and developer productivity goes down.
One way to solve this problem for complex android applications is to modularize. Modularization means to divide a program into separate sub-programs (or modules) based on features. Each feature will be its own module.
Some advantages of modularization are:
- Ease of incremental builds and deliveries
- Smaller modules are easier to unit test
- Modules can be added, modified, or removed without any impact on another module
- Modules can be reused
- Reused modules can lead to smaller APKs
- Easily pluggable into Instant Apps
- Increased developer productivity, as one person can have sole responsibility of a module
How do we split up a monolithic application into modules?
For business critical applications, discarding the current application and rewriting it from scratch is not an option. Therefore, the only option for critical apps is an iterative approach where you figure out the features that can be separated from the main application and do it in an orderly sequence with the ultimate goal being a set of feature modules.
The key to splitting up a monolithic application into modules is understanding the dependencies between features. This is the software’s architecture. Knowing this will allow you to make decisions on which modules can be split apart and how much work that will entail.
Here are the steps to follow:
- Understand what will be shared. This might seem counterintuitive, but if we didn’t do this we end up duplicating large amounts of code. Many modules share resources such as activities, fragments, and layout files. Looking at the application’s architecture can help identify the utilities that will be shared across the various modules. Looking at the “as-is” architecture can even identify where these utilities have incorrect dependencies.
- Understand how to split what isn’t shared. Modules can end up with definitions of objects that are partial views of the real object. For instance, a shipping module may not need the order history of the customer but does need to know the address where a product will be shipped. By understanding the dependencies from the shipping code, we can understand the actual dependencies that code has on different parts of our domain objects. This can help determine the code needed to interoperate between modules.
- Understand how to split up the data. Splitting up a database means understanding the dependencies within the database. The first thing to think about is foreign key-primary key relationships between tables. But what about stored procedures that access parts of the database? Are there embedded triggers in one of the tables that have dependencies on another table within the database? Again, looking at the architecture of the database (all the dependencies) can help you plan how you will split up the database
- Manage the evolution. Refactoring is never done in isolation. The business still needs new functionality and bug fixes. You need to be able to both add new features and fix bugs at the same time. This is where incremental refactoring can be beneficial. Update a few pieces of the architecture and see how the application is affected by the changes.
There are many benefits to modularizing (refactoring) your Android application: improved maintenance, improved quality, faster builds, and improved developer efficiency. Modularizing your architecture does not have to be a risky, error prone process that requires a massive rewriting the entire application. Instead, it can be accomplished in an orderly and incremental way that minimizes risk while bringing the benefits of modularization.