11

Reading through a scathing article on the downsides of OOP in favour of some other paradigm I've run into an example that I can't find too much fault with.

I want to be open to the author's arguments, and although I can theoretically understand their points, one example in particular I'm having a hard time trying to imagine how it would be better implemented in, say, a FP language.

From: http://www.smashcompany.com/technology/object-oriented-programming-is-an-expensive-disaster-which-must-end

// Consider the case where “SimpleProductManager” is a child of
// “ProductManager”:

public class SimpleProductManager implements ProductManager {
    private List products;

    public List getProducts() {
        return products;
    }

    public void increasePrice(int percentage) {
        if (products != null) {
            for (Product product : products) {
                double newPrice = product.getPrice().doubleValue() *
                (100 + percentage)/100;
                product.setPrice(newPrice);
            }
        }
    }

    public void setProducts(List products) {
        this.products = products;
    }
}

// There are 3 behaviors here:

getProducts()

increasePrice()

setProducts()

// Is there any rational reason why these 3 behaviors should be linked to
// the fact that in my data hierarchy I want “SimpleProductManager” to be
// a child of “ProductManager”? I can not think of any. I do not want the
// behavior of my code linked together with my definition of my data-type
// hierarchy, and yet in OOP I have no choice: all methods must go inside
// of a class, and the class declaration is also where I declare my
// data-type hierarchy:

public class SimpleProductManager implements ProductManager

// This is a disaster.

Note that I am not looking for a rebuttal for or against the writer's arguments for "Is there any any rational reason why these 3 behaviours should be linked to the data hierarchy?".

What I'm specifically asking is how would this example be modelled/programmed in a FP language (Actual code, not theoretically)?

closed as primarily opinion-based by amon, gnat, jwenting, Pieter B, MSalters May 11 '17 at 8:18

Many good questions generate some degree of opinion based on expert experience, but answers to this question will tend to be almost entirely based on opinions, rather than facts, references, or specific expertise. If this question can be reworded to fit the rules in the help center, please edit the question.

  • 44
    You can't reasonably expect to compare any programming paradigms on such short and contrived examples. Anyone here can come up with code requirements that make their own preferred paradigm look better than rest, especially if they implement other improperly. Only when you have real, big, changing project can you gain insights in strengths and weaknesses of different paradigms. – Euphoric May 10 '17 at 19:55
  • 20
    There's nothing about OO programming which mandates that those 3 methods should go together in the same class; similarly there's nothing about OO programming which mandates that behaviour should exist in the same class as data. That is to say, with OO Programming you can put data in the same class as behaviour, or you can split it out to a separate entity/model. either way, OO has nothing really to say about how data should relate to an object, since the concept of an object is fundamentally concerned with modelling behaviour by grouping logically-related methods into a class. – Ben Cottrell May 10 '17 at 20:00
  • 20
    I got 10 sentences into that rant of an article and gave up. Pay no attention to the man behind that curtain. In other news, I had no idea that True Scotsmen were primarily OOP programmers. – Robert Harvey May 10 '17 at 20:28
  • 11
    Yet another rant from someone who writes procedural code in an OO language, then wonders why OO is not working for him. – TheCatWhisperer May 10 '17 at 21:45
  • 11
    Though it is undoubtedly true that OOP is a disaster of design missteps from start to finish -- and I'm proud to be a part of it! -- this article is unreadable, and the example you give is basically making the argument that a poorly designed class hierarchy is poorly designed. – Eric Lippert May 10 '17 at 22:19
42

In FP style, Product would be an immutable class, product.setPrice would not mutate a Product object but return a new object instead, and the increasePrice function would be a "standalone" function. Using a similar looking syntax like yours (C#/Java like), an equivalent function could look like this:

 public List increasePrice(List products, int percentage) {
    if (products != null) {
        return products.Select(product => {
                double newPrice = product.getPrice().doubleValue() *
                    (100 + percentage)/100;
                return product.setPrice(newPrice);     
               });
    }
    else return null;
}

As you see, the core is not really different here, except the "boilerplate" code from the contrived OOP example is omitted. However, I don't see this as evidence that OOP leads to bloated code, only as evidence for the fact if one constructs a code example which is sufficiently artificial enough, it is possible to prove anything.

  • 7
    Ways to make this "more FP": 1) Use Maybe / Optional types instead of nullability to make it easier to write total functions instead of partial functions and use higher-order helper functions to abstract away "if (x != null)" logic. 2) Use lenses to define increasing price for a single product in terms of applying a percentage increase in the context of a lens on the product's price. 3) Use partial application / composition / currying to avoid an explicit lambda for the map / Select call. – Jack May 10 '17 at 21:29
  • 6
    Gotta say I hate the idea of a collection could be null instead of simply empty by design. Functional languages with native tuple/collection support operate that way. Even in OOP I hate returning null where a collection is the return type. /rant over – Berin Loritsch May 11 '17 at 0:34
  • But this can be a static method like in a utility class in OOP languages like Java or C#. This code is shorter partly because you ask to pass in the list and not hold it yourself. The original code also holds a data structure and just moving it out would make the original code shorter without a change in concepts. – Mark May 11 '17 at 0:58
  • @Mark: sure, and I think the OP knows this already. I understand the question as "how to express this in a functional way", not mandatory in a non-OOP language. – Doc Brown May 11 '17 at 6:08
  • @Mark FP and OO don't exclude eachother. – Pieter B May 11 '17 at 7:04
17

What I'm specifically asking is how would this example be modelled/programmed in a FP language (Actual code, not theoretically)?

In "a" FP language? If any suffices, then I pick Emacs lisp. It does have the concept of types (kind of, sort of), but only built-in ones. So your example reduces to "how do you multiply each item in a list by something and return a new list".

(mapcar (lambda (x) (* x 2)) '(1 2 3))

There you go. Other languages will be similar, with the difference that you get th benefit of explicit types with the usual functional "matching" semantics. Check out Haskell:

incPrice :: (Num) -> [Num] -> [Num]  
incPrice _ [] = []  
incPrice percentage (x:xs) = x*percentage : incPrice percentage xs  

(Or something like that, it has been ages...)

I want to be open to the author's arguments,

Why? I tried to read the article; I had to give up after a page and just quickly scanned the rest.

The problem of the article is not that it's against OOP. Neither am I blindly "pro OOP". I have programmed with logic, functional and OOP paradigms, quite often in the same language when possible, and frequently without any of the three, purely imperative or even on the assembler level. I would never say that any of those paradigms is vastly superiour to the other in every aspect. Would I argue that I like language X better then Y? Of course I would! But that is not what that article is about.

The problem of the article is that he uses an abundance of rhetoric tools (fallacies) from the first to the last sentence. It is completely futile to even begin to describe all the errors it contains. The author makes it abundantly clear that he has zero interest in discussion, he is on a crusade. So why bother?

At the end of the day all those things are just tools to get a job done. There may be jobs where OOP is better, and there may be other jobs where FP is better, or where both are overkill. The important thing is to pick the right tool for the job, and get it done.

  • 4
    "abundantly clear that he has zero interest in discussion, he is on a crusade" Have an upvote for this gem. – Euphoric May 11 '17 at 3:32
  • Don't you need a Num constraint on your Haskell code? how can you call (*) otherwise? – jk. May 11 '17 at 8:10
  • @jk., it has been ages that I did Haskell, that was just to satisfy the OP's constraint for the answer he is looking for. ;) If someone wants to fix my code, feel free. But sure, I'll switch it to Num. – AnoE May 11 '17 at 12:49
6

The author made a very good point then chose a lackluster example to attempt to back it up. The complaint isn't with the implementation of the class, it's with the idea that the data hierarchy is inextricably coupled with the function hierarchy.

It follows then, that to understand the author's point, it wouldn't help to only see how he would implement this single class in a functional style. You would have to see how he would design the entire context of data and functions around this class in a functional style.

Think about the potential data types involved in products and pricing. To brainstorm a few: name, upc code, category, shipping weight, price, currency, discount code, discount rule.

This is the easy part of object-oriented design. We just make a class for all of the above "objects" and we're good, right? Make a Product class to combine a few of them together?

But wait, you can have collections and aggregates of some of those types: Set[category], (discount code -> price), (quantity -> discount amount), and so forth. Where do those fit in? Do we create a separate CategoryManager to keep track of all the different kinds of categories, or does that responsibility belong to the Category class we already created?

Now what about functions that give you a price discount if you have a certain quantity of items from two different categories? Does that go in the Product class, the Category class, the DiscountRule class, the CategoryManager class, or do we need something new? This is how we end up with things like DiscountRuleProductCategoryFactoryBuilder.

In functional code, your data hierarchy is completely orthogonal to your functions. You can sort your functions in whatever manner makes semantic sense. For example, you might group all the functions that change the prices of products together, in which case it would make sense to factor out common functionality like the mapPrices in the following Scala example:

def mapPrices(f: Int => Int)(products: Traversable[Product]): Traversable[Product] =
  products map {x => x.copy(price = f(x.price))}

def increasePrice(percentage: Int)(price: Int): Int =
  price * (percentage + 100) / 100

mapPrices(increasePrice(25))(products)

I could probably add other price-related functions here like decreasePrice, applyBulkDiscount, etc.

Because we also use a collection of Products, the OOP version needs to include methods to manage that collection, but you didn't want this module to be about product selection, you wanted it to be about prices. The function-data coupling forced you to throw collection management boilerplate in there too.

You can try to solve this by putting the products member in a separate class, but then you end up with very tightly-coupled classes. OO programmers think of function-data coupling as very natural and even beneficial, but there is a high cost associated with it in loss of flexibility. Anytime you create a function, you must assign it to one and only one class. Anytime you want to use a function, you must find a way to get its coupled data to the point of use. Those restrictions are huge.

2

Simply separating the data and function as the author was alluding to could look like this in F# ("a FP language").

module Product =

    type Product = {
        Price : decimal
        ... // other properties not mentioned
    }

    let increasePrice ( percentage : int ) ( product : Product ) : Product =
        let newPrice = ... // calculate

        { product with Price = newPrice }

You can perform a price increase on a list of products this way.

let percentage = 10
let products : Product list = ...  // load?

products
|> List.map (Product.increasePrice percentage)

Note: If you are not familiar with FP, every function returns a value. Coming from a C-like language, you can treat the last statement in a function as though it had a return in front of it.

I included some type annotations, but they should be unnecessary. getter/setter are unnecessary here since the module doesn't own the data. It owns the structure of the data and the available operations. This can be seen with List as well, which exposes map to run a function on every element in the list, and returns the result in a new list.

Notice that the Product module doesn't have to know anything about looping, as that responsibility stays with the List module (who created the need for looping).

1

Let me preface this with the fact that I'm not an expert on functional programming. I'm more of an OOP person. So while I'm pretty sure the below is how you would accomplish the same sort of functionality with FP, I could be wrong.

This is In Typescript (hence all the type annotations). Typescript (like javascript) is a multi-domain language.

export class Product extends Object {
    name: string;
    price: number;
    category: string;
}

products: Product[] = [
    new Product( { name: "Tablet", "price": 20.99, category: 'Electronics' } ),
    new Product( { name: "Phone", "price": 500.00, category: 'Electronics' } ),
    new Product( { name: "Car", "price": 13500.00, category: 'Auto' } )
];

// find all electronics and double their price
let newProducts = products
    .filter( ( product: Product ) => product.category === 'Electronics' )
    .map( ( product: Product ) => {
        product.price *= 2;
        return product;
    } );

console.log( newProducts );

In detail (and again, not an FP expert), the thing to understand is that there isn't a lot of pre-defined behavior. There isn't an "increase price" method that applies a price increase across the whole list, because of course this isn't OOP: there is no class in which to define such behavior. Instead of creating an object that stores a list of products, you just create an array of products. You can then use standard FP procedures to manipulate this array in whatever way you desire: filter to select particular items, map to adjust internals, etc... You end with more detailed control over your list of products without having to be limited by the API that the SimpleProductManager gives you. This may be considered an advantage by some. It is also true that you don't have to worry about any baggage associated with the ProductManager class. Finally, there is no worry about "SetProducts" or "GetProducts", because there is no object that is hiding your products: instead, you just have the list of products that you are working with. Again, this may be an advantage or disadvantage depending on the circumstances/person you are talking to. Also, there is obviously no class hierarchy (which is what he was complaining about) because there are no classes in the first place.

I didn't take the time to read his entire rant. I do use FP practices when it is convenient, but I am definitely more of an OOP kind of guy. So I figured since I answered your question, I would also make some brief comments about his opinions. I think this is a very contrived example that highlights the "downsides" of OOP. In this particular case, for the functionality shown, OOP probably is over-kill, and FP would probably be a better fit. Then again, if this were for something like a shopping cart, protecting your products list and limiting access to it is (I think) a very important goal of the program, and FP has no way to enforce such things. Again, it may just be that I'm not an FP expert, but having implemented shopping carts for e-commerce systems, I would much rather use OOP than FP. The encapsulation principles in OOP give you much more control to make sure that things are modified properly, that you always move from one valid state to another, and that your shopping cart is always properly up-to-date.

Personally I have a hard time taking anyone seriously that makes such a strong argument for "X is just terrible. Always use Y". Programming has a variety of tools and paradigms because there are a wide variety of problems to solve. FP has its place, OOP has its place, and no one is going to be a great programmer if they can't understand the drawbacks and advantages of all our tools and when to use them.

**note: Obviously there is one class in my example: the Product class. In this case though it is simply a dumb data container: I don't think my use of it violates the principles of FP. It is more of a helper for type checking.

**note: I don't remember off the top of my head and didn't check if the way I used the map function would modify the products in-place, i.e. did I inadvertently double the price of the products in the original products array. That obviously is the sort of side effect that FP tries to avoid, and with a bit more code I could certainly make sure it doesn't happen.

  • 2
    This isn't really an OOP example, in the classical sense. In true OOP, data would be combined with behavior; here, you've separated the two. It's not necessarily a bad thing (I actually find it cleaner), but it's not what I would call classical OOP. – Robert Harvey May 10 '17 at 20:27
0

It does not seem to me the SimpleProductManager is child (extends or inherits) of something.

Its just implementation of the ProductManager interface which is basically a contract defining what actions (behaviors) the object have to do.

If it would be a child (or better said, iherited class or class extending another class functionality) it would be written as:

class SimpleProductManager extends ProductManager {
    ...
}

So basically, the author says:

Whe have some object which behavior is: setProducts, increasePrice, getProducts. And we don't care if the object has another behavior too or how the behavior is implemented.

The SimpleProductManager class implements it. Basically, it executes actions.

It can be also called PercentagePriceIncreaser as its main behavior is to increase the price by some percentage value.

But we can also implement another class: ValuePriceIncreaser which behavor will be:

public void increasePrice(int number) {
    if (products != null) {
        for (Product product : products) {
            double newPrice = product.getPrice() + number;
            product.setPrice(newPrice);
        }
    }
}

From external point of view, nothing has changed, the interface is the same, still have same three methods but the behavior is different.

As there is no such thing as interfaces in FP it would be hard to implement. In C, for example, we can hold pointers to functions and call appropriate one based on our needs. In the end, in OOP it works in very very similar way, but its "automated" by the compiler.

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