2

I've been trying to get a better understanding of OOP (I'm not the biggest fan of it, but I still want to understand it).

One of the core principles of OOP is encapsulation - you're supposed to subdivide your state between different classes and make it so the only way to work with that state is via the public functions you expose. Hard-core OOP enthusiasts will tell you that you're not even supposed to make getters or setters, because they cause you to blatantly expose the private state that you're trying so hard to encapsulate; instead, you're supposed to move any logic that depends on that state into the class (I don't yet understand how this wouldn't bloat your classes beyond-end, perhaps they're using some refactoring trick I'm currently unaware of - maybe they subdivide the classes using friend classes or something).

Next, when building a UI, it's often said that one should keep the UI and business logic separated. I wholeheartedly agree with this advice, and apply it all of the time. I've been experimenting around with trying to build UIs using OOP principles, but have been struggling to figure out how to live by this principle, while simultaneously encapsulating private state - the two seem to be at odds with each other.

  • All of my state is within the business logic, encapsulated in classes
  • If the view ever needs to rebuild itself, it'll need access to that state being encapsulated
  • Under normal circumstances, I suppose this means I should put the relevant UI building logic into the class, so that it's able to access the private state it needs. However, this is a clear violation of "separation of UI and business logic".
  • So, what else can be done? Do OOP-enthusiasts just have to lose out on this wonderful separation principle? Or do they have some other trick up their sleeves that allows them to keep the two sides separated?

I'll briefly mention another similar question posted on this webpage that I found while researching this topic. The user was wondering how MVC and OOP are able to fit together. Many answers explain that MVC works at a much higher abstraction layer than OOP, so the principles of OOP don't really apply there, and the two are able to live together. I fail to see how this really works in practice though, so I'd like to present a concrete example to help this discussion.

Here's a simple TODO app. You can add TODOs to a list, and you can toggle the visibility of the list. That's it. It's written in JavaScript, which I know has a limited set of OOP tools available to it, so you're welcome to explain how it would be done, if JavaScript was more full-featured in this regard.

Update: I've now aggressively commented the code below, so that those who are less familiar with JavaScript can still follow along.

<html>
  <body>
    <button id="toggle-list">Toggle list visibility</button>
    <div id="todo-list-container"></div>
    <input type="text" id="new-todo-input">
    <button id="create-todo">Create</button>
&lt;script type=&quot;module&quot;&gt;
  /* BUSINESS LOGIC/STATE */
  class TodoList {
    #todos = []
    add(todoItem) {
      this.#todos.push(todoItem)
    }

    // Bad! (according to strict OOP)
    // This completly violates encapsalation!
    getInternalDetails() {
      return { todos: this.#todos }
    }
  }

  /* VIEW */
  class View {
    #todoList = new TodoList()

    // Grabbing elements from the DOM
    #todoInput = document.querySelector('#new-todo-input')
    #todoListContainer = document.querySelector('#todo-list-container')
    #todoListBox = null

    constructor() {
      // Attaching event listeners to buttons
      const createTodoButton = document.querySelector('#create-todo')
      createTodoButton.addEventListener('click', () =&gt; this.#onCreateTodo())

      const toggleListButton = document.querySelector('#toggle-list')
      toggleListButton.addEventListener('click', () =&gt; this.#onToggleList())

      // Making the TODO list visible by default
      this.#onToggleList()
    }

    // Called when the &quot;create TODO&quot; button is pushed.
    #onCreateTodo() {
      // Grabs the TODO item from the textbox
      // and puts it into the list of TODOs
      // (if the list is currently visible)
      const todoItem = this.#todoInput.value
      this.#todoInput.value = ''
      if (this.#todoListBox) {
        const previousContent = this.#todoListBox.value === '' ? '' : this.#todoListBox.value + '\n'
        this.#todoListBox.value = previousContent + todoItem
      }
      // Notifies the business-logic side that a
      // new TODO has been added
      this.#todoList.add(todoItem)
    }

    // Called when the &quot;toggle todo list visibility&quot;
    // button is pushed.
    #onToggleList() {
      if (!this.#todoListBox) {
        // Creates the todo-list DOM element
        this.#todoListBox = document.createElement('textarea')
        this.#todoListBox.readonly = true
        this.#todoListBox.style.height = '200px'
        // Grabs the list of TODOs from the business-logic
        // side, so that we can populate this list.
        this.#todoListBox.value = this.#todoList.getInternalDetails().todos.join('\n')
        this.#todoListContainer.append(this.#todoListBox)
      } else {
        // Destroys the todo-list DOM element
        this.#todoListBox.parentNode.removeChild(this.#todoListBox)
        this.#todoListBox = null
      }
    }
  }

  // When the page has finished loading, we instantiate the view.
  globalThis.addEventListener('DOMContentLoaded', () =&gt; {
    new View()
  })
&lt;/script&gt;

</body> </html>

3 Answers3

5

One of the core principles of OOP is encapsulation - you're supposed to subdivide your state between different classes and make it so the only way to work with that state is via the public functions you expose. Hard-core OOP enthusiasts will tell you that you're not even supposed to make getters or setters, because they cause you to blatantly expose the private state that you're trying so hard to encapsulate; instead, you're supposed to move any logic that depends on that state into the class

Encapsulation does not mean that all state must be inaccessible from outside the class holding the state. It means that private state must remain private, but public state is allowed to be exposed through getters and setters.

To go with your TODO example, the whole purpose of TodoList is to contain a list of TODO items. That means that the list of items is public state that the object can pass on to outside itself (preferably in a read-only way).

Lets extend your example with the possibility to mark TODO items as completed and being able to still see them in the list.

The UI should be able to mark a Todo item as completed, retrieve if an item is completed and when it was marked as completed. That is all public state of a Todo item. What is private state is how exactly a Todo item stores the information if it was completed or not. That could be a flag and a date field, but it could also be a nullable date field (if the date is null, the todo is not completed, if it is non-null, the item is completed).

Here, the UI is not using a setter to mark the Todo item as completed. Rather it is telling the Todo item to mark itself as completed in whatever way it should do so.

0

it's often said that one should keep the UI and business logic separated

It is often said indeed. It's also completely wrong, at least how it is then often implemented.

As you noted, publishing your internal data so that someone else may do something with them is the exact opposite of encapsulation. So short answer: You can't. There is no way you can "separate" UI and "business logic" and still have encapsulation.

I've heard people trying to defend having both, trying to argue for exceptions, trying to change the meaning of words, or just giving up and not caring. The solution, if you want one, is to think about why we would want any of that, instead of thinking about dogma.

We want encapsulation because we want to have things that change together in one place. Encapsulation is the thing that allows us to do that consistently. So if you change the Todo List to store the todos differently, or introduce new "fields", or introduce different types of todos, etc., do you want to go out in the code and find all the places where you use the data?

We can actually separate the details of UI from the details of the "business logic". A Todo list should know it is presented, and should know to present itself, present widgets to modify itself, etc. It does not need to know how that happens in detail, with what colors, what layout, etc. But it has to know how to present itself in different scenarios, if we want encapsulation.

Abstraction is the thing that keeps objects small, even when you include some notion of presentation in them. If you want to see how this works in real life, you'll need to search a lot. There are very few good examples out there. Or, you can try yourself.

As an OO exercise, if you're building an application (so it is self-contained, as opposed to a library), you can actually do that without any getters at all. And I mean "getters" as in any access to objects in a method that already existed before the method was called, and is not an instance variable of this object or a parameter.

0

One of the core principles of OOP is encapsulation - you're supposed to subdivide your state between different classes...

I think there is a lot of confusion about OOP here. First of all: classes have no state, only instances of a class (=objects) can have a "state". A class defines the state variables, but this is only a template. They need to be instatiated to hold any values. The values of the (internal) state variables represent the "state" of an object. As each instance contains it´s own variable set, each instance has it´s own "state".

Encapsulation means, that each object has to care for it´s own state:

  • If a state variable needs some constrains to be useful, you implement setters to prevent external callers to set invalid values.
  • If a state change needs to trigger some action, you will implement a setter to call the appropriate functions
  • If a state value needs to be generated or controlled somehow, you can implement a getter to perform the necessary action.

Let assume you have a visual element with a state variable "color". A change from RED to GREEN has to be reflected in the visual representation. Setters are convenient to implement the necessary actions. But if a state change does not do anything else but change the value of the state variable, you can simply expose the state variable to the public.

Usually you do not subdivide your state, but you build a useful class hierarchy that defines the state variables and handle state changes. The state is encapsulated by one ore more objects that are designed to work together. If you need things like serialization of the object states, this is usually implemented in deeper layers of the parent classes to enable all objects to export their state in an appropriate form.

In larger OOP-hierarchies like the DOM, most of the complexity is covered below a relatively simple surface like the HTML_DOM_API. All the complex interactions a browser has to do to reflect the state change of a single property of a DOM element is encapsulated deep inside the base classes of the DOM.

Ok, modern browsers like CHROME are written in C++, but the principles are the same.

Using design patterns of functional programming to create OOP applications will probably not be very successful. There is definitively a different thinking behind both aproaches. OOP thinks more in functional units, while FP uses a more explicit way to deal with states. Both approaches have their pro´s and con´s and may shine more in the one or the other case.