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>