1

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>

    <script type="module">
      /* 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', () => this.#onCreateTodo())

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

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

        // Called when the "create TODO" 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 "toggle todo list visibility"
        // 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', () => {
        new View()
      })
    </script>
  </body>
</html>
7
  • 1
    "Hard-core OOP enthusiasts will tell you that you're not even supposed to make getters or setters [...]" I think your confusion stems from this statement which is not correct, or at least not precise enough. The issue is with systematically adding getters and setters to your classes, not getters and setters in general. Dec 7 '21 at 20:48
  • I think a better rule of thumb for architectural patterns like Model View Controller is Separation of Concerns. OOP principles are general principles, just like the SOLID principles. Like any principle, they work best when you balance them with practical concerns, rather than following them religiously just because someone says they're the "right way" to do things. Dec 7 '21 at 22:33
  • Encapsulation doesn't mean that an object should hide absolutely everything; an object has a set of public methods and associated data structures (that's what you exchange with the GUI). You encapsulate the internal representation (if any) & any extra internal state you need for computing things (if any). Encapsulation is embraced by OOP, but it's really a principle that goes beyond OOP, it's a core principle of modularization (separating things into isolated components), whether you use OOP or not. BTW, whatever those answers claimed, MVC is just a pattern, and it's very much an OO pattern Dec 7 '21 at 22:49
  • It's not that different from how you think of functions; a function has a narrowly defined job, encapsulates local variables and some computation, and provides a well-defined interface (parameters and the return type) so that the outside world can communicate with it. Some of those parameters can be other functions. Well, an object is the same thing, but on steroids - it has a narrowly defined job, encapsulates local data, and provides a well-defined interface (constructor, public methods) to interact with the outside world. Some of those methods can accept other methods or other objects. Dec 7 '21 at 22:55
  • 1
    The "printers instead of getters" seems to unnecessarily complicate things, and the first example "launders" it through string-named properties.
    – pjc50
    Dec 8 '21 at 9:06
-1

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.

24
  • 1
    "There is no way you can "separate" UI and "business logic" and still have encapsulation." This statement seems to rely on a wrong definition of what encapsulation is. The data a view needs to receive from the business logic is not the thing that is supposed to be encapsulated. What should be encapsulated is the private state, which by definition cannot contain "things an outsider (i.e. the view) needs to have".
    – Flater
    Dec 8 '21 at 9:35
  • 2
    The TodoList class is not the best implementation I've ever seen, but it encapsulates the array itself (#todos = []). This does actually highlight the value of encapsulation. If tomorrow the data is no longer stored in memory, but in another location (e.g. file storage, different project, ...), you only have to change the TodoList class, not the view itself. Had you not encapsulated the array in TodoList and instead just directly accessed it, this would not have been possible.
    – Flater
    Dec 8 '21 at 10:31
  • 1
    @ScottyJamison Encapsulation focuses on the separation between the public contract and the private implementation of said contract. By using proper encapsulation, any changes made to the private implementation are contained to the class itself, the consumers don't need to be updated. Your example of moving to async code changes the public contract. That is beyond the bounds of what encapsulation focuses on. If you change your public contract, then your consumers will be affected by this change. But that has nothing to do with encapsulation.
    – Flater
    Dec 8 '21 at 15:58
  • 1
    @Flater So consider this: Your "public contract" now includes the full data set, with its current structure, meaning, what each data field means, how it influences others, flags, dates, etc. You say that's cool, because you can change where to get the data from. Ok. So what about a "public contract" that does not contain the data. You get all the benefits from before, but now you can also change the data any way you please. Add new fields, add new meaning, new types of list items, etc. How is that not better? Dec 8 '21 at 16:20
  • 3
    While I know this answer is controversial, I'll mark it as accepted. My question was asked because I wanted to understand how those who hated getters/setters dealt with separating UI/business logic, which this answer clearly explains. Whether this sort of degree of encapsulation is actually part of OOP doctrine seems to be very debatable (as can be seen), but I know people out there do it, and this answer helps bring light to this perspective on programming. Dec 9 '21 at 6:19
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.

2
  • Thanks for this insight. Your overall point makes sense - if things were a little more complicated and I had already had a TodoItem class, then it would be simple to add and encapsulate the state of a marked todo item. Dec 8 '21 at 15:42
  • This did get me thinking: Let's say my TodoList class provided a simple getTodos() function, that returned a list of todos (instead of getInternalDetails()). Would you, at this time, create a micro SingleTodo class to encapsulate those individual strings (you pass the string in the constructor, and get it back out via getTodoText())? I feel most would consider that premature refactoring. Later, requirements change and we need to mark TODOs as done, and we don't have encapsulation to help us make this change. I guess all I'm saying is OOP's idea of encapsulation can't help in all scenarios. Dec 8 '21 at 15:43
1

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.

4
  • My question stemmed from trying to better understand a "pure" form of OOP, i.e. what OOP is like when people are strictly adhering to its principles, and aren't just lazily sprinkling bits of it over their application. The issue is, there's really isn't a solid definition of what OOP even is to begin with, and I ended up incorrectly interpreting extreme OOP perspectives as "the real OOP", and thought that everything else was just people not trying to adhere to OOP too strictly. ... Jan 4 at 1:35
  • ... I've realized my blunder now, and I think what you've answered is exactly right, those are the appropriate times to use getters and setters, and there's nothing wrong with using them. Jan 4 at 1:35
  • I also probably should have worded that phrase you quoted more clearly, what I intended is that, with encapsulation, we're subdividing ownership of different types of states between different classes. A TodoItem class describes the type of state a TodoItem can have, along with how the public is allowed to touch that state, while a TodoListItem describes the state a TodoList would have. Jan 4 at 1:45
  • Maybe even the concept of a "pure" form of OOP does not exist. OOP originally emerged as a way to handle complexity. People writing procedural code will some day realize that they have created a big block of spaghetti they cannot maintain anymore. OOP is a way to isolate parts of your code and make it reusable, but it takes alway a bit more effort to design a class hierarchy. So you will have to think if the effort pays back or not. There is a ToDo-Example in OO style here you can check out: efpage.de/DML/DML_homepage/page.html?idx=8
    – Eckehard
    Jan 4 at 15:28

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.