Skip to content

Detailed software design in the software lifecycle

In this lecture we'll contextualize the role of coding as only one activity in the software development process and walk through the software-lifecycle as an accountable path toward a reliable software product.

Lecture upshot

Coding is only one activity in the software developmemnt process. The engineering activity starts earlier and ends later.

The Software Development Lifecycle (SDLC)

So far we've mostly reduced software development down to "coding".

  • However, with maven's packaging phase we've also seen how software must also perform outside the IDE (a software deliverable musts work outside the IDE)
  • Ultimately, we're building software not for us (the developers), but for users.
  • So ideally what we delivered at the end of the day conforms perfectly to user needs.

Nailing Skyjo is relatively easy:

  • You have the ruleset: The requirements are already more or less set in stone and pretty clear.
  • You have parts of the solution: Graphics, user interaction, etc.
  • You have interfaces that guide your implementation.

Software engineering novices often confuse software engineering with just coding. But most software projects start a lot earlier, and end far later...

Software engineering is more about humans than code.

To begin with, users don't always know what they want. In fact, most of the time they don't. It is our job as software engineer to make sense of their (often) confusing, chaotic, or contradicting explanations, and nonetheless build something that meets their requirements.

Software lifecycle definitions

  • (Detailed) software design is only one activity when it comes to developing software.
  • Before we dive into hands-on design decisions, let's take a brief look at the terminology
    • Software lifecycle: phases a software product goes through from initial idea to retirement
    • Software process: structured set of activities and practices followed to produce software (the methodology)

Ian Sommerville says: A software process is a sequence of activities that leads to the production of a software product. There are four fundamental activities that are common to all software processes. These activities are:

  1. Software specification, where customers and engineers define the software that is to be produced and the constraints on its operation.
  2. Software development, where the software is designed and programmed.
  3. Software validation, where the software is checked to ensure that it is what the customer requires.
  4. Software evolution, where the software is modified to reflect changing customer and market requirements.

Info

In this lecture we're mostly focusing on the second point, i.e. we assume the requirements are sufficiently specified, and we do not bother with validation or maintenance. Depending on which software process is followed, these activities are not necessarily entirely separated.

The most popular models, waterfall and agile walk through the activities defined by Sommerville. For example the waterfall model, as well as the iterative model set on five minimum phases:

1. requirement analysis
2. conceptual design
3. implementation
4. testing
5. maintenance

Note: The waterfall model appears strictly linear, but occasional revisions to previous activities are considered.

Requirement analysis

  • Ideally the firsts stage is always obtaining an accurate understanding of what is actually wanted.
  • Unfortunately, often enough this stage is either skipped altogether, or not follows thoroughly enough.

Requirement analysis is essential

Beginning to code without a clear understanding of user requirements is one of the most effective ways to waste resources and fail a software project.

Textual use cases

Textual use cases are a written description of success scenarios. They describe in which order actors (users, components) interact with one another, to gradually lead to a desired outcome.

Textual use cases contain (commonly) the following sections:

  • ID, Name, Primary actor
  • Goal
  • Preconditions
  • Main flow
  • Postconditions
  • Alternate flow
  • Exceptions

Usually there's more than one

Typically, systems are too complex to be described by a single use case scenario. In that case use cases form a hierarchy from higher-level use cases, to referenced lower-level use cases, which in total form an accurate description of how the system is used.

Example: Automated Cat Feeding Device

Here's a common problem of cat owners. Our felines need to be fed every day, but somtimes we're out of town ( conferences, treehouse trips, etc...). Unfortunately we cannot just leave a mountain of cat-food, because our beloved pets are a bit overweight and tend to eat more than they should.

Keksli. Cute, but does not know his limits. If left with a mountain of cat food, he'll just eat everything... and then bad things will happen.

Here is a description of a success scenario for a system to alleviate the issue:

  Use Case ID: ACFD-01
  Use Case Name: Schedule Cat Feeding
  Primary Actor: User

  Goal:
  To ensure the cat receives food automatically after a set countdown, even when the user is not home.

  Preconditions:
  - The A.C.F.D. is powered and operational.
  - The Food Bay is empty or ready to be refilled.
  - The User has access to the Control Panel.

  Main Flow:
  1. The User fills the Food Bay with food.
  2. The User closes the Food Bay, ensuring the open status is “closed.”
  3. The User sets a countdown time for release using the Control Panel.
  4. The Control Panel sends the set time to the Display, which records the current time active value.
  5. The Display updates the Control Panel with the countdown status as time progresses.
  6. When the countdown reaches zero, the Control Panel triggers the Motor to drive the release mechanism.
  7. The Motor changes the open status of the Food Bay to “open.”
  8. The Cat, now able to access the food, empties the Food Bay by eating.

  Postconditions:
  - The Food Bay becomes empty.
  - The open status remains "open" until manually reset by the User.
  - The Cat’s hunger level  is assumed satisfied.

  Alternate Flow (Manual Abort):
  - At any time before countdown completion, the User can read the Display and cancel or adjust the timer via the Control Panel.

  Exceptions:
  - If the Food Bay is not properly closed, the Control Panel does not allow the timer to start.
  - If the Motor fails, the open command does not execute, and the User must intervene manually.
Which concepts are defined, and how do they relate to the main flow ?
  • Concepts are: User, Food Bay, Control Panel, Display, Motor, Cat
  • Each item in the main flow must declare one interaction between two components.

Domain model

  • Domain models are extracted from use cases, i.e. they are a visualization of the concepts listed in the textual use case description.
    • Actors
    • Components
  • Domain models relate concepts based on their interaction, i.e. there are no functions defined in concepts, we only list "what concepts want from another" to form relations. These relations define:
    • Concern: what is requested
    • Direction: who requests, who provides
    • Multiplicities: how many instances per one instance

Relations example

From a use case model for exam writing we could infere that professors create exams and students take them. In terms of concepts and multiplicities this translates to:

Domain model example: Automated Cat Feeding Device

Based on the previous ACFD use case description, we can extract the following concepts and relations, to build a domain model:

Note: empty diamonds signify an "aggregation", that is the target component belongs to the source component, and cannot exist alone.

So far not many technical details

Keep in mind that the purpose of a domain model is to capture and understand system requirements. At this point we do not want to include solution relevant technical detail, e.g. if the user interacts with the control panel via buttons, how many buttons are needed, how many segments are needed for the display, etc... we do not add components beyond what is specified in the textual use case description.

OO software design

Once the requirements are formalized, you can pass to thinking about conceptual solutions.

Sets you on course for implementation

Diagrams set you on course for a successful implementation. With the digrams on point, implementation is a straighforward task. It is faster to make fundamnetal decisions (and spot bad decisions) on paper, in early stages, compared to implementing out entire classes only to realize that the solution is overly complicates (or cannot possibly work). Design happens on paper, not in the IDE.

  • Although this phase is closer to code, this does not yet mean opening up an IDE and starting to write your software.
  • Before you loose time with implementing code you might not need, place your attention on design decisions, notably:
    • Datastructures
    • Information encoding (Objects vs primitives)
    • Class hierarchies, interfaces and abstract classes
    • Object creation strategies
    • Immutable vs Mutable objects
  • Ideally, the outcome of this phase provides your with two implementation-relevant model types:
    • Class diagrams: Which classes are required, what methods they offer, how they reference one another.
    • Sequence diagrams: How methods work internally to provide a specific functionality.

Design choices are by definition conscient decisions

Design choices are based on facts and reasoning. Clear flaws, e.g. missing documentation, poor programming style, etc. are not design decisions.

Catching structural design with class diagrams

Class diagrams define how requirement concepts provide to solution concepts, which functionality is provided by each solution component, and how they relate to one another, in terms of dependencies and class hierarchies. In detail:

  • For classes:
    • their fields
    • their methods
    • whether static or not
    • whether class, interface, or abstract
  • For relations:
    • Inheritance
    • Dependencies
    • Compositions
    • Aggregations
Anything missing for an implementation ?

Yes! Class diagrams tell us nothing about behaviour, that is, we know which methods exist, but we do not know how they work.

Example: Zoo

  • We are to implement a textual voice imitator system for a zoo, i.e. we want a small program that outputs the sound of a given animal.
  • For instance if presented with an Tiger, the system should output Rrrrroarrr!. We don't know how many animals are around, or which ones exactly.

An example for a design decision in this case are type-checks versus polymorphism.

  • To keep things extensible, it could make sense to store animals in a Set.
  • If there are thousands of animals, we could modularize comportment, i.e. avoid having one if/else-if sequence.
  • Instead of manual class-checking, we could use polymorphism.
  • However, polymorphism also means adding more classes, and if we know from our requirements that there will only be two animal types, avoiding polymorphism could be a meaningful design decision.

Option 1: string map

The structural layout for a string check solution can be expressed as follows:

classDiagram
    class Launcher {
    }

    class Zoo {
        -animals: Map<string, string>
        +addAnimal(type: string, sound: string)
        +getSoundForType(type: string): string
    }

    Launcher "1" *--> "1" Zoo

Option 2: Polymorphism

The structural layout for a polymorphic solution can be expressed as follows:

classDiagram
    class Launcher {
    }

    class Zoo {
        -animals: Map<string, Animal>
        +addAnimal(name: string, a: Animal)
        +getSoundForType(type: string): string
    }

    class Animal {
    <<Interface>>
        +makeSound(): string
    }

    class Tiger {
        +makeSound(): string
    }

    class Lion {
        +makeSound(): string
    }


    Animal <|-- Tiger
    Animal <|-- Lion

    Launcher "1" *--> "1" Zoo
    Zoo "1" *-- "1..*" Animal

Note: Unfortunately, multiplicities have limited support in the diagram formated used here.

Capturing behavioural design with sequence diagrams

To truly capture all design decisions necessary for an implementation, we also need to specify method functioning, that is "what happens inside a method".

  • This is done using sequence-diagrams, i.e. clear definitions of who-calls-who, and what potential calls are required inside a method.
  • Let's illustrate the diagram format by formalizing what should happen within the getSoundForType method of either implementation.

Option 1: String map

sequenceDiagram
    participant L as Launcher
    participant Z as Zoo
    participant M as animals: Map<string, string>

    L->>Z: getSoundForType("tiger")
    activate Z
    Z->>M: get("tiger")
    activate M
    M-->>Z: "Roar!"
    deactivate M
    Z-->>L: "Roar!"
    deactivate Z

Reading the diagram from top to bottom, and left to right, following each method call, gives us an accurate understanding of how each method functions internally.

  • Method execution is illustrated by a grey box.
  • Calls are black arrows (carrying target function and arguments)
  • Returns are dashed arrows (carrying result values)

Option 2: Polymorphism

sequenceDiagram
    participant L as Launcher
    participant Z as Zoo
    participant M as animals: Map<string, Animal>
    participant A as Animal (Tiger/Lion)

    L->>Z: getSoundForType("tiger")
    activate Z
    Z->>M: get("tiger")
    activate M
    M-->>Z: Animal (Tiger)
    deactivate M
    Z->>A: makeSound()
    activate A
    A-->>Z: "Roar!"
    deactivate A
    Z-->>L: "Roar!"
    deactivate Z

This second diagram reveals that the first solution is simpler, since there is no need for polymorphism if we just store all animal sounds in a map directly.

Testing

Software can be tested in many ways, but commonly we distinguish by scope (or horizon), i.e. "what to test":

Horizon Test type Example
Isolated module Unit-Test Calling java class with input x returns y.
Interplay of multiple modules Integration tests System sends email to alert@uqam.ca when critical condition arrives.
Interplay of entire system System test Click on accept finalizes flight booking and generates boarding pass PDF`
Non functional aspect Acceptance test System reacts sufficiantly fast for productive use.

In general

In general things get more difficult (and expensive) to test, the greater the horizon. E.g. unit tests are not expensive compared to hiring test users that attempt to interact with your system.

Testing and earlier phases

Even without an implementation, early software development lifecycle phases lay the groundwork for testing. Requirement analysis and design elicitation define:

  • What methods are required.
  • What comportment is expected.

Using these two, we can actually define unit tests before having an implementation. This practice is also referred to as "Test driven development".

Example

For a prime-checker, we can create a unit tests, to verify outputs for specific inputs:

import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

import org.junit.Test;

public class PrimeCheckerTest {
  private final PrimeChecker checker = new PrimeChecker();

  /**
   * Tests if the number 23 is correctly identified as a prime number.
   */
  @Test
  public void testIsPrime23() {
    assertTrue(checker.isPrime(23));
  }
}

A developer can use the test method as grounds for method signatures and advancing a reliable implementation.

Beware of test hacking

Fullfilling tests is no guarantee for correctness, if you do not trust a developer, do not hand out all tests beforehand - they might hack the results aroung your tests.

Maintenance

Although maintenance is not handled in more depth in this course, keep in mind that the software lifecycle does not end with delivering a software to the client.

Even if the software is perfect at the moment of delivery, the job is not done, because the environment constantly evolves:

  • The JDK may change, method may be deprecated or found insecure.
  • The hardware may change, new incompatible CPUs may replace the current.
  • And the worst... requirements may change (migrating old software is crazy expensive)
pie title Cost illustration of SDLC phases
"Requirement" : 3
"Design" : 8
"Implementation" : 7
"Testing" : 15
"Maintenance" : 67

On an average, the cost of software maintenance is more than 50% of all SDLC phases.

What about architectures ?

In the course layout we went directly from requirement analysis to OO design. Often times design also involves macro-structures, also called "software architecture" choices.

To clarify the distinction:

  • OO design is about individual classes, their functions, their relations.
  • Architecture is about macro-concerns:
    • What larger components are in a system (e.g. packages)
    • How does information flow between these components, are there restrictions

Model, View, Control

An example seen in class is MVC. MVC as is an architectural pattern (or style), dividing the software into three conceptual units:

  • Model: for representing system state
  • Controller: for transferring system state in a sane way, so the model is never corrupted / in an inconsistent state.
  • View: To represent model state and consume user interactions, map them to controller actions

The main interest of MVC is to provide protection for applications. Intuitively you could tend to implement any system just with a UI and a Model. That is possible, but then you have zero integrity protection:

  • Anyone can change anything.
  • Programming errors in the UI likely corrupt your application state. Just think of a bank implementation, where all clients have full database access and are trusted to correctly handle currency in your model...

MISC

To conclude the lecture, here a few philosophical considerations for accountable software engineering.

On the role of requirements

Engineering is all about meeting the requirements:

  • The product must meet the requirements (not the other way round).
  • If the product does not meet the requirements, the engineers failed their job.

If you only know a hammer, everything looks like a nail.

Unfortunately inexperienced engineers tend to often think "solution first", rather than "problem first":
If the only tool you know to use is a hammer, everything looks like a nail to you.

Why engineering is not trivial:

  • You must understand the requirements:
    • Read and listen carefully.
    • Be empathetic and anticipate needs.
  • You must be able to select the most appropriate tool:
    • Know more than one tool.
    • Understand which contexts each tool fits best.
    • Compare the problem context to the available tools.
    • Manage trade-offs.
    • Know how to effectively use the chosen tool.

Engineering is about trade-offs

There is rarely a single perfect solution. Engineering is about understanding and balancing risks, while keeping the focus on what matters most. Non-technical constraints such as time and budget often play a significant role as well.

Poor engineering

Engineering is about taking accountability for a software product:

  • If there is no need for accountability, then there is no need for an engineer.
  • As a software engineer, the value you bring to a company, is the liability you take for your code.

No-one, really no-one needs an engineer, who:

  • Chooses a solution without thinking about alternatives.
  • Copy-pastes code (a solution) without knowing how it works.
  • Using an LLM to generate code that you do not understand, and just hits "run" until things seemingly "work"

... this is not engineering, this is programming by trial and error.

However, it is ok to use modern solutions, if you can take accountability:

Don't rely on generative AI for critical decisions

Generative AI, especially LLMs operate on language models that complete provided text with what statistically often correlates.
To paraphrase what this means :
- AI tools have no cognitive understanding of the problem domain. Instead they wrap existing context with plausible sounding completions - for humans, we call this practice "bullshitting".
- Hallucination as a concept is not new, only the term, as a new euphisms for what we used to call a software bug: An inherent flaw that causes a software to be non-trustworthy.

Literature

Inspiration and further reads for the curious minds: