Video: https://www.youtube.com/watch?v=2JNJjqHrQ1g
Slides: https://studylib.net/doc/13120169/expressing-architectural-design-intent-in-code-george-fai...
Transcript formatted:
-
Agile development does not mean that software architecture is unnecessary.
-
The speaker compares this to Toyota:
- Lean development helps Toyota work efficiently.
- Automotive engineering knowledge helps Toyota build good cars.
- You need both.
-
Similarly, good software development needs:
- Agile practices such as short iterations, testing, and pair programming.
- Technical knowledge about design and architecture.
-
Agile practices do not replace software design knowledge.
The speaker says that "architecture" is often confused with three different concepts:
-
Architect
- A role in an organization.
- You may have a dedicated architect, or the developers may collectively perform the role.
-
Architecting
- The process used to make architectural decisions.
- This can happen through Scrum, design discussions, experimentation, etc.
-
Architecture
- The resulting technical structure of the system.
- This exists whether or not anyone explicitly planned it.
Important: A system has an architecture even if nobody designed it upfront.
-
Design intent is the developers' understanding of:
- How the system should work.
- Why it is structured that way.
- What constraints must remain true.
- How the system should evolve.
-
Some of this knowledge exists in:
- Developers' heads.
- Whiteboards.
- Conversations.
- Architecture diagrams.
- Design documents.
-
When the system is implemented, some of this knowledge is lost between the design and the code.
-
Later developers must reverse-engineer the original intent from the code.
The goal of the talk is therefore:
Make as much design intent as practical visible in the source code.
-
The common Agile idea is to "trust the code" or "use the source code as the ultimate truth."
-
The speaker argues that the code does not contain all design knowledge.
-
Example:
- Two programs can be mathematically/functionally equivalent.
- One can have meaningful names and structure.
- The other can be heavily obfuscated.
- The computer can execute both, but humans understand one much better.
-
Code also does not necessarily contain:
- Design rationale.
- Architectural constraints.
- Performance requirements.
- Quality requirements.
- Rules about what the system must not do.
Therefore:
Code is an important representation of design, but it is not a complete representation of design intent.
-
Programming languages are designed primarily to tell computers what to do.
-
Architecture also contains many statements about what must not happen.
-
Example:
- Suppose a system uses a cache.
- All access to the data must go through the cache.
- Bypassing the cache could make the cache inconsistent.
-
You may understand this rule as an architectural constraint, but ordinary source code may not explicitly state:
"Nobody is allowed to bypass this cache."
- This is an example of intentional knowledge that is difficult to directly encode in normal programming languages.
The speaker introduces two categories:
Things that can be explicitly named or enumerated.
Examples:
- Module A
- Module B
- Component C
- Connector X
- Port Y
These are relatively easy to represent in source code.
Rules that apply to things generally.
Examples:
- All filters communicate through pipes.
- Components must not depend on certain modules.
- A particular operation must be asynchronous.
- A component must satisfy a performance requirement.
- Certain dependencies are forbidden.
These are much harder to express directly.
Key distinction:
Extensional information says what exists. Intentional information says what must be true about what exists.
- Object-oriented design became popular partly because it allows programmers to represent domain concepts directly in code.
- For example:
Airplane
├── takeOff()
├── land()
└── persist()
-
The code structure can resemble the domain being modeled.
-
Meaningful:
- Class names
- Method names
- Variable names
- Responsibilities help humans understand the system.
This is what the speaker calls the "model in code" principle:
Expressing models in code improves comprehension and evolvability.
-
If your code represents your understanding of the domain, then the code can become inconsistent with reality.
-
For example:
- The domain model says an airplane can do X.
- The real domain changes.
- The code still represents the old model.
-
The speaker describes technical debt roughly as:
The difference between how we believe the system works and how the code actually works.
-
Therefore, modeling in code is both:
- Useful, because the model becomes visible.
- Dangerous, because the model can become outdated.
The speaker uses a definition similar to:
Software architecture is the set of structures needed to reason about a system, including its elements, relationships, and properties.
Important consequences:
- Every software system has an architecture.
- You do not need to design the architecture upfront.
- A system can evolve into its architecture.
- Architecture is observable from the resulting system.
Architecture influences qualities such as:
- Performance
- Latency
- Modifiability
- Usability
- Testability
- Reliability
- Scalability
The speaker prefers the term quality attributes or extra-functional requirements rather than "non-functional requirements."
Example:
- A centralized architecture may have different reliability characteristics from a peer-to-peer architecture.
- Skype's peer-to-peer architecture gives it properties that would be difficult to obtain from a purely centralized design.
-
Developers often draw everything as a traditional layered architecture.
-
The speaker warns against forcing every system into a layer diagram.
-
Other architectural styles include:
- Pipe-and-filter
- Client-server
- Peer-to-peer
- MapReduce
- N-tier
- Big ball of mud
-
The architecture should reflect the actual structure of the system, not the diagram style that is most familiar.
The speaker discusses architectural views, particularly:
-
Module view
- How source code is divided into modules.
-
Runtime view
- What components exist and communicate when the program runs.
-
Allocation/deployment view
- Where software runs.
- Which software runs on which machines.
- How machines communicate.
A major difficulty is:
Humans are relatively good at reading individual pieces of source code, but much worse at reconstructing the runtime architecture from source code.
This is the central idea of the talk.
Two programs can behave identically but have very different readability.
The speaker proposes an architecturally evident coding style:
Write code so that important architectural decisions are obvious to another developer reading the code.
This means intentionally doing more than is strictly necessary for the compiler.
For example:
calculateTotalExpenses()
is better for humans than:
te()
Even though both can compile and execute.
The same principle can be applied at the architectural level.
Potential benefits include:
- Reduce loss of architectural knowledge.
- Make code easier to understand.
- Reduce the amount of architecture developers must infer.
- Improve developer productivity.
- Make onboarding easier.
- Reduce documentation burden.
- Make future architectural violations easier to detect.
- Make evolution safer.
The central objective is:
Reduce the amount of architecture that developers have to reconstruct from clues.
The speaker presents several techniques for encoding architecture into code.
If modules have dependency rules:
- Use tools that can check dependencies.
- Use module systems/frameworks such as OSGi.
- Use package structures that communicate intended boundaries.
- Avoid relying solely on comments.
Why?
- Comments can become outdated.
- Comments usually cannot be automatically checked.
- Machine-checkable dependency rules are stronger.
Package organization itself can communicate design.
For example:
email-system/
infrastructure/
components/
interchange/
...
The compiler may not care about this organization.
Developers do.
A good package structure can help reveal:
- Which code belongs to which architectural area.
- Where components are located.
- Which data structures are exchanged.
- Which architectural style is being used.
There can be a mismatch between:
Source code modules
and:
Runtime components
The speaker recommends making this relationship explicit where practical.
For example:
Module A ─┐
Module B ─┼──> Runtime Component X
Module C ─┘
If the mapping is hidden, developers must reconstruct it mentally.
If the mapping is obvious, the architecture becomes easier to understand.
One particularly difficult thing to infer from source code is:
What runtime components actually exist?
A useful pattern is to centralize architecture construction.
For example:
main()
create components
create connectors
connect components
start components
Rather than scattering initialization across many files.
This makes the runtime topology much easier to see.
For a pipe-and-filter architecture, the speaker recommends a structure roughly like:
createPipes()
createFilters()
connectFilters()
startFilters()
This makes the runtime architecture visible.
Instead of forcing a developer to search through hundreds of lines to discover:
- What components exist.
- Which components communicate.
- How they are connected.
- When they start.
Components can be made recognizable through:
- Naming conventions.
- Marker interfaces.
- Base classes.
- Component-specific types.
For example:
abstract class Component {
...
}
abstract class Filter extends Component {
...
}Then developers can immediately recognize:
InputCleanupFilter
TaggingFilter
ClassificationFilter
as architectural components.
Instead of representing an architectural concept only through comments, create a type for it.
For example:
class Pipe<T> extends Connector {
...
}Now the architecture is represented directly in the type system.
This provides several benefits:
- Developers can recognize the architecture.
- IDE navigation becomes easier.
- Some mistakes can be detected automatically.
- Architectural responsibilities can be centralized.
Normal code might contain:
A.call(B)
But this does not tell you whether the communication is:
- Local method call
- Remote call
- Event
- Message
- Socket
- Pipe
- Shared memory
- Database interaction
The speaker suggests explicitly representing architectural connectors.
For example:
Pipe<Email> pipe;rather than hiding the communication mechanism inside arbitrary method calls.
A connector can contain more than just communication logic.
For example:
Connector
└── Pipe<T>
The connector can enforce:
- Communication protocols.
- Error checking.
- Concurrency behavior.
- Other architectural rules.
This moves complexity into a reusable architectural abstraction instead of forcing every component to implement it independently.
The speaker distinguishes:
- Interface: what operations are available.
- Port: a runtime interaction point through which a component communicates.
For example, a server may expose the same interface to many clients while maintaining different runtime connection states.
Making ports explicit can help represent:
- Connection state.
- Communication protocols.
- Runtime relationships.
Architectural properties can be expressed through:
asynchronousWrite()
readOnlyInventory()
@Asynchronous
@ReadOnlyAnnotations are particularly useful because they can potentially be machine checked.
Naming can communicate the architectural style.
For example:
Pipe
Filter
strongly suggests:
Pipe-and-filter architecture
Similarly, names such as:
Facade
Visitor
Adapter
communicate the use of known design patterns.
Soft mechanisms help humans understand the architecture but are not necessarily machine enforced.
Examples:
- Naming conventions.
- Package organization.
- Comments.
- Class names.
- Method names.
Example:
asynchronousWrite()The compiler does not necessarily verify that the operation is actually asynchronous.
Hard mechanisms can be checked or enforced by machines.
Examples:
- Type systems.
- Assertions.
- Preconditions/postconditions.
- Annotations with validation.
- Framework constraints.
- Architectural dependency-checking tools.
- Marker interfaces.
- Specialized compilers/preprocessors.
The speaker prefers hard mechanisms when practical because:
A machine-checkable architectural rule is stronger than a rule that exists only in documentation.
An invariant is something that must always remain true.
Example:
A cache must never be bypassed.
Possible mechanisms:
- Encode the invariant in the API.
- Use types/classes to enforce it.
- Use assertions.
- Use modeling/specification languages.
- Use automated tests.
- Use architectural analysis tools.
The key distinction is that an invariant applies broadly:
"For all components, this rule must remain true."
This is harder to express than simply defining individual components.
The speaker calls this "buried treasure."
Example:
launchSpaceShuttle()looks like it launches a space shuttle.
But if it actually calculates a bank balance, the code is misleading.
General rule:
Names should accurately reveal responsibilities.
Do not make developers discover unexpected behavior by digging through the implementation.
The talk ultimately argues for this development philosophy:
Architecture/design knowledge
↓
Source code
↓
Make important
architectural concepts
visible
↓
Humans can
understand the
architecture faster
Rather than:
Architecture
↓
Source code
↓
????
↓
Developer must reverse-engineer
the architecture
Don't only write code that works. Write code that makes the important design decisions visible.
That means deliberately using:
- Meaningful names
- Good package boundaries
- Explicit architectural types
- Explicit components
- Explicit connectors
- Explicit ports
- Centralized runtime assembly
- Annotations
- Type-system constraints
- Dependency-checking tools
- Tests and assertions
The goal is not to encode every piece of architectural knowledge into source code. The speaker explicitly says that this is impossible.
Instead:
Encode as much important architectural intent as the programming language and tooling can reasonably express, and make the remaining intent as easy as possible for humans to recognize.