The Object Oriented Software Engineering course covers an introduction to the principles and practices of software design, such as low coupling and high cohesion and a number of design patterns. This lecture covers higher level concepts in software design, often referred to as the system’s architecture. This is the high level metaphor or abstraction that governs the way the software behaves, data flows through the system and provides guidance (as well as constraints) on how new features are implemented.

Software Components

This is the high level metaphor or abstraction that governs the way the software behaves, data flows through the system and provides guidance (as well as constraints) on how new features are implemented. The first thing we need to understand are the building blocks for architecture, which we will refer to as components. You can see a number of definitions of components on this slide, but perhaps the key features are that they: Key features:

  • Discrete purpose, but not entire application
  • well defined documented interfaces for interaction
  • usually a set of smaller scale modules, usch as classes
  • developed maintained and delivered independantly,
  • composed into complete systems

Each of these components will expose a published application programming interface (API). This is the collection of methods or functions, observable state and so on that describes how to interact with the component.

Example

An airline reservation system. This can be viewed as, containing components for managing bookings, taking credit card payments, issuing board passes and organising luggage transfers, amongst other activities. The bookings components could probably be used in isolation from other components (at least on a temporary basis), and the credit card payment processing component might be used by many other systems (such as the airline’s inflight drinks cash register). However, other components, such as the boarding pass issuer will depend on the bookings system (it isn’t a good idea to issue boarding passes to passengers who haven’t paid) and the luggage management system will depend on information gathered by the boarding pass issuing component (to know where the luggage will be sent).

Components are not Object

Components provide us with a suitable abstraction for thinking about how to build complete systems, without having to be concerned with the plumbing of individual lines of code.

This means that they are usually separate development efforts for each component and may be hosted in separate version control repositories, with distinct release cycles and practices.

In addition, communication between components in an architecture is usually independent of the underlying programming language implementation. Communication between RESTful services is usually performed by making http requests, for example. Other interface definition languages exist for components too, such as the Google Sockets specification. Further, this means that components in a system can be implemented in different programming languages. For example, in web applications, the backend might be implemented in Python, whilst the frontend is implemented in JavaScript, leveraging the strengths of the respective languages for specific purposes.

Rather than compiling an entire system, components of an architecture are manually or automatically composed into an assembly. If done automatically, then the development team need to provide a recipe for instantiating and composing the components.

A good example of this is a Docker docker-compose script, which we’ll look at in the lecture on infrastructure as code. In effect, a component oriented architecture is a means of maximising the de-coupling between modules in a system.

Consequently, a reasonable question to ask is:

Why Not Make Every Object in an Architecture a Component?

There are two reasons this is a bad idea:

  1. Mediating component interactions imposes additional communication costs on top of that required for direct object to object interactions. This is because messages must be encoded, transmitted and then decoded via the middleware or other technology. This will have a measurably significant impact on performance.
  2. There are additional development costs associated with exposing an object as a component, since it is a published API. The documentation needs to be maintained, as it should be expected that others will need to understand how to use them. This also makes changes to the component API harder, since many system developments managed by other teams may depend on it.

Of course, the decision about which objects to treat as components will, to a certain extent, be specific to the software system under development.

The role of the component engineer is to decide which are the right objects in a system that should be specified as components that are exposed within the component middleware.

When building a system, a software engineer will work with two different types of component:

  • General purpose components provide common functionality that is intended for reuse in many different applications. For example, the card payment processing component in the reservation system described above could also be used to collect payments for duty-free shopping in an airport or on a plane, pay for meals in the airport restaurant and so on. These components are normally very well documented .
  • Application specific components implement the problem specific functionality and business logic of the application. The component developed to issue boarding passes at check-in for example, will contain business logic specific to the job of converting the airline’s reservation data into a boarding pass. These components are often called the application glue, because they do the job of orchestrating the behaviour of the general purpose components to realise application specific needs

Robert Glass (2001) argued that software engineering is very good at developing and managing general purpose components (such as the card payment processing system), but quite bad at translating components that contain business logic into reusable components.

For example: the reservation component could, in principle, be adapted to other uses, such as booking restaurant tables, travel on buses or cinema tickets. However, there are often subtle technical (and socio-technical) reasons why the business logic of reservation booking in one domain doesn’t work in another context. Being aware of these constraints can help you avoid attempting to inappropriately reuse a component that just doesn’t fit the proposed context.

How Much Functionality to Compile?

Software engineers will also need do decide how much functionality will be provided by a component they develop.

  • Combining lots of functionality in a component can make the maintenance and use of the component very complex, because changing the component may affect the use of the component by many other different systems. The interactions between the different component interfaces will also need to be carefully documented.
  • Conversely, making components very simple means that the interaction between components becomes very complex, because lots of simple component tasks have to be orchestrated when assembling the system to achieve some useful functionality for the application.

One way of mitigating this dilemma is to nest simpler components within more complex ones. This means that some of the functionality of a component is encapsulated (and replaceable) with other sub-components.

This approach can also help minimise the management of interactions between components at any given level of decomposition in the system. However, it isn’t a complete get-out clause, since we still need to reason between the different hierarchy of components and the interactions between them.

Design by Contract

This communication is often described as a contract between the provider and consumer of some functionality provided by methods of functions defined in a component’s published API. These contracts describe the:

  • Benefits of using the interface that are offered by the providing component.
  • Obligations imposed on the component that proposes to use the interface.

Design by contract is a way of developing architectures based on very precisely documented component contracts.

Component Interface Contract

To provide a contract, the documentation for the API can include:

  • Visible component state (component attributes) that is realised by the providing component.
  • *Invariants describing the legal states of the providing component.
  • For each method in the interface:
    • signature, comprising a method identifier, argument identifiers and types, method return type and any exceptions that might be raised as a result of improperly invoking the method.
    • Pre-conditions that describe the required state of the providing component before the method can be invoked and any restrictions on supplied arguments that cannot be expressed in the method signature.
    • Post-conditions that describe the state of the providing component after the method invocation has been completed and constraints on any values returned to the calling component that cannot be expressed by the IDL’s type system.
    • Semantics, including, for example, the correct sequence that methods should be invoked in.
    • The visibility of each method to other components in the system.

The API for a component is usually considered to be published, meaning that the development team have committed to it.

The API therefore needs to be much more stable than internal implementation details, because dependents (i.e. other components) rely on the definitions it contains for their own development.

Definition

 Method signatures are normally expressed formally in an interface definition language. Invariants, pre-conditions and post-conditions and semantics can be documented informally using source code comments, but can also be expressed formally.

Documenting a Component Interface Contract

Documenting a component interface contract involves specifying various aspects of the component’s API to ensure clear communication and understanding of its capabilities and requirements. Here’s what should be included:

  • Visible Component State: Document the attributes of the component that are visible and relevant to the component’s consumers.

  • Invariants: Define the legal states of the component to maintain throughout its lifecycle.

  • Methods Documentation: For each method in the component interface, include:

    • Signature: Specify the method identifier, argument identifiers and types, return type, and exceptions that might be raised.
    • Pre-conditions: Describe the necessary state of the component and any argument restrictions before method invocation.
    • Post-conditions: Detail the state of the component after the method has been executed, including constraints on return values.
    • Semantics: Clarify the correct sequence for invoking methods, if applicable.
    • Visibility: State the visibility of each method to other system components, determining how and where the method can be accessed from.
  • API Stability: The API should be considered published, meaning it is stable and reliable for use by other components within the system. The stability is crucial because dependent components rely on this API for their development and integration.

This structured approach to documentation ensures that all team members and system components can effectively interact with the component without ambiguity, fostering a robust and maintainable software environment.

Formally Defined Contracts

The different parts of a contracts can be expressed more or less formally, depending on the constraints and conventions in the underlying technology, the business domain for the software. For example, method signatures are normally expressed formally in an interface definition language. Invariants, pre-conditions and post-conditions and semantics can be documented informally using source code comments, but can also be expressed formally.

The Java method signature shown here has some formal documentation included. The formal notation usesjavax.validation package of annotations to express valid ranges of values for the parameters of the function and the return type.

Note that informal definitions of contracts using natural language documentation of function behaviour is far more common than formal notations. Although these are less rigorous, they are also often easier for users to understand.

In this case, we could document the functionality in the javadoc comment for the method. However, a benefit of formal definitions of contracts is that we can use automated checking tools to validate that the contracts are being followed. This can be done:

  1. Statically at compile time.
  2. Using test frameworks, such as JUnit.
  3. Within the program logic at runtime.
  4. By the component middleware at runtime.

The Problem of Leaky Abstractions

Leaky abstractions refer to a phenomenon in software design where the abstraction of a component interface does not completely hide its implementation details. This term was coined by Joel Spolsky to describe situations where:

  • Interface Usage: The anticipated way an interface provided by one component will be used by another.
  • Interface Realization: The actual implementation details of the providing interface.

Implications of Leaky Abstractions:

  • Underlying Implementation Leakage: Sometimes, developers need to understand the underlying implementations of the interfaces they use to build more effective systems. For instance, SQL databases like Postgres, MySQL, and SQLite execute queries in SQL but also have implementation-specific features like handling concurrent transactions or retrieving metadata, which can vary significantly.
  • Provider Constraints: The provider’s implementation choices become constrained by assumptions about how their interfaces will be used. An example highlighted by a Google engineer suggests that any observable implementation detail might inadvertently become part of the contract. This includes assumptions about:
    • The order of objects returned by an unordered set.
    • Specific behaviors and structures of database systems which can significantly affect performance.

Practical Impact:

  • Component Coupling: In theory, while interfaces should be separate from implementations, in practice, they often become intertwined. The details of how something is implemented can be crucial to the users, leading to a coupling between interface and implementation.

This coupling illustrates that in practical software development, the distinction between what an interface promises and how it fulfills that promise is not always clear, making it challenging to swap components seamlessly.

Architectural patterns

These are sometimes called architectural styles. Each pattern shows how to organise a set of components to communicate in a particular way to solve an architectural problem.

Benefits of centralising information storage and service provision

There are several reasons why centralising access to information and services could be a good design choice for a distributed software system. These include:

  • The management and organising of data can be maintained (and controlled) in a single, consistent structure.
  • The problem of ensuring consistency between multiple copies of the same data item is avoided.
  • The approach provides a single, globally known, point of access to users.
  • The management of changes to service functionality is can be undertaken in the server rather than in multiple clients.

Summary

A final point to re-state is that architectures (and patterns) are models. That means they are useful ways for thinking about (and communicating) the partition of responsibilities of the components in a system.

There are a couple of consequences to be aware of:

  • First, because architectures are only models, communication about them within a team is important so that a common understanding about how a system functions and how changes should be made is important. A team can facilitate this, for example, by keeping a high level description of the architecture of their system in their project documents, such as on a wiki. This architecture doesn’t need to describe implementation in detail, but does need to be consistent with it. For example, naming components on an architecture description with the same name as the module/package or directory the constituent code files are kept in helps newcomers relate the implementation to the architecture.
  • Secondly, a system architecture of several different patterns fulfilling various different responsibilities. For example, a sensor network may use a peer-peer pattern for information distribution, but a client-server pattern for node address registration and look-up. A pipe and filter pattern might be used to specify data processing pipelines for particular kinds of experiment, but a plugin-architecture could be used to identify the available components and link make them available for assembly. You should think about patterns as being useful devices for communicating about different aspects of an architecture, rather than each system only being contained within a single pattern.

So, the key takeaway is that software architecture helps engineers to reason about the high level structure and key components of a software system, and how they are coordinated and communicate with one another.