Building the Question and Answer Models
Modeling Question and Answer#
The Question
and Answer
classes form the backbone of our program. Together they represent the questions and answers users have created.
Questions can have multiple answers but only one chosen answer, and an answer can belong to only one question. The diagram below shows the relationship between Question
, Answer
, and the other entities in our program:

We have a circular dependency between Question
and Answer
, so we can implement either of them first. It's important we hold off on running either of them in isolation until both are implemented to avoid any runtime errors (i.e. Answer
calling Question
's getSummary()
method before it's been implemented).
Building the Answer Model#
Our initial Answer
implementation keeps track of an answer's ID, author, text, question, and attachments:
import Summary from "./Summary";
import Unique, { UniqueId } from "./Unique";
import User from "./User";
import Question from "./Question";
import getUniqueId from "../utils/getUniqueId";
import Attachment from "./Attachment";
class Answer implements Unique, Summary {
private id: UniqueId;
private author: User;
private text: string;
private question: Question;
private attachments: Attachment[];
constructor(
author: User,
question: Question,
text: string,
attachments: Attachment[] = [], // default parameter
) {
this.id = getUniqueId();
this.question = question;
this.attachments = attachments;
this.text = text;
this.author = author;
}
getId(): UniqueId {
return this.id;
}
// TODO: implement getSummary();
getSummary(): string {
return '';
}
}
export default Answer;
We use a default parameter to initialize the attachments
parameter to an empty array if none is provided. Users should be able to add attachments to questions, so we need an addAttachment()
method that adds items to the attachments
array using Array.prototype.push()
as shown below:
addAttachment(attachment: Attachment) {
this.attachments.push(attachment);
}
Updating an answer's text is simple as we can reassign the private text
property to whatever string is passed to the method:
setText(text: string) {
this.text = text;
}
Generating a Summary#
To generate an answer's summary, we outsource part of the logic to User
and Question
by calling their respective getSummary()
methods. We combine those summaries with a truncated version of the answer's text:
getSummary(prefix: string = ''): string {
const author = this.author.getSummary(' - ');
const question = this.question.getSummary(' - ');
const maxTextLength = 40;
const textSnippet = this.text.length >= maxTextLength
? `${this.text.substring(0, maxTextLength - 3)}...`
: this.text;
const lines = [
`Answer: ${textSnippet}`,
`Attachments: ${this.attachments.length}`,
'Author: ',
author,
'Question: ',
question,
];
return lines
.map(line => `${prefix}${line}`)
.join('\n');
}
If the answer's text is longer than 40 characters, we take the first 37 characters using the String.prototype.substring()
method and append three dots to the text to ensure the truncated string's length is also 40 characters long.
For generating a multi-line summary, we use the same strategy we used in Image.ts
: Array.prototype.join()
converts an array of strings into one big string where each item in the array becomes a new line in the final string.
The author and question sumaries are prefixed with -
(a dash surrounded by one space on both sides) to make it easier to see that they live under a heading.
Below is a sample answer summary:
Answer: We can use JSON.stringify() to conver
Attachments: 1
Author:
- Nick (0.1337) [2 qs, 3 ans]
Question:
- Question: How can a JSON object be serialized?
- Author:
- - Alex (0.4242) [10 qs, 2 ans]
- Answers: 3
- Has an answer been chosen? No
One of the key benefits behind the way we've built getSummary()
is that the Question
and User
classes can add and remove lines in their summaries without having to touch code in the Answer
class.
An Answer
object is associated with the user that opened the answer by assigning the user to the answer's this.author
property, but how will we ensure that the answer is added to the author's question list? And that the answer is added to the question's answers list? One possibility is to do it manually as shown below:
const question = new Question(/* params */);
const user = new User(/* params */);
const answer = new Answer(/* params */);
// manually associate user and question with answer
user.addAnswer(answer);
question.addAnswer(answer);
Improving addAnswer#
The problem with that approach is that we have no way of enforcing that User.prototype.addAnswer()
and Question.prototype.addAnswer()
are called every time an answer is created, so we could end up in a weird state where answers think they're associated with users and questions but the users and questions don't know the answers exist. To ensure that users and questions are always associated with an answer, we should make this association in the Answer
constructor by calling the User
and Question
's addAnswer()
method directly:
constructor(
author: User,
question: Question,
text: string,
attachments: Attachment[] = [], // default parameter
) {
this.id = getUniqueId();
this.attachments = attachments;
this.text = text;
this.question = question;
this.question.addAnswer(this); // 'this' points to the Answer object
this.author = author;
this.author.addAnswer(this); // 'this' points to the Answer object
}
Adding Private Setters#
To clean things up a bit we introduce two private methods, setQuestion()
and setAuthor()
that hold the extra logic of associating questions and users with the answer:
constructor(
author: User,
question: Question,
text: string,
attachments: Attachment[] = [], // default parameter
) {
this.id = getUniqueId();
this.attachments = attachments;
this.setText(text);
this.setQuestion(question);
this.setAuthor(author);
}
This page is a preview of Beginners Guide to TypeScript