Should I expose an instance's state with methods?

I've read somewhere about a pattern that is as follows: if it'll change the state of the instance, the method should be named with a verb and return void; and if the method just returns something (computed or simply a property), it should be named as a noun. E. g.: dog.walk() returns void and changes the instance's state. dog.barkVolume() will return an int and changes nothing. Another example is dog.runAfterTail() returns void and changes the state while dog.name() will return and string also and change nothing. As you can see, the instance's state is exposed through methods instead of getters or simply the property. When I see properties being exposed through a method (named with a noun), it seems like it's returning an immutable value (since I am talking about JS/TS, it would be something like return [...this.array_typed_property]) and when using a plain property or getter, it would return something like return this.property (possible to be mutated with .pop()). Would you guys agree with that and recommend it (or not)? I have this impression over methods because they seems more "closed", differently from a property, which seems to be exposing the state of the instance as if I were going into the instance's bowls and taking it from there. Also, is there a name for that pattern? As per request in the comments, this is what I have so far. Things are not very well structured and the readonlys are only enforced on dev mode since I can't Object.freeze some arrays because they're use along with Vue, which can't watch changes on a freezed object. In these examples, another doubt of mine would be how to properly expose properties that do change in the No class. For example, the property name won't change, ever, so it seems fine to simply add a readonly, but it also would feel safer to freeze it and maybe expose trough a getter or method. Or, maybe, expose it with a method like .toDTO() which would return an object of all the non-changing properties of my class, freezed. import type { TreeviewItem } from '@/app/ui/props.d' import { No } from '@/app/ui/treeview/no' export class Tree { private readonly _nodes: Array private _focused_node: No | null = null private _selected_node: No | null = null public constructor(items?: TreeviewItem) { this._nodes = items ? items.map( (item, i) => new No({ ...item, address: item?.address ?? [i] }, null, this) ) : [] } public nodes(): ReadonlyArray { return this._nodes } public focusedNode(): No | undefined { return this._focused_node ?? undefined } public selectedNode(): No | undefined { return this._selected_node ?? undefined } public visibleNodes(): ReadonlyArray { const visible_nodes: Array = [] const plain = (nos: Array) => { nos.forEach(no => { visible_nodes.push(no) plain(no.visibleChildren()) }) } plain(this._nodes) return Object.freeze(visible_nodes) } public focusNode(no: No): void { this._focused_node = no } public isChildNodeFocused(address: number[]): boolean { if (!this._focused_node) { return false } return this._focused_node.addressToString().startsWith(address.join('-')) } public goUp(): void { const visible_nodes = this.visibleNodes() const index = visible_nodes.findIndex(n => n === this._focused_node) if (index > 0) { this.focusNode(visible_nodes[index - 1]) } } public goDown(): void { const visible_nodes = this.visibleNodes() const index = visible_nodes.findIndex(n => n === this._focused_node) if (index < visible_nodes.length - 1) { this.focusNode(visible_nodes[index + 1]) } } public openOrFocusFirstChild(): void { if (!this._focused_node || this._focused_node.isLeaf()) { return } this._focused_node.isOpen() ? this.focusNode(this._focused_node.nodes[0]) : this._focused_node.toggleOpen() } public closeOrFocusParent(): void { if (!this._focused_node) { return } this._focused_node.isOpen() ? this._focused_node.toggleOpen() : this.focusNode(this._focused_node.parent()!) } public goFirstNode(): void { const visible_nodes = this.visibleNodes() if (visible_nodes.length > 0) { this.focusNode(visible_nodes[0]) } } public goLastNode(): void { const visisible_nodes = this.visibleNodes() if (visisible_nodes.length > 0) { this.focusNode(visisible_nodes[visisible_nodes.length - 1]) } } public closeAll(): void { const fecharNos = (nos: No[]) => { nos.forEach(no => { if (!no.isLeaf() && no.isOpen()) { no.toggleOpen() } fecharNos(no.nodes) }) } fecharNos(this._nodes) this.goFirstNode() } public selectNode(no: No): void { this._selected_node = no this.focusNode(no) } public closeAllLeaves(): ReadonlyArray { const leaves: Array = [] const planificar =

Jun 9, 2025 - 22:30
 0

I've read somewhere about a pattern that is as follows: if it'll change the state of the instance, the method should be named with a verb and return void; and if the method just returns something (computed or simply a property), it should be named as a noun. E. g.:

dog.walk() returns void and changes the instance's state. dog.barkVolume() will return an int and changes nothing. Another example is dog.runAfterTail() returns void and changes the state while dog.name() will return and string also and change nothing.

As you can see, the instance's state is exposed through methods instead of getters or simply the property. When I see properties being exposed through a method (named with a noun), it seems like it's returning an immutable value (since I am talking about JS/TS, it would be something like return [...this.array_typed_property]) and when using a plain property or getter, it would return something like return this.property (possible to be mutated with .pop()).

Would you guys agree with that and recommend it (or not)?

I have this impression over methods because they seems more "closed", differently from a property, which seems to be exposing the state of the instance as if I were going into the instance's bowls and taking it from there.

Also, is there a name for that pattern?

As per request in the comments, this is what I have so far. Things are not very well structured and the readonlys are only enforced on dev mode since I can't Object.freeze some arrays because they're use along with Vue, which can't watch changes on a freezed object.

In these examples, another doubt of mine would be how to properly expose properties that do change in the No class. For example, the property name won't change, ever, so it seems fine to simply add a readonly, but it also would feel safer to freeze it and maybe expose trough a getter or method. Or, maybe, expose it with a method like .toDTO() which would return an object of all the non-changing properties of my class, freezed.

import type { TreeviewItem } from '@/app/ui/props.d'
import { No } from '@/app/ui/treeview/no'

export class Tree {
  private readonly _nodes: Array
  private _focused_node: No | null = null
  private _selected_node: No | null = null

  public constructor(items?: TreeviewItem) {
    this._nodes = items
      ? items.map(
          (item, i) =>
            new No({ ...item, address: item?.address ?? [i] }, null, this)
        )
      : []
  }

  public nodes(): ReadonlyArray {
    return this._nodes
  }

  public focusedNode(): No | undefined {
    return this._focused_node ?? undefined
  }

  public selectedNode(): No | undefined {
    return this._selected_node ?? undefined
  }

  public visibleNodes(): ReadonlyArray {
    const visible_nodes: Array = []
    const plain = (nos: Array) => {
      nos.forEach(no => {
        visible_nodes.push(no)
        plain(no.visibleChildren())
      })
    }
    plain(this._nodes)
    return Object.freeze(visible_nodes)
  }

  public focusNode(no: No): void {
    this._focused_node = no
  }

  public isChildNodeFocused(address: number[]): boolean {
    if (!this._focused_node) {
      return false
    }

    return this._focused_node.addressToString().startsWith(address.join('-'))
  }

  public goUp(): void {
    const visible_nodes = this.visibleNodes()
    const index = visible_nodes.findIndex(n => n === this._focused_node)
    if (index > 0) {
      this.focusNode(visible_nodes[index - 1])
    }
  }

  public goDown(): void {
    const visible_nodes = this.visibleNodes()
    const index = visible_nodes.findIndex(n => n === this._focused_node)
    if (index < visible_nodes.length - 1) {
      this.focusNode(visible_nodes[index + 1])
    }
  }

  public openOrFocusFirstChild(): void {
    if (!this._focused_node || this._focused_node.isLeaf()) {
      return
    }

    this._focused_node.isOpen()
      ? this.focusNode(this._focused_node.nodes[0])
      : this._focused_node.toggleOpen()
  }

  public closeOrFocusParent(): void {
    if (!this._focused_node) {
      return
    }

    this._focused_node.isOpen()
      ? this._focused_node.toggleOpen()
      : this.focusNode(this._focused_node.parent()!)
  }

  public goFirstNode(): void {
    const visible_nodes = this.visibleNodes()
    if (visible_nodes.length > 0) {
      this.focusNode(visible_nodes[0])
    }
  }

  public goLastNode(): void {
    const visisible_nodes = this.visibleNodes()
    if (visisible_nodes.length > 0) {
      this.focusNode(visisible_nodes[visisible_nodes.length - 1])
    }
  }

  public closeAll(): void {
    const fecharNos = (nos: No[]) => {
      nos.forEach(no => {
        if (!no.isLeaf() && no.isOpen()) {
          no.toggleOpen()
        }
        fecharNos(no.nodes)
      })
    }

    fecharNos(this._nodes)
    this.goFirstNode()
  }

  public selectNode(no: No): void {
    this._selected_node = no
    this.focusNode(no)
  }

  public closeAllLeaves(): ReadonlyArray {
    const leaves: Array = []

    const planificar = (nos: Array) => {
      nos.forEach(no => {
        if (no.isLeaf()) {
          leaves.push(no)
        }
        planificar(no.nodes)
      })
    }
    planificar(this._nodes)

    return Object.freeze(leaves)
  }

  public nodeLikeName(nome: string): No | undefined {
    const plain = (nodes: Array): No | undefined => {
      for (const node of nodes) {
        if (node.name.includes(nome)) {
          return node
        }
        const encontrado = plain(node.nodes)
        if (encontrado) {
          return encontrado
        }
      }
      return undefined
    }

    return plain(this._nodes)
  }

  public nodeByAddress(endereco: number[]): No | undefined {
    let node: No | undefined = this._nodes[endereco[0]]

    for (let i = 1; i < endereco.length; i++) {
      if (!node) {
        return undefined
      }
      node = node.nodes[endereco[i]]
    }

    return node
  }

  public toPlainObject(): TreeviewItem {
    const transformRecursively = (nodes: Array): TreeviewItem => {
      return nodes.map(node => ({
        address: node.address,
        badge: node.badge,
        expanded: node.isOpen(),
        items: transformRecursively(node.nodes),
        loading: node.isLoading(),
        title: node.name,
        value: node.value,
      }))
    }

    return transformRecursively(this._nodes)
  }

  public openNode(endereco: Array): void {
    let node: No | undefined = this._nodes[endereco[0]]

    for (let i = 1; i < endereco.length; i++) {
      if (!node) {
        return undefined
      }
      node.toggleOpen()
      node = node.nodes[endereco[i]]
    }
  }

  public openParentNode(): ReadonlyArray {
    const parents: Array = []

    const getOpenParents = (nodes: ReadonlyArray) => {
      for (const node of nodes) {
        if (node.isOpen() && !node.isLeaf()) {
          parents.push(node)
        }

        if (!node.isLeaf()) {
          getOpenParents(node.nodes)
        }
      }
    }

    return Object.freeze(parents)
  }
}
import type { BadgeProps, TreeviewItem } from '@/app/ui/props.d'
import type { Tree } from '@/app/ui/treeview/tree'

export class No {
  private readonly _tree: Tree
  private readonly _parent: No | null
  private _open: boolean
  private _loading: boolean
  public readonly address: Array
  public readonly name: string
  public readonly nodes: Array
  public readonly badge?: BadgeProps
  public readonly value: any

  public constructor(
    data: TreeviewItem[number],
    parent: No | null,
    tree: Tree
  ) {
    this.address = data.address
    this.name = data.title
    this.nodes = data.items
      ? data.items.map(
          (item, i) =>
            new No({ ...item, address: [...data.address, i] }, this, tree)
        )
      : []
    this._open = data.expanded ?? false
    this.badge = data.badge
    this._parent = parent
    this._tree = tree
    this.value = data.value
    this._loading = data.loading ?? false
  }

  public isOpen(): boolean {
    return this._open
  }

  public isLoading(): boolean {
    return this._open
  }

  public parent(): No | null {
    return this._parent
  }

  public close(): void {
    this._open = false
  }

  public toggleOpen(): void {
    if (!this.isLeaf()) {
      if (!this._open) {
        this.nodes.forEach(n => n.close())
      }

      this._open = !this._open

      if (!this._open && this._tree.isChildNodeFocused(this.address)) {
        this._tree.focusNode(this)
      }
    }
  }

  public isLeaf(): boolean {
    return this.nodes.length === 0
  }

  public isExpanded(): boolean {
    return this._open && !this.isLeaf()
  }

  public addressToString(): string {
    return this.address.join('-')
  }

  public visibleChildren(): Array {
    return this._open ? this.nodes : []
  }

  public addNodes(data: TreeviewItem): void {
    data.map(d => this.nodes.push(new No(d, this, this._tree)))
  }

  public toggleLoading(): void {
    this._loading = !this._loading
  }
}