Architecture
Before proceeding further, we should discuss how to structure an application properly. This requires a broader discussion of software architecture, and software qualities.
What is Architecture?
Architecture is the holistic understanding of how your software is structured, and the effect that structure has on it’s characteristics and qualities. Architecture as a discipline suggests that structuring software should be a deliberate action.
Decisions like “how to divide a system into components” have a huge impact on the characteristics of the software that you produce.
- Some architectural decisions are necessary for your software to work properly.
 - The structure of your software will determine how well it runs, how quickly it performs essential operations, how well it handles errors.
 - Well-designed software can be a joy to work with; poorly-designed software is frustrating to work with, difficult to evolve and change.
 
I like this quote by Bob Martin:
It doesn’t take a huge amount of knowledge and skill to get a program working. Kids in high school do it all the time… The code they produce may not be pretty; but it works. It works because getting something to work once just isn’t that hard.
Getting software right is hard. When software is done right, it requires a fraction of the human resources to create and maintain. Changes are simple and rapid. Defects are few and far between. Effort is minimized, and functionality and flexibility are maximized. 
— Robert C. Martin, Clean Architecture (2016).
In Martin’s view, software should be enduring. Software that you produce should be reliable, and continue to function for a long period of time. You should expect to make adjustments over time as defects are found and fixed, and new features are introduced, but these changes should be relatively easy to make.
Building software to this level of quality does not happen accidentally, but requires deliberate action on our part.

https://www.atr.org/40-years-of-failure-irs-unable-to-fix-computer-system/
Software Qualities
Let’s think about the qualities that we want in the software that we build. What would we expect from any piece of software that we produce?
1. Usability
A system must be usable for its intended purpose, and meet its design objectives. This includes both functional and non-functional requirements.
- Functional: features that are required to address the problem that the software is intended to solve; desirable features for our users.
 - Non-Functional: qualities or characteristics of software that emerge from its structure. e.g., performance, reliability and other quality metrics.
 
Usability requires us to carefully ensure that we are meeting all reasonable requirements up-front, typically by collaborating with our users to define problems and solutions. See Process Models.
2. Extensibility
You should expect to adapt and modify your software over a lifetime of use:
- You will find errors in your code (aka bugs) that will need to be addressed.
 - You might find that requirements were incomplete, so existing features may need to be modified.
 - You might uncover new features that need to be implemented.
 - Your operating environment might change (e.g. OS update) which necessitates updates.
 
We need to design systems such that you can respond and adapt your software to these changes, effectively, and with the least amount of work.
Your design goal is not to deliver software once, it’s to design in a way that supports delivering frequently and reliably over the life of your solution.
3. Scalability
Extensibility refers to handling new or changing requirements. Scalability refers to the ability to handle increased data, number of users, or features. e.g., an online system might initially need to handle hundreds of concurrent users, but could be expected to scale to tends of thousands over time. Scalability is challenging because you don’t want to incur the deployment costs up-front, but instead you want to design in a way that lets you expand your system over time. This can include replacing modules with more capable ones at a later time, e.g., swapping out a low-performance database for something more performant.
4. Robustness
The system should be able to handle error conditions gracefully, without compromising data. This includes user input errors, errors processing data from external sources, and recovering from error conditions, e.g., power outages.
5. Reusability
Software is expensive and time-consuming to produce, so anything that reduces the required cost or time is welcome. Reusability or code reuse is often positioned as the easiest way to accomplish this. Reusing code also reduces defects, since you’re presumably reusing code that is tested and known-good.
I see three levels of reuse. At the lowest level, you reuse classes: class libraries, containers, maybe some class “teams” like container/iterator.
Frameworks are at the highest level. They really try to distill your design decisions. They identify the key abstractions for solving a problem, represent them by classes and define relationships between them. JUnit is a small framework, for example. It is the “Hello, World” of frameworks. It has Test, TestCase, TestSuite and relationships defined.
A framework is typically larger-grained than just a single class. Also, you hook into frameworks by subclassing somewhere. They use the so-called Hollywood principle of “don’t call us, we’ll call you.” The framework lets you define your custom behaviour, and it will call you when it’s your turn to do something. Same with JUnit, right? It calls you when it wants to execute a test for you, but the rest happens in the framework.
There also is a middle level. This is where I see patterns. Design patterns are both smaller and more abstract than frameworks. They’re really a description about how a couple of classes can relate to and interact with.
– Shvets (citing Erich Gamma), Dive Into Design Patterns (2019).
We want an application that exhibits all of these qualities. How do we get there?
Architectural Principles
The following are suggested best-practices that will help you build robust, extensible and scalable software.
1. Separation of concerns
“A module should be responsible to one, and only one, user or stakeholder.“
- Robert C. Martin on the “Single Responsibility Principle”.
It follows from the SOLID Single Responsibility Principle that our software should be written as a set of components, where each one has specific responsibilities. By component, we can mean a single class, or a set of classes that work closely together, or even a function that delivers functionality.
Modularity refers to the logical grouping of source code into these areas of responsibility. Modularity can be implemented through namespaces (C++), packages (Java or Kotlin). When discussing modularity, we often use two related and probably familiar concepts: cohesion, and coupling.
Cohesion is a measure of how related the parts of a module are to one another. A good indication that the classes or other components belong in the same module is that there are few, if any, calls to source code outside the module (and removing anything from the module would necessitate calls to it outside the module).
    Coupling refers to the calls that are made between components; they are said to be tightly coupled based on their dependency on one another for their functionality. Loosely coupled means that there is little coupling, or it could be avoided in practice; tight coupling suggests that the components probably belong in the same module, or perhaps even as part of a larger component.
    When designing modules, we want high cohesion (where components in a module belong together) and low coupling between modules (meaning fewer dependencies). This increases the flexibility of our software, and makes it easier to achieve desirable characteristics, e.g. scalability.
2. Drive the UI from Data
Your application should include a User Interface (UI), which displays the application data, and a Data Model that describes that data. Data models are independent from the user interface (UI), but the UI should always reflect the state of the data.
Your data model should be local and persistant:
Localmeaning that the data model is stored in memory; your application should continue to work even with the loss of a network connection.- ‘Persistant’ meaning that it is saved when your application is working (and specifically on exit), and then restored when your application is launched. You data model can be persisted on local storage (e.g., hard drive or local database) or remotely (e.g., cloud storage).
 
3. Single “Source of Truth” for Data
You should have a Single Source of Truth (SSOT) where your application’s data model is persisted. The source of truth for application data is typically a database, but can also be a class or set of classes that “own” that data and are responsible for modifying and/or mutating it.
To achieve this, the SSOT exposes the data using an immutable type, and to modify the data, the SSOT exposes functions or receive events that other types can call to modify the data.
This pattern brings multiple benefits:
- It centralizes all the changes to a particular type of data in one place.
 - It protects the data so that other types cannot tamper with it.
 - It makes changes to the data more traceable. Thus, bugs are easier to spot.
 
4. Unidirectional Data Flow
The Unidirectional Data Flow (UDF) pattern states that data flows in only one direction. The events that modify the data flow in the opposite direction. e.g., the user may interact with the UI, which in turn will send a request to the model to fetch data, which in turn may ask the database to fetch that data. This is uni-directional.
This pattern better maintains data consistency, is less prone to errors, and is easier to debug.
Application Architecture
We need an application architecture that addresses these principles.
1. Model-View-Controller (MVC)
Model-View-Controller (MVC), was created by Trygve Reenskaug for Smalltalk-79 in the late 1970s as a method of structuring interactive applications. It suggests that an application should consist of the following components:
- Model: the information or program state that you are working with,
 - View: the visual representation of the model, and
 - Controller: which lays out and coordinates multiple views on-screen, and handles routing user-input.
 
    In a “standard MVC” implementation:
- input is accepted and interpreted by the 
Controllerclass, and then - routed to the 
Model, where it changes the program state (in some meaningful way), and then - changes are published to the 
Viewthrough a notification mechanism. 
MVC provides a mechanism for having the Model (Data) drive the View (UI), and supports Unidirectional Data Flow. However, it doesn’t handle the Single Source of Truth requirement very well, and the Controller implementation doesn’t align very well with modern toolkits and practices (as we’ll see in the UI lecture later).
Let’s consider alternatives.
2. Model-View-ViewModel (MVVM)
Model-View-ViewModel was invented by Ken Cooper and Ted Peters in 2005. Based on Martin Fowler’s Presentation Model, it was intended to simplify event-driven programming and user interfaces in C#/.NET.
MVVM suggests two major changes from MVC:
- MVVM removes the 
Controllerclass, and - MVVM adds a data container class named the 
ViewModel, that sits between theViewandModel. 
    This reduces our application to the following components:
Model: As with MVC, the Model is the primary Domain object, holding the application state.View: The structure, layout and presentation of what is on-screen. With modern toolkits, the View handles both input and output i.e. the complete user experience.ViewModel: A component that stores data that is relevant to the View to which it is associated. This may be a simple subset of Model data, but is more often a reinterpretation of that data in a way that makes sense to the View e.g., dollar amounts in USD in the Model may be reflected in a local currently in the ViewModel.
One interesting trend that works in favor of MVVM is the idea of reactive programming, where changes in one component are automatically published to other interested components. MVVM is often implemented in a way where we can use a binding mechanism to map variables in the ViewModel directly to widgets in the View, so that updating one directly updates the other.
What is the benefit of a ViewModel?
- We will often want to pull “raw” data from the 
Modeland modify it before displaying it in aViewe.g., currency that is stored in USD but should be displayed using a local currency. - We sometimes want to make local changes to data, but not push them automatically to the 
Modele.g., undo-redo where you don’t persist the changes until the user clicks a Save button. 
MVVM recommends that you have one ViewModel for each View, and that ViewModel manages all data for that View.
3. Layered Architecture
A Layered Architecture Pattern in some ways resembles MVC, but is more suitable for scalable applications. A layered architecture attempts to handle modularity by organizing software into horizontal layers:
    There are variations of this pattern that use more layers, but these three are common.
- User Interface (UI) Layer: UI layer that the user interacts with.
 - Domain (Business Logic) Layer: the application logic, or “business rules” for the application.
 - Data Layer: manages data persistance e.g., database, public API, data file.
 
The major characteristic of a layered architecture is that it enforces a clear separation of concerns between layers: the User Interface layer doesn’t know anything about the application state or logic, it just displays the information; the Domain layer knows how to manipulate the data, but not how it is stored and so on.
A layered architecture is an extremely common way to build an application. It falls out of some organizational styles quite naturally (see Conway’s Law).
- Front-end developers and designers work on the UI and Domain layers.
 - Back-end developers work on the Model and Service layers.
 - Full-stack developers integrate these layers.
 
Dependency Rule
Dependencies flow down and data flows up.
What do we mean by dependencies flow down? It means that layers can only directly reference the layer directly below them. e.g., the UI layer can use (depend) on the Domain layer and any classes it contains. This is a one-way dependency; the Domain classes are not allowed to have any reference back to the UI layer.
What do we mean by data flows up? The Model needs to have some way of notifying the View that the data has changed, without a direct reference to that view (“loose coupling”, remember?). We use a loose coupling mechanism to send messages to the View. This is effectively the Observer design pattern, where the View (ISubscriber interface) is registers itself with the Model (IPublisher). The Model publishes changes to all registered Views when they occur.
This mechanism works for any data change in the model e.g., a system event causes the data to change, or data changes in the database, or the user changes data in one window causing a second window to update).
Required Layers
The User Interface Layer, is the part of your application that the user interacts with. The UI layer handles user input, and expresses the application’s output. This layer can be quite complex since it handles IO for all devices (keyboards, mice, touchpads) as well as the UI that the user interacts with (console, graphical). Graphical interfaces typically consist of multiple screens, each with their own interaction support and visual representation.
We will commonly represent a screen with a View class, and the data for that screen in a corresponding ViewModel class. The View class should be stateless and the ViewModel should have full responsibility for maintaining the UI state (and communicating with the Model).
The Domain layer describes the application logic for our program. e.g., rules for managing customer data, or bank transactions, or how to combine ingredients in a recipe, or whatever else is needed. It serves as an intermediate layer between the raw data (e.g., records from the Model) and how that data is presented (e.g., screens in the UI layer).
The Data Layer manages persistance, usually to some external service e.g., a database, web API, configuration file. You will likely have one or more classes, each representing a different data source.
Abstraction
Finally, one important piece of this approach is the use of Interfaces to promote loose coupling. By describing component relationships in terms of behaviours we have the flexibility to swap in new implementations at any time (abstractions not concretions).
This is especially important for
Subscribers: we want any form of user interface to be able to receive notifications, not just GUI screens. e.g., we might send output to a voice dictation system, or a printer.Publishers: we want the flexibility of multiple models. We may not do this in production, but we can certainly do it when testing.Services: finally, we want to be able to request data and save data to a variety of services without knowing the implementation details. e.g., your model shouldn’t know the details of how to save to a SQL database, it should rely on an abstraction that exposes save behaviour. This let us swap databases, or even just save data to a file for testing instead of our remote DB.
Sample Code
Let’s imagine a simple application with a single View, ViewModel and Model.
  ---
displayMode: compact
gantt:
    useWidth: 400
    compact: true
---
classDiagram
    View "1" --> "1" UserViewModel
    View "1" <.. "1" UserViewModel
    UserViewModel "*" --> "*" UserModel
        UserViewModel "*" <.. "*" UserModel
    ISubscriber "1" <|.. "1" UserViewModel
    IPublisher <|.. UserModel
    ISubscriber "*" <.. "*" IPublisher
    class View {
        -UserViewModel viewModel
    }
    class ISubscriber {
        <<Interface>>
        +update()
    }
    class UserViewModel {
        -View view
        -UserModel model
        +update()
    }
    class IPublisher {
        <<Interface>>
        -List~Subscriber~ subscribers
        +notify()
    }
    class UserModel {
        -String firstname
        -String lastname
        +subscribe(ISubscriber)
        +unsubscribe(ISubscriber)
    }
Here’s what the code would look like. In our Main class, we instantiate classes and use dependency injection to connect our class instances. These should mirror the relationships on our diagram above.
// main class
class Main {
    val model = Model()
    val viewModel = ViewModel(model)
    val view = View(viewModel)
    model.add(viewModel)
}The Subscriber interface is an abstraction for any class that wants to be notified of model changes. Any class can act like a view as long as it supports the appropriate method to allow notifications from the model. In this case, our ViewModel is the Subscriber. It will be subscribed for Model updates, and will take care of notifying its associated View as needed.
// UI classes
interface Subscriber {
  fun update()
}
class View(val viewModel: ViewModel) {
    // some user interface class that relies in the viewModel for its state
    // assume it can pull state from the viewModel and display it
}
class ViewModel(val model: Model) : Subscriber {
    override fun update() {
        // this method is called by the Publisher (aka Model) when data updates
        // fetch data from model when the model updates
    }
}The Publisher (aka Model) maintains a list of all Subscribers (aka ViewModels), and notifies each one of them when its state changes. The Publisher has no control over how they react; each Subscriber must decide what to do with the update basd on whether the data is relevant to them. i.e., they might ignore the notification if it’s data they don’t use, or they might choose to fetch updated data from the Model.
In the code below, the Publisher is an abstract class so that we can add default implementation code for managing the Subscriber list.
abstract class Publisher {
    val views: List<Subscriber> = emptyList()
    fun addView(view: Subscriber) {
        views.add(view)
    }
    fun update() {
        for (view : views) {
            view.update()
        }
    }
}
class Model {
    fun fetchData() {
        // do something that causes data to change
        update() // notify subscribers
    }
}Benefits
Layering our architecture really helps to address our earlier goals (reducing coupling, setting the right level of abstraction). Additionally, it provides these other specific benefits to our application:
- Independence from frameworks. The architecture does not depend on a particular set of libraries for its functionality. This allows you to use such frameworks as tools, rather than forcing you to cram your system into their limited constraints. It becomes more testable. Layers can be tested independently of one another. e.g., the business rules can be tested without the UI, database, web server, or any other external element.
 - Independence from the UI. The UI can be changed without changing the rest of the system. A web UI could be replaced with a console UI, for example, without changing the business rules.
 - Independence from the data sources. You can swap out Oracle or SQL Server for Mongo, BigTable, CouchDB, or something else. Your business rules are not bound to the database or to the source of your data.