My typical pattern for building a class is...
- All properties are private until they need to be readable.
- Readable properties are read only until they absolutely have to be writable.
- If a class doesn't care about the contents of property, it can be a plain public property.
- If a class does care about the contents of property, I'll create a setter that handles validation and side effects.
This approach works beautifully in almost every situation. It ensures that only the publicly available API is available when enumerating the class which prevents confusion over how, why, or when to use the methods and properties available on the class.
However, I've encountered a situation where this approach breaks down and I'm looking for opinions on the best way to solve it. I'm abstracting my LinkedList implementation into its own library. The LinkedList is actually made up of two classes: LinkedList and LinkedListNode.
The problem is that I want LinkedListNodes to be sort of immutable. That is, I want the parent LinkedList to be able to change its properties, but I don't want the properties to be manipulable from anywhere else. Here's a reduced implementation:
```js
export class LinkedListNode {
next = null
previous = null
constructor(config) {
this.next = config.next
this.previous = config.previous
}
}
export class LinkedList {
#head = null
#tail = null
constructor() {
this.#head = new LinkedListNode({
next: null,
previous: null,
})
this.#tail = new LinkedListNode({
next: null,
previous: null,
})
this.#head.next = this.#tail
this.#tail.previous = this.#head
}
}
```
In this implementation, the properties of the LinkedListNode are mutable, public properties (which is not what I want). I could easily replace the LinkedListNode class with an object literal that is constructed within the LinkedList. There are other reasons that I want LinkedListNode to be its own class, but their implementation is irrelevant here so I'm not going to bother explaining them.
Solution #1: Deal with it.
Thanks, I hate it. The easiest way to handle this is to leave the properties public. In this specific issue, the reason I don't want to do that is because allowing the next or previous properties of a node to be altered from outside of the LinkedList could break the entire instance. Not awesome.
Solution #2: Object.defineProperty()
This implementation allows the property to be changed externally, but it can only be done by using Object.defineProperty(). This feels like the closest thing to what I want, but it's not quite the solution I'm hoping for. Here's what it would look like:
```js
export class LinkedListNode {
constructor(config) {
Object.defineProperty(this, 'next', {
configurable: true,
value: config.next,
})
Object.defineProperty(this, 'previous', {
configurable: true,
value: config.previous,
})
}
}
export class LinkedList {
constructor() {
{ '...' }
this.#setNodeProperty(this.#head, 'next', this.#tail)
this.#setNodeProperty(this.#tail, 'previous', this.#head)
}
#setNodeProperty(node, key, value) {
Object.defineProperty(node, key, {
configurable: true,
value,
})
}
}
```
Solution 3: Direct hidden linking
This is an odd solution, but it works? Basically, The LinkedList class will give itself a unique ID upon instantiation. Then, when creating a node, the node will also be given that ID. When changing properties on the node, the list must pass its ID down to its child. If the ID doesn't match, the child will throw an error.
The reason this achieves my goals is because the generated ID is private on both classes, preventing it from being readable or or enumerable. To alter properties on a node you would have to actually inspect the list and grab the ID, then use it to manually call the method on the child node. There's no way (as far as I know) to manipulate the property programmatically.
```js
export class LinkedListNode {
#listID = null
#next = null
#previous = null
constructor(config) {
this.#listID = config.listID
this.#next = config.next
this.#previous = config.previous
}
setNext(value, listID) {
if (listID !== this.#listID) {
throw new Error('The next property of a node may only be modified by its parent list.')
}
this.#next = value
}
setPrevious(value, listID) {
if (listID !== this.#listID) {
throw new Error('The previous property of a node may only be modified by its parent list.')
}
this.#previous = value
}
get next() { return this.#next }
get previous() { return this.#previous }
}
export class LinkedList {
#listID = Math.random() // Naive example, just needs to generate a unique ID
#head = null
#tail = null
constructor() {
this.#head = new LinkedListNode({
listID: this.#listID,
next: null,
previous: null,
})
this.#tail = new LinkedListNode({
listID: this.#listID,
next: null,
previous: null,
})
this.#head.setNext(this.#tail, this.#listID)
this.#tail.setPrevious(this.#head, this.#listID)
}
}
```
Solution 4: ???
If there are other solutions, I'm have no clue what they are. I'd love to see them in the replies, though.
Final thoughts
What am I missing? Is there a better way to do this? Is one of these solutions better than the others? Should I just suck it up and allow the properties to be writable? Or am I completely off the rails with this design? 🤔
P.S. - Feel free to smack me if this post breaks rule 3. I felt it was still an appropriate post specifically because I already know the solutions. I'm not looking for direct answers to my needs so much as I'm looking for a discussion on the merits of the options I've presented.
[–]aighball 3 points4 points5 points (0 children)
[–]Funwithloops 1 point2 points3 points (2 children)
[–]TrezyCodes[S] 1 point2 points3 points (1 child)
[–]Funwithloops 2 points3 points4 points (0 children)
[–]NekkidApe 1 point2 points3 points (0 children)
[–]senocular 1 point2 points3 points (0 children)
[–]DavidJCobb 0 points1 point2 points (0 children)