My question is how I can achieve more encapsulation in TypeScript.
I have a class Item
, with a public setter isOwned
, but I only want to call this method in specific situations: if the item is picked up or dropped by a Player
.
/** @package A */
class Item {
private _is_owned: boolean = false
// ...
/** @returns - Has this item been picked up by a player? */
get isOwned(): boolean {
return this._is_owned
}
/** @param owned - Is a player picking up this item? */
set isOwned(owned: boolean) {
this._is_owned = owned
}
}
/** @package B */
class Player {
private _items: Set<Item> = new Set()
// ...
/**
* @param item - the item in question
* @returns - Does this player own the item?
*/
owns(item: Item): boolean {
return this._items.has(item)
}
/**
* @param item - the item to pick up
* @throws - if the item is already owned
*/
pickUp(item: Item): void {
if (item.isOwned) throw new Error('cannot pick up item that is already owned')
this._items.add(item)
item.isOwned = true // WARNING public method
}
/**
* @param item - the item to drop
* @throws - if this player does not own the item
*/
drop(item: Item): void {
if (!this.owns(item)) throw new Error('cannot drop item that player does not own')
this._items.delete(item)
item.isOwned = false // WARNING public method
}
}
The methods Player#pickUp
and Player#drop
need to check the ownership of an item to determine whether the player is allowed to pick up or drop the item. So in those methods, the setter Item#isOwned
is called, and therefore it needs to be public. (In TypeScript, there is no such thing as package-private, and even if there were, Player
and Item
are not in the same package.)
I want that setter to be called only in those methods though, because there are security implications: Since Item#isOwned
is public, a “hacker” could bypass the checks before the #pickUp
and #drop
methods, as so:
/**
* Allow a player to pick up an item,
* even if that item is owned by another player.
*/
function hack(player1: Player, player2: Player, item: Item): void {
player1.pickUp(item)
try {
player2.pickUp(item)
} catch (e) {
item.isOwned = false // <!-- this is very bad
player2.pickUp(item)
}
}
I’ve tried moving the methods to the Item
class, but that won’t work because the methods need to access the private field Player#_items
.
Then I tried separating each method into two: one in class Player
where the item is added/deleted to the player, and another in class Item
where ownership is tested, but that gives the same problem (just with different method names).
It seems like no matter what I do, getting/setting the ownership of an item needs to be public, so any program that calls Player#pickUp
and Player#drop
will also be able to get/set the item’s ownership status.
So how do I solve this problem? Is there a different data structure I need to use, or do I need to rethink my entire strategy?
private
orpublic
does not affect security in any way that matters. – Christian Hackl Feb 18 '19 at 14:43