Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: SDC Decomposition #1284

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
+++
title = "Adding like/dislike"
headless = true
time = 120
facilitation = false
emoji= "👍👎"
objectives = [
"Identify what data needs to be stored and exchanged between a client and server.",
"Devise a scheme for differentiating messages with different meanings (e.g. a new message vs a new like).",
"Contrast giving updated values as absolute values or relative changes.",
"Implement an end-to-end feature involving data updates and reconciliation across a client and server.",
]
+++

The last requirement we have for our chat application is the ability to like/dislike a message (and see what messages have been liked/disliked).

{{<note type="Exercise">}}
Think about what information a client would need to provide to a server in order to like/dislike a message.

Think about what information a server would need to provide to a client in order to display how many likes/dislikes a message has.

Think about what information a server would need to provide to a client in order to _update_ how many likes/dislikes a message has.

Write these things down.
{{</note>}}

### Identifiers

One of the key new requirements to add liking/disliking a message is knowing _which_ message is being liked/disliked.

When a client wants to like a message, it needs some way of saying _this_ is the message I want to like.

This suggests we need a unique identifier for each message:
* When the server tells a client about a message, it needs to tell it what the identifier is for that message.
* When a client tells the server it wants to like a message, it needs to tell it the identifier for the message it wants to like.
* When the server tells a client a message has been liked, it needs to tell the client which message was liked, and the client needs to know enough about that message to be able to update the UI.

### Message formats

Now that your server will be sending multiple kinds of updates ("Here's a new message", or "Here's an update to the number of likes of an existing message"), you'll need to make sure the client knows the difference between these messages. The client will need to know how to act when it receives each kind of message.

### Changes or absolutes?

When new likes happen, a choice we need to make is whether the server should tell a client "this message was liked again" or should tell the client "this message now has 10 likes". Both of these can work.

{{<note type="Exercise">}}
Write down some advantages and disadvantages of a server -> client update being "+1 compared to before" or "now =10".

Choose which approach you want to take.
{{</note>}}

{{<note type="Exercise">}}
Implement liking and disliking messages.

If a message has a non-zero number of likes or dislikes, the frontend needs to show this.

The frontend needs to expose some way for a user to like or dislike any message.

When a user likes or dislikes a message, the frontend needs to tell the backend about this, and the backend needs to notify all clients of this.

When a frontend is notified by a backend about a new like or dislike, it needs to update the UI to show this.

You may do this in your polling implementation, WebSockets implementation, or both.
{{</note>}}
35 changes: 35 additions & 0 deletions common-content/en/module/decomposition/adding-quotes/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
+++
title = "Adding quotes"
headless = true
time = 60
facilitation = false
emoji= "➕"
hide_from_overview = true
objectives = [
"POST data from a frontend to a backend in JSON format.",
]
+++

{{<note type="Exercise">}}
Add a form to your frontend which allows users to add quotes to the backend's list of quotes.

Note: Your backend expects the quotes to be submitted as JSON. This means you will need to use a `fetch` request from JavaScript to do the POSTing.

You can't just use a `<form method="POST">` tag because that would post the information in a different format. (It would also redirect the user to a page which just says "ok" after submitting the quote, which isn't a great user experience!)
{{</note>}}

After a user tries to add a quote, if they successfully added the quote we should give the user some feedback so they know it was successful.

If a user tried to add a quote and the `fetch` failed (perhaps because the backend wasn't running), or the response said there was an error, we should give the user some feedback so they know something went wrong (and maybe what they should do about it).

Now that our backend allows users to post quotes, we want to restrict what they can post.

For instance, we don't want to let people post quotes which are empty.

We need to validate the inputs our users give us.

{{<note type="Think">}}
Where do you think we want to do this validation?

On the frontend? On the backend? Or both?
{{</note>}}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
+++
title = "Limitations of backends"
headless = true
time = 30
facilitation = false
emoji= "✋"
objectives = [
"Explain why a backend on its own doesn't provide reliable data persistence.",
"Explain why we may prefer doing work in a frontend rather than backend to avoid latency.",
]
+++

We've already explored limitations of frontends.

We know a backend is just a program that runs for a long time.

### Lifetime

A major limitation of backends is that "a long time" probably isn't forever.

Sometimes we change the code of the backend. Or we need to restart the computer it's running on for an upgrade (computers are real, physical things!). Or its computer loses power and we need to start it again.

When this happens, the program starts again.

Think back to our quote server that allows users to POST new quotes. If we had to stop the server and start it again, we would lose all of the quotes users had saved. They're just stored in a variable, and a variable only lasts while the program it's in is running.

### Location

Another major limitation of a backend is where the code runs.

A backend's code runs on whatever server it's running on.

In contrast, a web frontend's code runs in the user's web browser.

#### Latency

One problem here is latency.

You've already seen latency problems when using `fetch`. We used Promises or `async`/`await` to handle a latency problem. And we did things like set placeholder text while waiting for data from a `fetch`.

Imagine if you had a "dark mode" button on a website, but clicking it required making a request to a backend.

Depending on where the backend and the user are physically located, it may take anywhere between 1ms and 500ms for a request to go between them.

If every time you clicked on something on a web page you needed to talk to the backend, you may need to wait half a second just for the request to travel to the server and for the response to travel back, ignoring how long it takes to actually process the request. This would be unusably slow for many applications.

So sometimes we do work in a frontend to avoid needing to talk to a backend.

For instance, many websites that show you a list of information let you sort or filter the information. We often implement sorting in the frontend, so that we don't need to wait for a round-trip to a backend. Sometimes this even means we implement some functionality twice - once on the backend _and_ once on the frontend, so that on first page load the backend will do some sorting or filtering, but if you interact with the frontend, additional sorting or filtering may be done there.

#### Context

Because web frontends run in the user's own browser, they have easy access to lots of information about the user's computer. For instance, a user's browser knows what language to to use, what the user's time zone is, how big the browser window is, etc.

If our frontend code were instead running in a backend, the browser may need to include all of this information in every request it makes, just in case the backend needs to know it. This has a couple of drawbacks: It makes the requests bigger (which makes them slower, and maybe cost more), and it ends up sharing lots of data with the server that it may not need, which may compromise the user's privacy.

Imagine you're trying to buy a sofa. There are a few ways we could do this:
1. We could take the entire architectural plans for our home to a shop, and for each sofa work out where it could fit.
2. We could go to the shop, and for each sofa we may like, go home and see if the sofa would fit into our space.
3. We could measure the space we want to put it in, and then go to a shop where you can compare all of the available sofas against those measurements.

Approach 1 requires transferring a lot of data. Approach 2 requires a lot of traveling back and forth. Approach 3 tries to minimise both of these things.

### Pull not push

A backend lives at a well-known address - we know how to connect to it. A user's web browser does not.

This means that a backend cannot try to open a connection to a user's web browser. The web browser needs to initiate the request, and then the backend can reply to the request.

Once a web browser opens a request to a backend, there are some ways to keep a two-way communication channel open. But the very first request needs to come from the web browser.
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
+++
title = "Backend statefulness"
headless = true
time = 30
facilitation = false
emoji= "🧠"
objectives = [
"Identify whether some program is stateful or stateless.",
]
+++

Our example backend is {{<tooltip title="Stateless" text="stateless">}}Stateless means it doesn't keep state. It doesn't remember things between one request and the next. One request cannot impact another.{{</tooltip>}}. If you make several requests to it, it will always do the same thing (even though doesn't always return exactly the same result!).

If you made a request from a different computer, or from different country, or on a different day, it would keep doing exactly the same thing. If you restarted the server, it would keep doing exactly the same thing.

If our backend allowed users to add new quotes, it would start having _state_. It would need to remember what quotes had been added. It would be **stateful**.

```js
import express from "express";

const app = express();
const port = 3000;

const quotes = [
{
quote: "Either write something worth reading or do something worth writing.",
author: "Benjamin Franklin",
},
{
quote: "I should have been more kind.",
author: "Clive James",
},
];

function randomQuote() {
const index = Math.floor(Math.random() * quotes.length);
return quotes[index];
}

app.get("/", (req, res) => {
const quote = randomQuote();
res.send(`"${quote.quote}" -${quote.author}`);
});

app.post("/", (req, res) => {
const bodyBytes = [];
req.on("data", chunk => bodyBytes.push(...chunk));
req.on("end", () => {
const bodyString = String.fromCharCode(...bodyBytes);
let body;
try {
body = JSON.parse(bodyString);
} catch (error) {
console.error(`Failed to parse body ${bodyString} as JSON: ${error}`);
res.status(400).send("Expected body to be JSON.");
return;
}
if (typeof body != "object" || !("quote" in body) || !("author" in body)) {
console.error(`Failed to extract quote and author from post body: ${bodyString}`);
res.status(400).send("Expected body to be a JSON object containing keys quote and author.");
return;
}
quotes.push({
quote: body.quote,
author: body.author,
});
res.send("ok");
});
});

app.listen(port, () => {
console.error(`Quote server listening on port ${port}`);
});
```

Here we have added a new request handler. If someone makes a POST request to the path `/`, we try to interpret the body they posted as a JSON object.

If we can find a quote and author in it, we will store it in our list of quotes, and start serving it up to future requests.

If we can't process the request, we return an error describing what went wrong.

The details of exactly how we understand the request aren't important. The important thing is that we _are_ taking information from the request, and are then modifying the `quotes` array.

> [!NOTE]
> **State** is a general term that is used for related but different things in different contexts.
>
> State almost always refers to information that we store, which may change.

Because we're _modifying_ the `quotes` array, it is now state.
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
+++
title = "Chat application requirements"
headless = true
time = 30
facilitation = false
emoji= "📝"
objectives = [
"Explain the requirements of our chat application.",
"Explain what requirements are out of scope for our chat application.",
]
+++

You are going to make a chat application which lets multiple users exchange messages.

You've seen a similar application in the quote application. This time, you will be writing all of the code yourself.

Let's think about some requirements for our application:
* As a user, I can send add a message to the chat.
* As a user, when I open the chat I see the messages that have been sent by any user.
* As a user, when someone sends a message, it gets added to what I see.
* As a user, I can "like" or "dislike" someone's message.
* When messages are liked or disliked, a count of the likes and dislikes is displayed next to the message.
* As a user, I can schedule a message to be sent at some time in the future.
* As a user, I can change the colour messages that I send.
* As a user, I can make some words in my messages bold, italic, or underlined.
* As a user, I can indicate that my message is a reply to another message.

We can imagine other requirements too (e.g. reacting with emojis, being able to edit or delete messages, registering exclusive use of a username, ...). We will stick just to the requirements we've listed. In fact, the requirements we've listed are probably more than we have time to implement, so we will need to prioritise them and choose which ones we have time to build. Think about which requirements are _absolutely_ required - we will definitely need to build those!

Because users want to see things, we know we'll need a frontend.

Because multiple users want to be able to share information (messages), we know we'll need a backend for them to communicate via.

### What we already know and what's new

Some of these requirements are similar to the quote server we've already implemented:
* Adding messages is like adding quotes.
* Seeing messages is like seeing quotes.

Others are new:
* Live updates
* Interacting with a message
* Scheduling sending messages.
* Changing colour of messages.
* Formatting specific parts of a message.
* Replying to messages.

First let's make a backend and a frontend to do what we already know. This shouldn't take us very long (we know how to do it, and have done it recently). It will give us a useful framework to experiment with the things that are new to us.

If we thought it would take us a long time to do what we already know, we may approach this differently. We would probably try to work out the new things first. Because they may change how we want to do everything. But because it should be quick to do what we do know, we'll start there.
27 changes: 27 additions & 0 deletions common-content/en/module/decomposition/data-validation/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
+++
title = "Data validation"
headless = true
time = 60
facilitation = false
emoji= "🔎"
hidden_from_overview = true
objectives = [
"Explain the trade-offs of doing validation on the frontend or backend.",
]
+++

If we only do the validation on the backend, the user won't know anything is wrong until they submit the form.

If we only do validation on the frontend, it would be possible for users to add invalid quotes by using `curl` themselves, or building their own frontend.

So we normally do validation _twice_ - once in the frontend to give fast feedback (e.g. by adding the `required` attribute to an `<input>` tag), and once in the backend (to make sure no one can sneak bad data to us).

{{<note type="Exercise">}}
Add validation that authors and quotes are both non-empty, to both your frontend, and your backend.

Make sure when someone tries to post an empty quote, make sure they quickly know what's wrong.

Make sure if someone tries to use `curl` to post an empty quote, it doesn't get saved either.

If the backend rejects a request from the frontend, we should show the user why.
{{</note>}}
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
+++
title = "Deploying the chat application"
headless = true
time = 30
emoji= "📖"
[objectives]
1="Deploy all components of a frontend/backend/database application so it can be used on the internet"
time = 60
emoji= "➡️"
objectives = [
"Deploy a frontend and backend so it can be used on the internet.",
]
+++

### Deploying the chat application
{{<note type="Exercise">}}
Deploy your chat application so that both the frontend and backend are on the Internet, can talk to each other, and can be used by people.
{{</note>}}
Loading
Loading