Secrets of a Software Architect

or, guiding principles for software design

Isaac Lin

version 1.1 (press "Backspace" for help; "t" to see notes)

If you are using the Opera web browser, use full screen mode to see the presentation in slide show format.

Location of presentation: http://www.carrotpatch.org/reference/software-arch-secrets/

Press "t" to toggle between slideshow mode and outline view. See "Controls for S5 presentations" for more controls.

Contents

  • What is a software architect?
  • Principles
  • Advice
  • Summary

This presentation covers key guiding software engineering principles for creating a maintainable, extensible design.

It is not a comprehensive guide to design principles. Notably, performance design principles are not covered.

Definition

Architecture

  1. the art or science of building; specifically: the art or practice of designing building structures and especially habitable ones
  2. a. formation or construction resulting from or as if from a conscious act;
    b. a unifying or coherent form or structure
    [ ... ]
  3. the manner in which the components of a computer or computer system are organized and integrated

Merriam-Webster OnLine

Similar to traditional architecture, computer architecture is about how multiple components are built to interact with each other to form a cohesive system that can meet specific design objectives.

What is a software architect?

A software architect is responsible for guiding the overall design and evolution of a software system.

  • Decides how software should be structured to best meet current and future needs.
  • Works with developers on modifications that affect the architecture of the software system.
  • Works closely with code owner, who ensures that code changes align with the overall architectural direction, and do not affect the stability of the existing software.
  • Works closely with product architect, who is responsible for the overall product design and evolution.

“Needs” include many aspects:

  • Ability to extend design to support new capabilities.
  • Reduce ongoing support and maintenance costs.
  • Fit well with software development processes (e.g. software ownership).

The importance of meeting the desired behaviour must be evaluated within the context of the project’s business objectives. A software architect must judge what future flexibility is not warranted, given what is known about the business environment, and choose to eliminate these paths. Choosing what to cut out can be more important that choosing what to keep.

From a process standpoint: software architects are responsible for approving High Level Designs.

Designers looking to get their designs approved should think about how they can make it as easy as possible for the architect to understand the proposed changes and any necessary background information.

For the purposes of this presentation, the following types of software work products are defined:

software application
A set of processes that interact together to provide a service or other capabilities.
software library
A software component intended to be reused in a variety of different software applications.
software platform
A set of software libraries and/or applications that provide a common set of services and capabilities for software applications.

Contents

  • What is a software architect?
  • Principles
  • Advice
  • Summary

Principle 1: Tell a story

Every component of software should have a clearly defined role, which interacts with other components in clear ways. They are like characters in a story: each with their own purpose.

FictionSoftware
character component
story line walkthrough
character motivation purpose for component
character relationships component interactions, message sequence charts
  • Where possible, use established, well-known roles.
  • Define a clear purpose for each role.

Why do books / films / TV shows use archetypes and stereotypes? Until the characters / roles are established, readers / viewers have trouble following what is going on — they have not yet determined the appropriate frame of reference.

By using familiar characters, the author helps the audience get up to speed more quickly.

In the same way, by using familar software programming components, constructs, tools, libraries, and patterns, those who need to understand your code can do so more quickly.

cast photo of Everybody Loves Raymond

Keep roles consistent

Good stories introduce new aspects of a character’s personality that are consistent with the character’s overall nature.
Bad stories introduce random facts on an “as needed” basis to suit the purposes of the plot.

Good software has clearly defined components with specific roles. As changes are made, they are integrated into the existing roles.
Bad software has many bits of state here and there to meet the needs of today’s new feature or bug fix — the components lack cohesion.

Key challenge: how to define the roles in a way that is useful on all levels (e.g. class, component, subsystem, process).

A key problem in software design is how to scale the idea of roles. Each piece of software has its own specific role, but since humans cannot think about hundreds or thousands of roles in their mind at once, these roles must be composed into higher level roles. A software architect must think of a way to design the roles so they make sense on a small, medium, and large scale.

Principle 2: Keep components loosely coupled

Design components so they can be enhanced and extended independently from each other.

  • Define clear interfaces to components.
  • If there is data or logic that needs to be shared between components, then separate this information into a component of its own.
  • Minimize dependencies (both implicit and explicit) on other components — but go ahead and use them if you need to!

Each component must define a clear interface that fits with that component’s role. The interface might not be strictly procedural, although with traditional procedural-based languages, this is the easiest way to have strong enforcement of the interface. Other components must only interact with the component using the documented interface.

Having a documented interface makes it easier to change the internals of a component, and to reuse the component in new ways.

A component should not have needless dependencies, including implicit dependencies where it has detailed knowledge of the implementation of other components, as this increases maintenance costs. However, it should use other components rather than clone their behaviour and knowledge.

Note the tradeoffs for minimizing your dependencies are a bit different if you are designing a software library versus a software application. Although the basic principle is still the same, a standalone software library may need to put a higher priority on avoiding using other components. A software library that is part of a software platform, on the other hand, can take advantage of this and reuse platform components.

Principle 3: Open-closed principle

Software components are open for extension, but closed for modification.

  • Design the program framework so new behaviour can be plugged into it, without having to modify existing code.
  • For classes: define abstract interfaces in a parent class; provide implementation in subclasses that can be substituted for each other. Use a creational pattern like the Abstract Factory pattern in the main framework to select the required subclass.
  • For algorithms: design points for extension that allow the main logic to remain unchanged.
    • Make algorithms data-driven where possible.

The Open/Closed Principle was first used in Bertrand Meyer’s 1988 book, Object-Oriented Software Construction. It was redefined by Robert C. Martin in the January 1996 issue of the C++ Report to refer to the inheritance of interfaces which are “closed” (having been defined in the parent class), and having new implementations added in new subclasses.

The intent is to allow new features to be added by adding new software, without having to change the existing software. Following this principle leads to designs where separate features are decoupled from each other and implemented with encapsulated components.

Note bug fixes of course require modification of existing software. Enhancements which meet the following might also be suitable for keeping within existing components rather than creating new ones:

  • enhancements must always be deployed with the existing component: the changes are inherently coupled
  • fit within the scope of existing components
  • consistent with component’s purpose: still fits within the overall story, while preserving the essential character/personality/role of the component

However, components can only be extended at its provided extension points. If no suitable points exist, then the base framework must be modified, either to provide the right points, or by reworking the logic to handle the new requirement. This may require recasting the roles of the components to fit the program's new needs.

Finding the right balance of extensibility is a challenge (see Principle 6).

Challenge with data-driven algorithms: how is the data managed and configured? How is it validated and checked for consistency?

Principle 4: Know the lifetime of information

Desiging software is like designing a murder mystery: who knows what, and when?

  • Decide what pieces of information need to be known, and by who.
  • Decide when the information needs to be updated.
  • Decide what component should own the information.
  • Decide how other components will access the information.

Programming can be considered to be about managing flows of data and processing them. The lifetime of information is an important aspect of a software design, seen from a data-centric point of view.

“information” includes both the data that a program is processing, as well as data, algorithms, mappings, strategies, and so forth used by a program to process the data.

Note in an object-oriented design, similar decisions must be made for the lifetime of objects. Typically, their lifetime will mirror the lifetime of some form of information required by the software.

Managing the lifetime of information is key to keeping components encapsulated from each other. Without clear information ownership and management policies, it is easy for information to leak across multiple components.

Designing the ownership policies for information is a useful step in avoiding circular dependencies, since knowing the component responsible for each bit of information also identifies the dependencies between components for each bit of information. The ownership policies also help avoid upwards coupling of components, where a lower level component is dependent on an upper level component.

The lifetime of a variable should match the required lifetime of the information it holds. For example, if an object only needs to be used within a method, then it should be held in a local variable, rather than a data member.

Avoid duplication of information

Have a single owner for each piece of information who will be responsible for it.

  • Other components will have to go through the owner’s interface to access the information.
  • Performance tradeoff: sometimes to achieve the required performance, the information must be duplicated. This increases maintenance costs since the data must now be kept in sync.
    • Example: program data that must live beyond the life of the program.
    • For efficiency, this data is typically kept in memory, and flushed to disk periodically.
    • A strategy for flushing the data and how often to do it must be implemented, as well as a recovery strategy in case of disk failure.

Principle 5: Maintain levels of abstraction

Organize components into different levels of abstraction. As a general rule, components should not bypass intermediate levels within a hierarchy.

  • Components on the same level interact with each other.
  • Components ask components on the immediate level below to perform tasks.
  • Components respond to requests from components on the immediate level above.

Parameters to a function should be roughly at the same level of abstraction.

Performance issues may dictate a need to bypass levels.

Note this guideline is flexible and you must use your best judgment. There are cases where it is more useful for a component to interact with many different levels. For example, some components might provide general services that are needed at a variety of levels (e.g. logging functions).

Keeping a method/function’s parameters at the same level of abstraction helps maintain encapsulation. The user of the function will not have to track information across multiple levels of abstraction.

Having levels of abstraction limits the amount of interactions each component can have with others. This helps with defining clear roles for each component and in simplifying their story.

Principle 6: Balance extensibility vs. encapsulation and cohesion

  • More extensibility can result in less encapsulation and cohesion, since a component or function may be extended in a way that is contrary to its primary purpose.
  • Example: operator overloading — typically not used except for proxy classes, arithmetic, and extraction/insertion (>> and <<) because other uses can obscure understanding.
  • Languages like Perl and Lisp that allow you to redefine any function on the fly are very flexible, but unless care is taken, implicit cross-dependencies can creep in.

The benefits of adding additional extensibility must be weighed against the projected need for the additional capability, and the potential loss of encapsulation or cohesiveness. A component with a fuzzy purpose is harder to understand than one with a focused role; this ambiguity increases maintenance costs.

Use techniques to keep the hooks for extensibility well defined and controlled, such as strategy pattern, template method pattern.

Contents

  • What is a software architect?
  • Principles
  • Advice
  • Summary

See both the big and the small

  • Be able to balance big picture concerns with low-level implementation issues.
  • Balance architectural desires against costs.

Keep programming skills up to date

  • Know recommended programming techniques.
  • Keep up to date on latest methods.

Pick your battles

  • Select the most important aspects of the design and be willing to stand firm on them if warranted.

Advice for architects:

  • Newer designers can be given less leeway than those who have proven that they can handle greater flexibility.

Encourage participation

  • Encourage everyone to participate in providing feedback on a design. Even when alternatives are not taken, the discussion around them helps validate the chosen approach.
  • Be open to new ideas; try to look for the advantages, if only to try to incorporate them into the existing plans. Don’t be afraid to abandon the existing plan if after consideration the newly proposed one is better.

Explain your reasoning

  • Explain your views across a variety of domains (e.g. big and small concerns).
  • Admit what you don’t know and take advantage of what others do know.

Advice for architects:

  • If there is no specific reason, admit it. As architect, you are empowered to make judgment calls based on your experience. (If it is the kind of decision that gets upper management attention, or is contentious in some way, you will probably need to come up with some justification!)

Contents

  • What is a software architect?
  • Principles
  • Advice
  • Summary

Summary

  • Principles
    • Tell a story.
    • Keep components loosely coupled.
    • Open-closed principle
    • Know the lifetime of information.
    • Maintain levels of abstraction.
    • Balance extensibility vs. encapsulation and cohesion.
  • Advice
    • See both the big and the small.
    • Keep programming skills up to date.
    • Pick your battles.
    • Encourage participation.
    • Explain your reasoning.