Let us now focus on an aspect which forms the main topic for the rest of this book–Architectural Quality Attributes.
In a previous section, we discussed how an architecture balances and optimizes stakeholder requirements. We also saw some examples of contradicting stakeholder requirements, which an architect seeks to balance, by choosing an architecture which does the necessary trade-offs.
The term quality attribute has been used to loosely define some of these aspects that an architecture makes trade-offs for. It is now the time to formally define what an Architectural Quality Attribute is:
"A quality attribute is a measurable and testable property of a system which can be used to evaluate the performance of a system within its prescribed environment with respect to its non-functional aspects"
There are a number of aspects that fit this general definition of an architectural quality attribute. However, for the rest of this book, we will be focusing on the following quality attributes:
Many studies show that about 80% of the cost of a typical software system occurs after the initial development and deployment. This shows how important modifiability is to a system's initial architecture.
Modifiability can be defined as the ease with which changes can be made to a system, and the flexibility with which the system adjusts to the changes. It is an important quality attribute, as almost every software system changes over its lifetime—to fix issues, for adding new features, for performance improvements, and so on.
From an architect's perspective, the interest in modifiability is about the following:
Now, what kind of changes are we talking about here? Is it changes to code, changes to deployment, or changes to the entire architecture?
The answer is: it can be at any level.
From an architecture perspective, these changes can generally be captured at the following three levels:
Non-local changes are more difficult to make than local changes, require careful analysis, and wherever possible, integration tests to avoid code regressions.
These changes are the riskiest, and also the costliest, in terms of resources, time and money. An architect needs to carefully vet the different scenarios that may arise from the change, and get his/her team to model them via integration tests. Mocks can be very useful in these kinds of large-scale changes.
The following table shows the relationship between Cost and Risk for the different levels of system modifiability:
Level |
Cost |
Risk |
---|---|---|
Local |
Low |
Low |
Non-local |
Medium |
Medium |
Global |
High |
High |
Modifiability at the code level is also directly related to its readability:
"The more readable a code is, the more modifiable it is. Modifiability of a code goes down in proportion to its readability."
The modifiability aspect is also related to the maintainability of the code. A code module which has its elements very tightly coupled would yield to modification much less than a module which has a loosely coupled elements—this is the Coupling aspect of modifiability.
Similarly, a class or module which does not define its role and responsibilities clearly would be more difficult to modify than another one which has well-defined responsibility and functionality. This aspect is called Cohesion of a software module.
The following table shows the relation between Cohesion, Coupling, and Modifiability for an imaginary module A. Assume that the coupling is from this module to another module B:
Cohesion |
Coupling |
Modifiability |
---|---|---|
Low |
High |
Low |
Low |
Low |
Medium |
High |
High |
Medium |
High |
Low |
High |
It is pretty clear from the preceding table that having higher Cohesion and lower Coupling is the best scenario for the modifiability of a code module.
Other factors that affect modifiability are as follows:
Testability refers to how much a software system is amenable to demonstrating its faults through testing. Testability can also be thought of as how much a software system hides its faults from end users and system integration tests—the more testable a system is, the less it is able to hide its faults.
Testability is also related to how predictable a software system's behavior is. The more predictable a system, the more it allows for repeatable tests, and for developing standard test suites based on a set of input data or criteria. Unpredictable systems are much less amenable to any kind of testing, or, in extreme case, not testable at all.
In software testing, you try to control a system's behavior by, typically, sending it a set of known inputs, and then observing the system for a set of known outputs. Both of these combine to form a testcase. A test suite or test harness, typically, consists of many such test cases.
Test assertions are the techniques that are used to fail a test case when the output of the element under the test does not match the expected output for the given input. These assertions are usually manually coded at specific steps in the test execution stage to check the data values at different steps of the testcase:
The preceding diagram shows an example of a representative flowchart for a testable function "f" for a sample input "X" with expected output "Y".
In order to recreate the session or state at the time of a failure, the record/playback strategy is often used. This employs specialized software (such as Selenium), which records all user actions that led to a specific fault, and saves it as a testcase. The test is reproduced by replaying the testcase using the same software which tries to simulate the same testcase; this is done by repeating the same set and order of UI actions.
Testability is also related to the complexity of code in a way very similar to modifiability. A system becomes more testable when parts of it can be isolated and made to work independent of the rest of the system. In other words, a system with low coupling is more testable than a system with high coupling.
Another aspect of testability, which is related to the predictability mentioned earlier, is to reduce non-determinism. When writing test suites, we need to isolate the elements that are to be tested from other parts of the system which have a tendency to behave unpredictably so that the tested element's behavior becomes predictable.
An example is a multi-threaded system, which responds to events raised in other parts of the system. The entire system is probably quite unpredictable, and not amenable to repeated testing. Instead, one needs to separate the events subsystem, and possibly, mock its behavior so that those inputs can be controlled, and the subsystem which receives the events becomes predictable and hence, testable.
The following schematic diagram explains the relationship between the testability and predictability of a system to the Coupling and Cohesion between its components:
Modern-day web applications are all about scaling up. If you are part of any modern-day software organization, it is very likely that you have heard about or worked on an application that is written for the cloud, which is able to scale up elastically on demand.
Scalability of a system is its capacity to accommodate increasing workload on demand while keeping its performance within acceptable limits.
Scalability in the context of a software system, typically, falls into two categories, which are as follows:
Performance of a system is related to its scalability. Performance of a system can be defined as follows:
"Performance of a computer system is the amount of work accomplished by a system using a given unit of computing resource. Higher the work/unit ratio, higher the performance."
The unit of computing resource to measure performance can be one of the following:
Performance is closely tied to scalability, especially, vertical scalability. A system that has excellent performance with respect to its memory management would easily scale up vertically by adding more RAM.
Similarly, a system that has multi-threaded workload characteristics and is written optimally for a multicore CPU, would scale up by adding more CPU cores.
Horizontal scalability is thought of as having no direct connection to the performance of a system within its own compute node. However, if a system is written in a way that it doesn't utilize the network effectively, thereby producing network latency issues, it may have a problem scaling horizontally effectively, as the time spent on network latency would offset any gain in scalability obtained by distributing the work.
Some dynamic programming languages such as Python have built-in scalability issues when it comes to scaling up vertically. For example, the Global Interpreter Lock (GIL) of Python (CPython) prevents it from making full use of the available CPU cores for computing by multiple threads.
Availability refers to the property of readiness of a software system to carry out its operations when the need arises.
Availability of a system is closely related to its reliability. The more reliable a system is, the more available it is.
Another factor which modifies availability is the ability of a system to recover from faults. A system may be very reliable, but if the system is unable to recover either from complete or partial failures of its subsystems, then it may not be able to guarantee availability. This aspect is called recovery.
The availability of a system can be defined as follows:
"Availability of a system is the degree to which the system is in a fully operable state to carry out its functionality when it is called or invoked at random."
Mathematically, this can be expressed as follows:
Availability = MTBF/(MTBF + MTTR)
Take a look at the following terms used in the preceding formula:
This is often called the mission capable rate of a system.
Techniques for Availability are closely tied to recovery techniques. This is due to the fact that a system can never be 100% available. Instead, one needs to plan for faults and strategies to recover from faults, which directly determines the availability. These techniques can be classified as follows:
Availability of a system is closely tied to the consistency of its data via the CAP theorem which places a theoretical limit on the trade-offs a system can make with respect to consistency versus availability in the event of a network partition. The CAP theorem states that a system can choose between being consistent or being available—typically leading to two broad types of systems, namely, CP (consistent and tolerant to network failures) and AP (available and tolerant to network failures).
Availability is also tied to the system's scalability tactics, performance metrics, and its security. For example, a system that is highly horizontally scalable would have a very high availability, since it allows the load balancer to determine inactive nodes and take them out of the configuration pretty quickly.
A system which, instead, tries to scale up may have to monitor its performance metrics carefully. The system may have availability issues even when the node on which the system is fully available if the software processes are squeezed for system resources, such as CPU time or memory. This is where performance measurements become critical, and the system's load factor needs to be monitored and optimized.
With the increasing popularity of web applications and distributed computing, security is also an aspect that affects availability. It is possible for a malicious hacker to launch remote denial of service attacks on your servers, and if the system is not made foolproof against such attacks, it can lead to a condition where the system becomes unavailable or only partially available.
Security, in the software domain, can be defined as the degree of ability of a system to avoid damage to its data and logic from unauthenticated access, while continuing to provide services to other systems and roles that are properly authenticated.
A security crisis or attack occurs when a system is intentionally compromised with a view to gaining illegal access to it in order to compromise its services, copy, or modify its data, or deny access to its legitimate users.
In modern software systems, the users are tied to specific roles which have exclusive rights to different parts of the system. For example, a typical web application with a database may define the following roles:
This way of allocating system control via user roles is called access control. Access control works by associating a user role with certain system privileges, thereby decoupling the actual user login from the rights granted by these privileges.
This principle is the Authorization technique of security.
Another aspect of security is with respect to transactions where each person must validate the actual identity of the other. Public key cryptography, message signing, and so on are common techniques used here. For example, when you sign an e-mail with your GPG or PGP key, you are validating yourself—The sender of this message is really me, Mr. A—to your friend Mr. B on the other side of the e-mail. This principle is the Authentication technique of security.
The other aspects of security are as follows:
Deployability is one of those quality attributes which is not fundamental to the software. However, in this book, we are interested in this aspect, because it plays a critical role in many aspects of the ecosystem in the Python programming language and its usefulness to the programmer.
Deployability is the degree of ease with which software can be taken from the development to the production environment. It is more of a function of the technical environment, module structures, and programming runtime/languages used in building a system, and has nothing to do with the actual logic or code of the system.
The following are some factors that determine deployability:
3.15.231.194