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>

    <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>
  • 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. – Vincent Savard Dec 07 '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. – Robert Harvey Dec 07 '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 – Filip Milovanović Dec 07 '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. – Filip Milovanović Dec 07 '21 at 22:55
  • So, it sounds like all of you are saying that, while encapsulation is an important part of OOP, the notion that [some](https://medium.com/codex/why-getters-setters-arent-oop-use-this-technic-instead-665c05c310e4) [people](https://www.yegor256.com/2016/04/05/printers-instead-of-getters.html) put out that "getters and setters are always evil" is a false notion and not a part of OOP. Perhaps it's accurate to call them a "code smell" - you need to make sure you're not unnecessarily publishing private data. But, it's not inherently wrong or evil. – Scotty Jamison Dec 07 '21 at 23:05
  • (and yes, I also get that religiously following any ideology too far is generally a bad thing, and that some of these people are certainly promoting an unhealthy adherence to practices that shouldn't be followed so strictly, but I'm still wanting to understand where they're coming from, assuming what they put out is actually part of OOP literature and not just some made-up fairy-tail that doesn't scale) – Scotty Jamison Dec 07 '21 at 23:14
  • 1
    The "printers instead of getters" seems to unnecessarily complicate things, and the first example "launders" it through string-named properties. – pjc50 Dec 08 '21 at 09:06

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.

Bart van Ingen Schenau
  • 71,712
  • 20
  • 110
  • 179
  • 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. – Scotty Jamison Dec 08 '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. – Scotty Jamison Dec 08 '21 at 15:43
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.

Robert Bräutigam
  • 11,473
  • 1
  • 17
  • 36
  • 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 08 '21 at 09:35
  • Why is the "view" considered an outsider? Is it not part of the same project? I mean in this case it clearly is. So what makes it an "outsider"? Do you mean all other objects are outsiders? And if so, do you mean all state is private, unless I need it, in which case it isn't? – Robert Bräutigam Dec 08 '21 at 10:03
  • Encapsulation is class based, not project based. Whether the view is part of the same project or not is irrelevant. _"And if so, do you mean all state is private, unless I need it, in which case it isn't?"_ As long as you don't extend that into "I could need/want anything", that's a reasonably accurate summation. The business logic lives to serve the todo list data, which obviously means that this data is part of its public contract and not some private implementation detail. The latter should be encapsulated, the former should not. – Flater Dec 08 '21 at 10:08
  • Ok, so what does `TodoList` encapsulate in the example? – Robert Bräutigam Dec 08 '21 at 10:23
  • 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 08 '21 at 10:31
  • So then all instance variables that are contained in an object are "encapsulated" in the object? Things are always "encapsulated" regardless of what methods I define? – Robert Bräutigam Dec 08 '21 at 11:29
  • _Private_ (or protected, for that matter) instance variables are by definition encapsulated as they are shielded from an outside consumer. There might be a method that passes the value that is stored in that field, but **the field itself** remains encapsulated in that case. The goal of encapsulation is to hide **the implementation details** of the class, not (necessarily) the values contained in those implementation details. So yes, the array is encapsulated, even though its content is given to outside consumers using the (badly named) `getInternalDetails` method. – Flater Dec 08 '21 at 11:34
  • Alright, I got your definition then. A private instance variable is always encapsulated, regardless of what methods are available, even if there is a public getter that returns it as-is. Ok. I think that is a pretty useless definition, but I understand. The ironic thing is, the name `getInternalDetails()` is _exactly_ right. :) The OP gets it. – Robert Bräutigam Dec 08 '21 at 12:47
  • @Flater - you said the class still achieves encapsulation by letting me choose where I get my data from. I'm not sure this is actually true. getInternalDetails() can't just turn async in JavaScript without all callers being updated to "await" it. Perhaps I could do the request at object construction time (which would require updating anyone constructing the class to use await). But, from what I see, I would get the same level of encapsulation if I just tossed my class and used a public array (how I normally do it). I could still switch to network requests just as easily at any point. – Scotty Jamison Dec 08 '21 at 15:02
  • Even with non-async tasks, it still seems like I would have just as much encapsulation if my todos array was on a public state object (how I normally program these sorts of things, redux-style). If I wanted at some point to make that value derived from some other piece of state, then I guess that's a little harder to do if I'm not using classes w/ getters functions, but not impossible. I would be able to use property-getter syntax, to trigger a function call when someone tries to get the value of a particular property (but, those always leave a bad taste in my mouth, I dislike their magic). – Scotty Jamison Dec 08 '21 at 15:09
  • @ScottyJamison _"Even with non-async tasks, it still seems like I would have just as much encapsulation if my todos array was on a public state object"_ If your consumers were to directly use the array field, you would never be able to move away from using that array field as your data source. When accessing the data via `getInternalDetails`, you are able to change the method body as you please. Whether it fetches the data from the private field, calls another dependency (e.g. repository) to fetch the data, calls an external API, ... This gives you the freedom to change your implementation. – Flater Dec 08 '21 at 15:56
  • 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 08 '21 at 15:58
  • @ScottyJamison: Overall, be aware that "I could just do [this]" does not necessarily mean "[this] is an approach of equal quality". You _could_ use a static and publically accessible array and your code would work, but it would be a significant code smell and be very resistant to future changes and maintenance. "It works" is not the be-all-end-all of how we should approach development. – Flater Dec 08 '21 at 16:00
  • 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? – Robert Bräutigam Dec 08 '21 at 16:20
  • @Flater *you would never be able to move away from using that array field as your data source.* You can, I probably just explained how to do so poorly, so I'll just let [the docs](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/get) speak for themselves. I do agree that a class does a better job than this (magic) language feature at achieving this effect. – Scotty Jamison Dec 08 '21 at 16:22
  • 1
    @Robert Bräutigam - There's a tradeoff. You're correct that it's easier to update the way you store your data, but you're forced to move any logic that touches that data to be in the same class. This can cause algorithms to be spread across many classes, sub-dividing an algorithm into lots of tiny pieces. If you want to "encapsulate" the algorithm instead, keeping it as an atomic unit that you find in one place, you'll need to make your data a little more public. So, here's the big question - what's better to encapsulate, an algorithm, or private data? Perhaps it depends on the situation. – Scotty Jamison Dec 08 '21 at 16:25
  • As a concrete example, take [this](https://codereview.stackexchange.com/questions/256691/javascript-ood-2048) from the code-review stack exchange. It's a 2048 game that they wish to make more OOP. If it's designed with less encapsulation, then it's trivial to make the tile-moving logic in a single location, and for anyone reading the code to understand how tiles move by looking at the one function. If you need to update the algorithm, you touch one function only. Separating it into a Board and BoardCell class would force the algorithm to be smeared across two classes. – Scotty Jamison Dec 08 '21 at 16:31
  • Do you really want to understand how a tile is moving? Like what instructions it takes in detail? I just spent 5 minutes looking at the code, I still don't quite know what it's doing. OOP is not just random stuff. It's a way to organize code, to divide and conquer, but also to make it more readable and maintainable. Let go trying to control and build on cooperation and delegation. – Robert Bräutigam Dec 08 '21 at 16:55
  • @RobertBräutigam - I spent forever on that person's question trying to figure out how to divide it into multiple classes without overcomplicating it, and couldn't figure out how. If you think you have a good solution, I would love to see an answer from you over there, I'm intrigued at how you would approach this problem. I'm sure the O.P. would be grateful as well, because I don't think either answer really helped them very much with writing OOP code. – Scotty Jamison Dec 08 '21 at 16:59
  • 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. – Scotty Jamison Dec 09 '21 at 06:19
  • @RobertBräutigam: _"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?"_ Because it defeats the purpose of encapsulation. When the data changes structurally, the consumer has to adapt to those changes. Therefore, there is nothing gained from removing the data's structure from what you would consider to be the public contract, as any change made to the data's structure inevitably impacts the consumer. – Flater Dec 10 '21 at 11:40
  • 1
    @Flater I think you're missing the point. There is no "consumer". You don't have access to the data. At all. _That_ is what some of us refer to as "encapsulation". – Robert Bräutigam Dec 10 '21 at 11:53
  • @RobertBräutigam: I'm all for DTO mapping, but you're somewhat missing the mark here. The example code does not reveal where the data structure is defined, because that's JS and its dynamic typing for you. As of right now, there is no underlying layer on which the `TodoList` class depends, so the reasonable assumption here is that the data structure resides on the same layer as `TodoList` does, therefore making it the DTO in the DTO mapping, not the source data. DTOs are part of the public contract, as it defines how the consumer should interact with and expect from the returned value. [..] – Flater Dec 10 '21 at 12:05
  • @RobertBräutigam: I do agree with you that if there is an underlying dependency, that _its_ data structure should not pass through the `TodoList` app without getting remapped onto a DTO, so as to ensure that `TodoList` is properly encapsulated. – Flater Dec 10 '21 at 12:06
  • This is such a beautiful idea, I wonder how pragmatic that is in practice, and how far encapsulation can be pushed. I've been trying to push it lately, and I find it hard to define what's internal state and what isn't. It seems the line can be drawn between properties and "computed properties", although it seems a bit arbitrary.. EG: `country.isoCode` vs `country.asText(locale)`. There is a lot of similarity with biology in this idea too. – Ced Nov 03 '22 at 06:08
  • @Ced I've been developing complex, data-processing as well as UI containing applications this way for years now. I mean having _all_ state "encapsulated", i.e. having no getters nor setters at all. So it definitely can be "pushed" that far and I think it's not only practical, it is a must for maintainability. Note, this applies to self-contained applications. Libraries, where the end-use-case is not known, may need to publish data. You can't include a behavior if you don't know the behavior. – Robert Bräutigam Nov 03 '22 at 10:06
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.

Eckehard
  • 17
  • 6
  • 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. ... – Scotty Jamison Jan 04 '22 at 01: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. – Scotty Jamison Jan 04 '22 at 01: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. – Scotty Jamison Jan 04 '22 at 01: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: https://efpage.de/DML/DML_homepage/page.html?idx=8 – Eckehard Jan 04 '22 at 15:28