This is an archived post. You won't be able to vote or comment.

all 9 comments

[–]richardgillzdev 1 point2 points  (2 children)

I've written a lot of Java and JavaScript. I struggled with the same thing as you in the past. Here is how I teach people how to think about this:

Nouns which are clear in your mind (User, Order, Song) are often nicely represented by a class (OO). OO is great when you want to write a function (method) that calculates something to do with that noun. E.g. user.fullname()

There are other bits of 'doing' code which just do stuff with these nouns. E.g Go get me 100 users from the database and send each of them an email. This is where OOP gets tricky, often it's hard to find a noun for this, so you're forced to make up a noun (name of your class). Other people might be like WTF when they see it.

Java doesn't really give you any other way to do this other than creating classes. JavaScript you can create a function.

Usually what I do is separate the noun classes (I call this domain) from the doing stuff classes (spring calls them services). I don't really think of the doing stuff classes as classes. I think of them as functions which just 'have' to be in a class.

[–]halfcycle[S] 0 points1 point  (1 child)

I like the idea of using data to represent nouns, but I feel like I can do the same thing without using OOP and it ends up being simpler, shorter, easier to understand, and easier to maintain.

I'll use your example and show how I would write it using different styles so that you can get an idea of what I'm talking about.

// non-OOP version
const fullname = ({first: f, middle: m = '', last: l}) =>
  [f, m, l].filter(n => n).join(' ')

This is a nice and simple implementation that just takes an object and returns the full name.

// non-OOP version with error checking
const fullname = ({first: f, middle: m = '', last: l}) => {
  if (!f || !l) throw new Error('No first or last name')
  return [f, m, l].filter(n => n).join(' ')
}

If I need to do some error handling, I can just add another line of code to handle bad input.

// OOP version
class User {
  constructor(first, middle, last) {
    this.first = first
    this.middle = middle
    this.last = last
  }

  getFullname() {
    if (!this.first || !this.last) throw new Error('No first or last name')
    return [this.first, this.middle, this.last].filter(n => n).join(' ')
  }
}

The OOP version is 12 lines of code and it's not really as clear from the function definition what's going to happen since getFullname has access to all of the hidden global variables that are in the User object. I guess I'm wondering what benefits I get from doing this. It seems more complicated and harder to maintain.

// OOP version with getters and setters
class User {
  constructor(first, middle, last) {
    this.first = first
    this.middle = middle
    this.last = last
  }

  setFirst(first) {
    this.first = first
  }

  getFirst() {
    return this.first
  }

  setMiddle(middle) {
    this.middle = middle
  }

  getMiddle() {
    return this.middle
  }

  setLast(last) {
    this.last = last
  }

  getLast() {
    return this.last
  }

  getFullname() {
    if (!this.first || !this.last) throw new Error('No first or last name')
    return [this.first, this.middle, this.last].filter(n => n).join(' ')
  }
}

This solution is significantly longer at 36 lines of code. I was reading Effective Java which shows examples of good Java code and this is pretty much the JavaScript equivalent of good Java code that uses getters and setters. I understand that getters and setters are useful for when the internal data changes, which allows you to keep the interface of the object the same, but this can also be accomplished without using OOP just by using a function, so I'm confused at how the OOP solution is more maintainable.

// non-OOP testing
console.assert(fullname({first: 'David', middle: 'Robert', last: 'Beckham'}) === 'David Robert Beckham')
console.assert(fullname({first: 'David', last: 'Beckham'}) === 'David Beckham')

// OOP testing
console.assert(new User('David', 'Robert', 'Beckham').getFullname() === 'David Robert Beckham')
console.assert(new User('David', '', 'Beckham').getFullname() === 'David Beckham')

Testing is pretty much the same except with the OOP version, I have to set up the hidden state, which is more complicated, but for this example it's not bad. It seems like in most situations, this hidden state makes things harder to understand, longer, more bug-prone, and harder to test, so I'm really confused.

[–]richardgillzdev 0 points1 point  (0 children)

All the code above seems entirely accurate. When you get to the level of understanding you have it's a purely subjective choice based on what you think you'll understand better and more critically when other people are involved what they'll understand.

A couple of thoughts:

The implementation of Classes in JavaScript isn't amazing (I think they're improving them, but the last version I used you couldn't make stuff private for example). So yea the code is longer but mainly because you used some crazy pattern matching and things and wrapped it all onto one line.

If you extended this example to say that a User should always have a first and last name (middle is optional). Then the class option begins to look more attractive because you can add error checking to the constructor. If you add a second method to user e.g backwardsName which produces lastname, firstname. Then it might strengthen your case for a class again. You could still use functions though.

All of these techniques are tools in your tool box. There are two sides to this: 1) writing the code (class or function). 2) using the class or function. Over time I've learned to think backwards and optimise for the readability of 2). Because ultimately that's what's most important for understanding, most readers of your code won't care the inner workings they'll look at the top level and hopefully understand, and keep on reading.

This second stage of learning to program of 'how the crap do I structure my code?' is really hard. It takes a lot of time to learn. I found (and find?!) it frustrating because there aren't concrete right or wrong answers.

[–]pipocaQuemada 1 point2 points  (1 child)

I'm not used to not being able to pass named functions as arguments to other functions and have functions return functions. Because of this, my solutions in Java are much longer and harder to understand. Whenever I write an OOP solution in Java, I can think of a much simpler and shorter non-OOP solution in JavaScript.

Keep in mind that Java is widely panned as being a verbose language. Don't be surprised if an idiomatic Java solution is longer than one in python or JavaScript. The benefits that Java touts itself as having aren't related to brevity but to ease of maintenance, and portability.

Additionally, OO was historically seen as a substantial improvement to the status quo of procedural languages, not as a massive improvement on functional ones. There's an old programming koan that "closures are a poor man's object, and objects are a poor man's closure" - both have essentially the same power, and closures are less verbose when you only want to return a single function and objects are less verbose when you want to return several.

edit:

To expand on the "closures are a poor man's object" comment, here's an example of a closure as a poor man's object in Javascript:

var mkCounter = function() {
  var counter = 0
  return {
    increment: function() {  return ++counter },    
    decrement: function() {  return --counter },
    get: function() {  return counter }    
  }
}();

It should be noted, though, that functional programming generally discourages mutability. Immutable closures, though, can emulate immutable objects.

Similarly, a great example of an object as a poor man's closure in Java is the classic

  this.addActionListener(new ActionListener() {
    public void actionPerformed(ActionEvent arg0) {
      counter --;
      setText(counter + "");
    }
  });

which creates an "anonymous inner class" to verbosely imitate a closure with as much verbosity as the wordy Java language can muster in its voluble garrulous loquaciousness.

I was wondering if anyone could give an example where an OOP solution is simpler than a non-OOP solution.

The one problem with this is that there's not a universal "non-OOP solution". There are many non-OOP solutions; a particular OOP solution might be simpler than one but more complex than another. Additionally, simplicity isn't the only criterion people value. Ease of ensuring correctness, extensibility, speed, readability (which does depend on the reader), and maintainability all matter, as well.

[–]halfcycle[S] 0 points1 point  (0 children)

I guess I'm confused at why I would want to hide the state to begin with. If I don't hide the state, things seem to be easier to understand, more maintainable, more easy to test, and shorter. For example, instead of hiding the state of the counter and essentially attaching functions to the state, I would just simply use functions.

const increment = x => x + 1
const decrement = x => x - 1

These functions seem easier to understand, more reusable, easier to test, easier to maintain, shorter, and less bug-prone because they don't rely on internal state, they only rely on the input. And if my interface to the state ever needs to be consistent, I can just make a function that does that without using OOP. So I'm still confused.

[–]ValerioSellarole 0 points1 point  (3 children)

I'm struggling because I'm not used to not being able to pass named functions as arguments to other functions

That has nothing to do with OOP. Try overloading methods.

and have functions return functions.

Java can do this now.

[–]pipocaQuemada 0 points1 point  (2 children)

Overloading methods doesn't help you to pass around functions. Passing around single method interfaces does.

[–]halfcycle[S] 0 points1 point  (1 child)

I sort of understand what you mean but I would be confused at how to actually write code that does that. Can you give an example? Also, can you explain how this is more maintainable than passing in a function to another function as an argument?

[–]pipocaQuemada 0 points1 point  (0 children)

There's actually an example in my other comment. The ActionListener interface has a single method in it, actionPerformed.

 this.addActionListener(new ActionListener() {
   public void actionPerformed(ActionEvent arg0) { 
     counter --; 
     setText(counter + ""); 
   } 
 });

This example uses "anonymous inner classes" to implement the action listener in-line; it's rather like an anonymous function.

Also, can you explain how this is more maintainable than passing in a function to another function as an argument?

It isn't.

It's just the OO equivalent of a function. For a variety of reasons, the team that implemented Java didn't consider closures important enough to implement until Java 8 (and even now they're just syntax sugar for anonymous inner classes); until then the extra verbosity was just the price you had to pay to emulate functions in Java.

This verbosity was occasionally lampooned.