3

Let's say I have a sort method inside of my class and another class that has no relationship with that class needs the same method. Instead of writing that method twice and breaking the do not repeat yourself principle of OOP, I decide to make a class like it was an invention to solve that method calling it a Sorter. I've seen this done before and usually they contain more then one of common methods.

Is this a procedural design because it would hold a bunch of methods for all classes yet not be needed in a model of my solution or is it OOP because it is an algorithm/event I am modeling?

  • 2
    Depends on the language, the function being wrapped and the typical use cases of said function. In Java, everything has to be a class anyway. In many languages, functions can't be arguments or return values, but objects can be. – Ixrec Feb 3 '16 at 19:22
  • 4
    Food for thought: Don't Create Objects That End With -ER. – Roman Reiner Feb 3 '16 at 19:32
  • 1
    I gathered much insight into my question with that link. Perhaps, inventions to solve methods isn't really oop and is a procedural approach being hidden as OOP? Is this on the right track? – user275564 Feb 3 '16 at 19:48
  • 6
    See also: Execution in the Kingdom of Nouns – Daenyth Feb 3 '16 at 20:10
  • 2
    You are still using the terms "solve a method" and "like it was an invention". What does "solve a method" mean? What it is an "invention"? What does it mean for something to be "like an invention"? – Jörg W Mittag Feb 4 '16 at 23:48
1

As the question is tagged with Java I will focus on it in the answer:

The main reason for these classes were that Java did not have any simple method of passing a function as a parameter (e.g. C(++) has pointers to functions). One good example for when this is really annoying is a generic sorting method. The only way to pass a custom comparison method to the sorting method was to create a class that had a comparison function, like

interface ISorter {
    boolean compare(Object o1, Object o2);
}

class Sorter implements ISorter {
    boolean compare(Object o1, Object o2) { /* do the comparison */ }
}

[...]

Vector<Object> sort(Vector<Object> objs, ISorter sortBy) { 
     // do the sorting using the sortBy object
}

// actual call
Vector<Object> sorted = sort(objs, new Sorter());

This is, obviously, tedious as you had to define an interface as well as a class that implements that interface (which I will call function object). If you just wanted to change the sorting criterion from "<=" to ">=" this introduced a huge overhead.

But now lambdas to the rescue: These things allow you to define functions objects "on the fly". This means that you can write code similar to

sort(objsToSort, (Object o1, Object o2) -> o1 <= o2))
sort(objsToSort, (Object o1, Object o2) -> o1.length() >= o2.length()))
sort(objsToSort, (Object o1, Object o2) -> abritraryFunctionReturningComparison(o1,o2))

In these cases it is totally obvious what you are doing, no class needs to be used to guess what comparison will be used for sorting.

But this does not mean that having a class that is a "fake invention" is always wrong. If you want to specify some complex functionality, putting it in a separate class might be more appropriate. This is especially true if it implements a whole algorithm that uses temporary variables.

Concerning this topic you should, as has already mentioned, read this awesome blog post

5

Objects are models. They don't have to correspond to real-world objects. Sometimes actions need to be modelled.

Take, for example, the typical Bank Account Scenario, that is used in many introductory OO courses. The design that is taught looks a bit like this:

class BankAccount {
  Money balance;

  void deposit(Money amount) {
    balance += amount;
  }

  bool withdraw(Money amount) {
    if (balance < amount) { return false; }
    balance -= amount;
    return true;
  }

  bool transfer(Money amount, BankAccount target) {
    if (balance < amount) { return false; }
    withdraw(amount);
    target.deposit(amount);
    return true;
  }
}

So, as you can see, the balance is data and the transfer is an operation. Makes sense, right?

But there are a couple of problems. For example, if I have two bank accounts, and I want to transfer funds from one into the other, but both have a transfer method, why is it that the transfer method in the source account is the one to call? Why not the one in the target account?

Can we model this differently? Yes, we can! In fact, how is banking done "in the real world"? There is no money in your account. And when you transfer funds, there is no actual money taken out of your account and placed into a different account. Rather, a transaction slip is created, which lists the source account, target account, and amount. And at the end of the day, those transaction slips are all added together, and then we know, how much money is in each account.

Note how we have almost dualized the design now: the amount is no longer data, it is an operation, and the transfer is no longer an operation, it is data:

class Transaction {
  BankAccount source;
  BankAccount target;
  Money amount;
}

class BankAccount {
  Money balance() {
    // find all transaction slips which have `this` as either source or target
    // add all the amounts which have `this` as target
    // subtract all the amounts which have `this` as source
    return result;
  }

  void deposit(Money amount) {
    TransactionLog.append(new Transaction(CASH, this, amount));
  }

  void withdraw(Money amount) {
    TransactionLog.append(new Transaction(this, CASH, amount));
  }
}

This model has some advantages. Note: the fact that it is closer to how banking works "in the real world" is actually not necessarily an advantage. After all, we are building a software system, not a bank, we aren't constrained by physical realities, so the system doesn't have to mirror the real world.

For one: there is (almost) no mutation and no side-effects. All objects are immutable and all methods are referentially transparent. (I'm glossing over how TransactionLog.append might be implemented, though.) Adding asynchrony, concurrency, and parallelism to this system will be easier than to the first one. The TransactionLog leaves an audit trail of all transactions. This is generally important for banking. (You could e.g. add a user ID of the user who initiated the transaction to the Transaction objects.)

There is also no asymmetry in the transfer. It isn't even a method of the account anymore, it will now be a method of the banking system.

As you can see, we can arrive at two completely opposite designs for the exact same problem. Neither one of those is right or wrong. They are, as I said, models, and whether or not they are "good" models depends on the context they are going to be used in, and the important properties they are supposed to model.

At the veeeeery beginning of an OO course, we are sometimes taught the design method of simply writing down the use case and then underlining all the verbs and nouns. The nouns become objects, the verbs become methods. And the instructor will often teach this as if it was some infallible design method. But consider this: in English, you can verb a noun, and nouning of verbs is also possible. So, it really depends on how you write the use case down.

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