A class exposes a collection field directly (getter returning the internal array, public field, etc.) — callers can mutate the collection from outside.
A class's internal collection is never returned directly; callers add or remove via methods on the class, and reads return a snapshot or iterator.
Before the refactoring
class Person {courses;getCourses() { return this.courses; }}
After the refactoring
class Person {#courses = [];courses() { return [...this.#courses]; }enroll(course) { this.#courses.push(course); }drop(course) { this.#courses = this.#courses.filter(c => c !== course); }}
Invariants on the collection (size, uniqueness, ordering) can't be enforced; bugs hide where one consumer mutates the collection in a way another assumed wouldn't happen.
Returning a shallow copy on every read can hide bugs where callers expected mutation to be reflected — be explicit about the contract.
The owner can enforce invariants (uniqueness, ordering, max size); refactoring the collection's internal shape is local.
Returning a shallow copy on every read without telling callers — they expect mutations to be reflected, get silent no-ops, and bugs appear in unexpected places.