This is the third time class
has been problematic. But now prototype
is not involved. The
syntax itself is making it hard to do refactoring with it, especially when
layered lifetime
is involved.
Did you just make up a new word?
Yes. Layered Lifetime, I Just Made Up That Phrase
Lifetime is literally when an object lives---the period between its creation and its deletion from the memory.
Specifically in TypeScript/JavaScript, an object's lifetime ends when nothing refers to it---when there are no references left; subsequently, non-referenced objects are removed by the GC, but the period between the reference count zeroing and the collection does not matter because nothing could be done with said objects.
"Layered lifetime" simply refers to a situation where there are objects whose lifetimes are diverse.
A perfect example of "Layered lifetime" would be a Page
that has a Modal
which is open only at times.
This example will be written in React.
const Page = () => {
const [modalOpen, setModalOpen] = useState(false);
return (
<Layout>
<Toggle onClick={setModalOpen((open) => !!open)} />
{modalOpen && <Modal />}
</Layout>
);
};
The above code says:
- "A
Page
contains aLayout
, aToggle
, and optionally aModal
" - "
Toggle
, when clicked, flipsmodalOpen
fromtrue
tofalse
, and vice versa." - "
Modal
is open only whenmodalOpen
istrue
."
Talking in terms of lifetimes:
Modal
lives within thePage
's lifetime;Modal
lifetime is shorter than thePage
's;Modal
cannot exist withoutPage
.Page
can exist withoutModal
.
The diversity of the lifetime is apparent here.
There are two objects Modal
and Page
, and their lifetimes differ.
Contrast this with the relationship of Page
and Layout
whose lifetime is the same.
Lifetime and Type's Interesting Relationship
Above 6 PM I tend to write very dumb code such as:
class User {
id?: string;
name: string;
address: string;
constructor(
name: string,
address: string
) { this.name = name; this.address = address }
setId(id: string) { this.id = id }
isInvalid() { return this.id === undefined; }
}
// On user input I want to create a "user"
function onUserInput(name: string, address: string) {
return new User(name, address);
}
// The product manager wants to only reserve some Id
// from a centralized-badly-architectured-single-threaded-obviously-strawman-database
// ONLY WHEN the user click a confirmation to "confirm their seriousness in registering"
async function onUserConfirm(user: User) {
user.setId(await reserveIdFromCentralizedDatabase())
return user;
}
// When the user actually submits
async function submitUser(user: User) {
// setId should be called before this
if(user.isInvalid()) throw new Error()
return await submitEntity("user", user);
}
The next morning, logic comes back to me.
I realized that User
without id
does not make sense.
So I refactored the code into:
class UserData {
public name: string;
public address: string;
constructor(name: string, address: string) {
this.name = name;
this.address = address;
}
}
class User extends UserData {
id: string;
constructor(id: string, userData: UserData) {
this.name = userData.name;
this.address = userData.address;
}
}
// On user input I want to create a "user"
function onUserInput(name: string, address: string) {
return new UserData(name, address);
}
// The product manager wants to only reserve some Id
// from a centralized-badly-architectured-single-threaded-obviously-strawman-database
// ONLY WHEN the user click a confirmation to "confirm their seriousness in registering"
async function onUserConfirm(userData: UserData) {
return new User(await reserveIdFromCentralizedDatabase(), userData);
}
// When the user actually submits
async function submitUser(user: User) {
// IMPORTANT: Now there's no need to call `invalid`
await submitEntity("user", user);
}
What I realize is that a User
's lifetime can't outlive its id
.
After the refactor, id
is not optional.
submitUser
does not need to validate the always-valid User
.
This way, the code represents entities' lifetimes' relationships more correctly.
Too Many Lines Of Codes
"You claim it is simpler. Why are there more lines of code now?" - some review
Whelp! Such is the price of correctness.
"Why don't you just use type
?" - some other review.
That's actually a great idea. Well, it was written in class, so I followed.
In hindsight, a class
without methods is wasteful.
type UserData = { name: string; address: string };
type User = UserData & { id: string };
const onUserInput = (name: string, address: string): UserData => ({
name,
address,
});
const onUserConfirm = async (data: UserData): User => ({
...data,
id: await reserveIdFromCentralizedDatabase(),
});
const submitUser = (user: User) => await submitEntity("user", user);
There you go!
Wait! Isn't this what "Functional Programming" looks like (or at least, advertised to look like)? It is one of the "evolutionary phases" of systemic-ts.