Reliable Programming
Reliable Programming
• Customers have to be confident that your product will not crash or lose
information, and users have to be able to learn to use the software
quickly and without mistakes.
• There are three simple techniques for reliability improvement that can
be applied in any software company.
• Fault avoidance You should program in such a way that you avoid introducing
faults into your program.
• Input validation You should define the expected format for user inputs and
validate that all inputs conform to that format.
• A static relationship is one that is stable and does not depend on program
execution.
• Whether or not one component is part of another component is a static relationship.
• Dynamic relationships, which change over time, are more complex than
static relationships.
• An example of a dynamic relationship is the ‘calls’ relationship between functions.
• Reading complexity
This reflects how hard it is to read and understand the program.
• Structural complexity
This reflects the number and types of relationship between the
structures (classes, objects, methods or functions) in your program.
• Data complexity
This reflects the representations of data used and relationships between
the data elements in your program.
• Decision complexity
This reflects the complexity of the decisions in your program
Structural complexity
• Functions should do one thing and one thing only
• Functions should never have side-effects
• Every class should have a single responsibility
• Minimize the depth of inheritance hierarchies
• Avoid multiple inheritance
• Avoid threads (parallelism) unless absolutely necessary
Data complexity
• Define interfaces for all abstractions
• Define abstract data types
• Avoid using floating-point numbers
• Never use data aliases
Conditional complexity
• Avoid deeply nested conditional statements
• Avoid complex conditional expressions
• They will therefore be less complex and easier to understand and change.
• However, if you add a print method, you are associating another data type (a
report) with the class. Another reason for changing this class might then be to
change the format of the printed report.
• Deeply nested conditional (if) statements are used when you need to
identify which of a possible set of choices is to be made.
Program 8.2
Using guards to
make a selection
• The problem with deep inheritance is that if you want to make changes to a
class, you have to look at all of its superclasses to see where it is best to
make the change.
• You also have to look at all of the related subclasses to check that the
change does not have unwanted consequences. It’s easy to make mistakes
when you are doing this analysis and introduce faults into your program.
• Definition
• A general reusable solution to a commonly-occurring problem within a
given context in software design.
• Separation of concerns
• This means that each abstraction in the program (class, method, etc.) should
address a separate concern and that all aspects of that concern should be
covered there. For example, if authentication is a concern in your program, then
everything to do with authentication should be in one place, rather than
distributed throughout your code.
• Structural patterns
• These are concerned with class and object composition. Structural design
patterns are a description of how classes and objects may be combined to create
larger structures.
• Behavioural patterns
• These are concerned with class and object communication. They show how
objects interact by exchanging messages, the activities in a process and how
these are distributed amongst the participating objects.
Description
This pattern separates the display of an object from the object itself. There may be
multiple displays associated with the object. When one display is changed, all
others are notified and take action to update themselves.
Problem
Many applications present multiple views (displays) of the same data with the
requirement that all views must be updated when any one view is changed. You
may also wish to add new views without the object, whose state is being
displayed, knowing about the new view or how the information is presented.
Solution
The state to be displayed (sometimes called the Model) is maintained in a Subject
class that includes methods to add and remove observers and to get and set the
state of the Model. An observer is created for each display and registers with the
Subject. When an observer uses the set method to change the state, the Subject
notifies all other Observers. They then use the Subject’s getState( ) method to
update their local copy of the state and so change their display. Adding a new
display simply involves notifying the Subject that a new display has been created.
Implementation
This pattern is implemented using abstract and concrete classes. The abstract
Subject class includes methods to register and deregister observers and to notify
all observers that a change has been made. The abstract Observer class
includes a method to update the local state of each observer. Each Observer
subclass implements these methods and is responsible for managing its own
display. When notifications of a change are received, the Observer subclasses
access the model using the getState( ) method to retrieve the changed
information.
Things to consider
The Subject does not know how the Model is displayed so cannot organize its
data to optimize the display performance. If a display update fails, the Subject
does not know that the update has been unsuccessful.
• the consequences and trade-offs of using the pattern and other issues that you
should consider.
• It also makes it easier to change, which means that you reduce the
chances of making mistakes when you introduce new features.
• Martin Fowler, a refactoring pioneer, suggests that the starting point for
refactoring should be to identify code ‘smells’.
• Code smells are indicators in the code that there might be a deeper
problem.
• For example, very large classes may indicate that the class is trying to do too
much. This probably means that its structural complexity is high.
Large classes
Large classes may mean that the single responsibility principle is being violated.
Break down large classes into easier-to-understand, smaller classes.
Long methods/functions
Long methods or functions may indicate that the function is doing more than one thing.
Split into smaller, more specific functions or methods.
Duplicated code
Duplicated code may mean that when changes are needed, these have to be made
everywhere the code is duplicated. Rewrite to create a single instance of the
duplicated code that is used as required
Meaningless names
Meaningless names are a sign of programmer haste. They make the code harder to
understand. Replace with meaningful names and check for other shortcuts that the
programmer may have taken.
Unused code
This simply increases the reading complexity of the code. Delete it even if it has been
commented out. If you find you need it later, you should be able to retrieve it from the
code management system.
Reading complexity
You can rename variable, function and class names throughout your program to
make their purpose more obvious.
Structural complexity
You can break long classes or functions into shorter units that are likely to be
more cohesive than the original large class.
Data complexity
You can simplify data by changing your database schema or reducing its
complexity. For example, you can merge related tables in your database to
remove duplicated data held in these tables.
Decision complexity
You can replace a series of deeply nested if-then-else statements with guard
clauses, as I explained earlier in this chapter.
• User input errors are the most common cause of database pollution.
• You should define rules for every type of input field and you should
include code that applies these rules to check the field’s validity.
• If it does not conform to the rules, the input should be rejected.
• If you use rules like these, it becomes impossible to input very long
strings that might lead to buffer overflow, or to embed SQL commands in
a name field.
Explicit comparisons
You can define a list of allowed values and possible abbreviations and check
inputs against this list. For example, if a month is expected, you can check this
against a list of all months and recognised abbreviations.
Regular expressions
You can use regular expressions to define a pattern that the input should match
and reject inputs that do not match that pattern.
• A search can be defined as a pattern and all items matching that pattern
are returned. For example, the following Unix command will list all the
JPEG files in a directory:
• ls | grep ..*\.jpg$
• A single dot means ‘match any character’ and \* means zero or more
repetitions of the previous character. Therefore ..\* means ‘one or more
characters’. The file prefix is .jpg and the $ character means that it must
occur at the end of a line.
namex = r"^[a-zA-Z][a-zA-Z-']{1,39}$"
if re.match (namex, s):
if re.search ("'.*'", s) or re.search ("--", s):
return False
else:
return True
else:
return False
• Number checking is used with numeric inputs to check that these are
not too large or small and that they are sensible values for the type of
input.
• For example, if the user is expected to input their height in meters then you
should expect a value between 0.6m (a very small adult) and 2.6m (a very tall
adult).
• As well as checking the ranges of inputs, you may also perform checks
on these inputs to ensure that these represent sensible values.
• These protect your system from accidental input errors and may also
stop intruders who have gained access using a legitimate user’s
credentials from seriously damaging their account.
• Software is so complex that, irrespective of how much effort you put into
fault avoidance, you will make mistakes. You will introduce faults into
your program that will sometimes cause it to fail.
• Whatever the cause, you have to plan for failure and make provisions in
your software for that failure to be as graceful as possible.
• Program exceptions
• The program enters a state where normal continuation is impossible. If these
exceptions are not handled, then control is transferred to the run-time system which
halts execution. For example, if a request is made to open a file that does not exist
then an IOexception has occurred.
• Timing failures
• Interacting components fail to respond on time or where the responses of
concurrently-executing components are not properly synchronized. For example, if
service S1 depends on service S2 and S2 does not respond to a request, then S1 will
fail.
• The user should be able to recover the work that they’ve done before
the failure occurred;
• You should always ‘fail secure’ so that confidential data is not left in a
state where an attacker can gain access to it.
• Activity logging
• You keep a log of what the user has done and provide a way to replay that
against their data. You don’t need to keep a complete session record, simply a
list of actions since the last time the data was saved to persistent store.
• Auto-save
• You automatically save the user’s data at set intervals - say every 5 minutes.
This means that, in the event of a failure, you can restore the saved data with
the loss of only a small amount of work.
• Usually, you don’t have to save all of the data but simply save the changes that
have been made since the last explicit save.
• If your software uses external services, you have no control over these
services and the only information that you have on service failure is
whatever is provided in the service’s API.
• When you are calling an external service, you should always check that
the return code of the called service indicates that it has operated
successfully.
• You should, also, if possible, check the validity of the result of the
service call as you cannot be certain that the external service has
carried out its computation correctly.
• The most important quality attributes for most software products are reliability,
security, availability, usability, responsiveness and maintainability.
• To avoid introducing faults into your program, you should use programming practices
that reduce the probability that you will make mistakes.
• You should always aim to minimize complexity in your programs. Complexity makes
programs harder to understand. It increases the chances of programmer errors and
makes the program more difficult to change.
• Design patterns are tried and tested solutions to commonly occurring problems. Using
patterns is an effective way of reducing program complexity.
• Input validation involves checking all user inputs to ensure that they are in the format
that is expected by your program. Input validation helps avoid the introduction of
malicious code into your system and traps user errors that can pollute your database.
• You should check that numbers have sensible values depending on the type
of input expected. You should also check number sequences for feasibility.
• You should assume that your program may fail and to manage these failures
so that they have minimal impact on the user.
• You should log user updates and maintain user data snapshots as your
program executes. In the event of a failure, you can use these to recover the
work that the user has done. You should also include ways of recognizing
and recovering from external service failures.