Comment cela serait-il programmé en non-OO? [fermé]

11

En lisant un article cinglant sur les inconvénients de la POO en faveur d' un autre paradigme, je suis tombé sur un exemple avec lequel je ne trouve pas trop de défaut.

Je veux être ouvert aux arguments de l'auteur, et bien que je puisse théoriquement comprendre leurs points, un exemple en particulier, j'ai du mal à imaginer comment il serait mieux mis en œuvre dans, disons, un langage FP.

De: 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.

Notez que je ne cherche pas de réfutation pour ou contre les arguments de l'auteur pour "Y a-t-il une raison rationnelle pour laquelle ces 3 comportements devraient être liés à la hiérarchie des données?".

Ce que je demande spécifiquement, c'est comment cet exemple serait-il modélisé / programmé dans un langage FP (code réel, pas théoriquement)?

Danny Yaroslavski
la source
44
Vous ne pouvez pas raisonnablement vous attendre à comparer des paradigmes de programmation sur des exemples aussi courts et artificiels. N'importe qui ici peut proposer des exigences de code qui rendent leur propre paradigme préféré plus beau que le repos, surtout s'ils en implémentent d'autres incorrectement. Ce n'est que lorsque vous avez un projet réel, grand et changeant que vous pouvez obtenir des informations sur les forces et les faiblesses de différents paradigmes.
Euphoric
20
Il n'y a rien dans la programmation OO qui exige que ces 3 méthodes doivent aller ensemble dans la même classe; de même, il n'y a rien dans la programmation OO qui exige que le comportement existe dans la même classe que les données. Autrement dit, avec la programmation OO, vous pouvez placer les données dans la même classe que le comportement, ou vous pouvez les diviser en une entité / un modèle séparé. de toute façon, OO n'a rien à dire sur la façon dont les données doivent être liées à un objet, car le concept d'un objet est fondamentalement concerné par la modélisation du comportement en regroupant les méthodes logiquement liées dans une classe.
Ben Cottrell
20
J'ai obtenu 10 phrases dans cette diatribe d'un article et j'ai abandonné. Ne faites pas attention à l'homme derrière ce rideau. Dans d'autres nouvelles, je ne savais pas que les Vrais Écossais étaient principalement des programmeurs OOP.
Robert Harvey
11
Encore une autre diatribe de quelqu'un qui écrit du code de procédure dans un langage OO, puis se demande pourquoi OO ne travaille pas pour lui.
TheCatWhisperer
11
Bien qu'il soit sans aucun doute vrai que la POO est un désastre de faux pas de conception du début à la fin - et je suis fier d'en faire partie! - cet article est illisible, et l'exemple que vous donnez est essentiellement l'argument selon lequel une hiérarchie de classes mal conçue est mal conçue.
Eric Lippert

Réponses:

42

Dans le style FP, Productserait une classe immuable, product.setPricene muterait pas un Productobjet mais retournerait un nouvel objet à la place, et la increasePricefonction serait une fonction "autonome". En utilisant une syntaxe similaire à la vôtre (comme C # / Java), une fonction équivalente pourrait ressembler à ceci:

 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;
}

Comme vous le voyez, le noyau n'est pas vraiment différent ici, sauf que le code "passe-partout" de l'exemple de POO artificiel est omis. Cependant, je ne vois pas cela comme une preuve que la POO conduit à du code gonflé, seulement comme une preuve du fait que si l'on construit un exemple de code suffisamment artificiel, il est possible de prouver n'importe quoi.

Doc Brown
la source
7
Façons de rendre ce "plus FP": 1) Utilisez des types Peut-être / Facultatif au lieu de nullabilité pour faciliter l'écriture de fonctions totales au lieu de fonctions partielles et utilisez des fonctions d'aide d'ordre supérieur pour résumer "if (x! = Null)" logique. 2) Utiliser des lentilles pour définir l'augmentation du prix d'un seul produit en termes d'application d'un pourcentage d'augmentation dans le contexte d'une lentille sur le prix du produit. 3) Utilisez une application / composition / currying partielle pour éviter un lambda explicite pour la carte / l'appel Select.
Jack
6
Je dois dire que je déteste l'idée d'une collection qui pourrait être nulle au lieu d'être simplement vide par conception. Les langages fonctionnels avec le support natif de tuple / collection fonctionnent de cette façon. Même dans la POO, je déteste retourner nulllà où une collection est le type de retour. / coup de
gueule
Mais cela peut être une méthode statique comme dans une classe utilitaire dans des langages OOP comme Java ou C #. Ce code est plus court en partie parce que vous demandez de passer dans la liste et de ne pas la tenir vous-même. Le code d'origine contient également une structure de données et le simple fait de le déplacer rendrait le code original plus court sans changement de concepts.
Mark
@Mark: bien sûr, et je pense que l'OP le sait déjà. Je comprends la question comme «comment l'exprimer de manière fonctionnelle», non obligatoire dans une langue non-OOP.
Doc Brown
@Mark FP et OO ne s'excluent pas mutuellement.
Pieter B
17

Ce que je demande spécifiquement, c'est comment cet exemple serait-il modélisé / programmé dans un langage FP (code réel, pas théoriquement)?

Dans "un" langage FP? Si un seul suffit, je choisis Emisps lisp. Il a le concept de types (sorte de, sorte de), mais seulement ceux intégrés. Ainsi, votre exemple se réduit à "comment multiplier chaque élément d'une liste par quelque chose et renvoyer une nouvelle liste".

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

Voilà. Les autres langages seront similaires, à la différence près que vous bénéficiez de types explicites avec la sémantique fonctionnelle "d'appariement" habituelle. Découvrez Haskell:

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

(Ou quelque chose comme ça, ça fait des siècles ...)

Je veux être ouvert aux arguments de l'auteur,

Pourquoi? J'ai essayé de lire l'article; J'ai dû abandonner après une page et j'ai rapidement scanné le reste.

Le problème de l'article n'est pas qu'il est contre la POO. Je ne suis pas non plus aveuglément "pro OOP". J'ai programmé avec des paradigmes logiques, fonctionnels et POO, assez souvent dans le même langage lorsque cela est possible, et souvent sans aucun des trois, purement impératif ou même au niveau assembleur. Je ne dirais jamais que l'un de ces paradigmes est largement supérieur à l'autre dans tous les aspects. Dois-je dire que j'aime mieux la langue X que la langue Y? Bien sûr que je le ferais! Mais ce n'est pas de cela qu'il s'agit.

Le problème de l'article est qu'il utilise une abondance d'outils rhétoriques (erreurs) de la première à la dernière phrase. Il est complètement vain de commencer même à décrire toutes les erreurs qu'il contient. L'auteur montre clairement qu'il n'a aucun intérêt à discuter, il est en croisade. Alors pourquoi s'embêter?

Au bout du compte, toutes ces choses ne sont que des outils pour faire un travail. Il peut y avoir des emplois où la POO est meilleure, et il peut y avoir d'autres emplois où la PF est meilleure, ou où les deux sont excessifs. L'important est de choisir le bon outil pour le travail et de le faire.

AnoE
la source
4
"Il est très clair qu'il n'a aucun intérêt à discuter, il est en croisade" Ayez un vote positif pour ce joyau.
Euphoric
Vous n'avez pas besoin d'une contrainte Num sur votre code Haskell? comment pouvez-vous appeler (*) autrement?
jk.
@jk., ça fait longtemps que j'ai fait Haskell, c'était juste pour satisfaire la contrainte du PO pour la réponse qu'il cherche. ;) Si quelqu'un veut corriger mon code, n'hésitez pas. Mais bien sûr, je vais le passer à Num.
AnoE
7

L'auteur a fait un très bon argument puis a choisi un exemple terne pour tenter de le sauvegarder. La plainte ne concerne pas l'implémentation de la classe, c'est l'idée que la hiérarchie des données est inextricablement couplée à la hiérarchie des fonctions.

Il s'ensuit alors que pour comprendre le point de l'auteur, il ne serait pas utile de voir seulement comment il implémenterait cette classe unique dans un style fonctionnel. Il faudrait voir comment il concevrait tout le contexte des données et des fonctions autour de cette classe dans un style fonctionnel.

Pensez aux types de données potentiels impliqués dans les produits et les prix. Pour en faire un remue-méninges: nom, code upc, catégorie, poids d'expédition, prix, devise, code de réduction, règle de remise.

C'est la partie facile de la conception orientée objet. Nous faisons juste une classe pour tous les "objets" ci-dessus et nous sommes bons, non? Faire un Productcours pour en combiner quelques-uns?

Mais attendez, vous pouvez avoir des collections et des agrégats de certains de ces types: Définir [catégorie], (code de réduction -> prix), (quantité -> montant de la réduction), etc. Où se situent ceux-ci? Créons-nous une entité distincte CategoryManagerpour garder une trace de tous les différents types de catégories, ou cette responsabilité appartient-elle à la Categoryclasse que nous avons déjà créée?

Maintenant, qu'en est-il des fonctions qui vous offrent une remise de prix si vous avez une certaine quantité d'articles de deux catégories différentes? Est-ce que cela va dans la Productclasse, la Categoryclasse, la DiscountRuleclasse, la CategoryManagerclasse, ou avons-nous besoin de quelque chose de nouveau? Voilà comment nous nous retrouvons avec des choses comme DiscountRuleProductCategoryFactoryBuilder.

En code fonctionnel, votre hiérarchie de données est complètement orthogonale à vos fonctions. Vous pouvez trier vos fonctions de la manière la plus sémantique possible. Par exemple, vous pouvez regrouper toutes les fonctions qui modifient les prix des produits ensemble, auquel cas il serait judicieux de tenir compte des fonctionnalités communes comme mapPricesdans l'exemple Scala suivant:

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)

Je pourrais probablement ajouter d' autres fonctions liées au prix ici comme decreasePrice, applyBulkDiscount, etc.

Parce que nous utilisons également une collection de Products, la version OOP doit inclure des méthodes pour gérer cette collection, mais vous ne vouliez pas que ce module soit sur la sélection de produits, vous vouliez qu'il soit sur les prix. Le couplage fonction-données vous a également obligé à y insérer un passe-partout de gestion de collection.

Vous pouvez essayer de résoudre ce problème en plaçant le productsmembre dans une classe distincte, mais vous vous retrouvez ensuite avec des classes très étroitement couplées. Les programmeurs OO pensent que le couplage fonction-données est très naturel et même bénéfique, mais il y a un coût élevé associé à une perte de flexibilité. Chaque fois que vous créez une fonction, vous devez l'assigner à une et une seule classe. Chaque fois que vous souhaitez utiliser une fonction, vous devez trouver un moyen d'obtenir ses données couplées au point d'utilisation. Ces restrictions sont énormes.

Karl Bielefeldt
la source
2

La simple séparation des données et de la fonction à laquelle l'auteur faisait allusion pourrait ressembler à ceci en F # ("un langage FP").

module Product =

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

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

        { product with Price = newPrice }

Vous pouvez effectuer une augmentation de prix sur une liste de produits de cette façon.

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

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

Remarque: Si vous n'êtes pas familier avec FP, chaque fonction renvoie une valeur. Venant d'un langage de type C, vous pouvez traiter la dernière instruction d'une fonction comme si elle avait un returndevant.

J'ai inclus quelques annotations de type, mais elles ne devraient pas être nécessaires. getter / setter sont inutiles ici car le module ne possède pas les données. Il possède la structure des données et les opérations disponibles. Cela peut également être vu avec List, qui expose mapà exécuter une fonction sur chaque élément de la liste, et renvoie le résultat dans une nouvelle liste.

Notez que le module Produit n'a rien à savoir sur le bouclage, car cette responsabilité appartient au module Liste (qui a créé le besoin de bouclage).

Kasey Speakman
la source
1

Permettez-moi de faire précéder cela du fait que je ne suis pas un expert en programmation fonctionnelle. Je suis plutôt une personne OOP. Donc, même si je suis presque sûr que ce qui suit est de savoir comment vous accomplirez le même type de fonctionnalité avec FP, je peux me tromper.

C'est en Typographie (d'où toutes les annotations de type). Le tapuscrit (comme javascript) est un langage multi-domaines.

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 );

En détail (et encore une fois, pas un expert en PF), la chose à comprendre est qu'il n'y a pas beaucoup de comportements prédéfinis. Il n'y a pas de méthode "d'augmentation de prix" qui applique une augmentation de prix sur toute la liste, car bien sûr ce n'est pas la POO: il n'y a pas de classe pour définir un tel comportement. Au lieu de créer un objet qui stocke une liste de produits, vous créez simplement un tableau de produits. Vous pouvez ensuite utiliser des procédures FP standard pour manipuler ce tableau de la manière que vous souhaitez: filtrer pour sélectionner des éléments particuliers, mapper pour ajuster les internes, etc. Vous vous retrouvez avec un contrôle plus détaillé de votre liste de produits sans avoir à être limité par le API que le SimpleProductManager vous donne. Cela peut être considéré comme un avantage par certains. Il est également vrai que vous ne le faites pas t avoir à vous soucier des bagages associés à la classe ProductManager. Enfin, vous n'avez pas à vous soucier des "SetProducts" ou des "GetProducts", car il n'y a aucun objet qui cache vos produits: à la place, vous avez juste la liste des produits avec lesquels vous travaillez. Encore une fois, cela peut être un avantage ou un inconvénient selon les circonstances / la personne à qui vous parlez. De plus, il n'y a évidemment pas de hiérarchie de classes (ce dont il se plaignait) car il n'y a pas de classes en premier lieu. cela peut être un avantage ou un inconvénient selon les circonstances / la personne à qui vous parlez. De plus, il n'y a évidemment pas de hiérarchie de classes (ce dont il se plaignait) car il n'y a pas de classes en premier lieu. cela peut être un avantage ou un inconvénient selon les circonstances / la personne à qui vous parlez. De plus, il n'y a évidemment pas de hiérarchie de classes (ce dont il se plaignait) car il n'y a pas de classes en premier lieu.

Je n'ai pas pris le temps de lire toute sa diatribe. J'utilise les pratiques de PF quand c'est pratique, mais je suis définitivement plus du genre OOP. J'ai donc pensé que depuis que je répondais à votre question, je ferais aussi quelques brèves remarques sur ses opinions. Je pense que c'est un exemple très artificiel qui met en évidence les «inconvénients» de la POO. Dans ce cas particulier, pour la fonctionnalité présentée, la POO est probablement trop destructrice, et FP serait probablement mieux adapté. Là encore, si c'était pour quelque chose comme un panier d'achat, protéger votre liste de produits et en limiter l'accès est (je pense) un objectif très important du programme, et FP n'a aucun moyen de faire respecter de telles choses. Encore une fois, il se peut que je ne sois pas un expert en FP, mais après avoir mis en place des paniers d'achat pour les systèmes de commerce électronique, je préfère de loin utiliser la POO que la FP.

Personnellement, j'ai du mal à prendre au sérieux quelqu'un qui fait un argument si fort pour "X est tout simplement terrible. Utilisez toujours Y". La programmation a une variété d'outils et de paradigmes car il y a une grande variété de problèmes à résoudre. FP a sa place, OOP a sa place, et personne ne sera un grand programmeur s'il ne peut pas comprendre les inconvénients et les avantages de tous nos outils et quand les utiliser.

** note: Évidemment, il y a une classe dans mon exemple: la classe Product. Dans ce cas, il s'agit simplement d'un conteneur de données stupide: je ne pense pas que mon utilisation en soit contraire aux principes de la PF. Il s'agit plutôt d'une aide pour la vérification de type.

** Remarque: je ne me souviens pas du haut de ma tête et je n'ai pas vérifié si la façon dont j'utilisais la fonction de carte modifierait les produits en place, c'est-à-dire que j'ai doublé par inadvertance le prix des produits dans les produits d'origine tableau. C'est évidemment le genre d'effet secondaire que FP essaie d'éviter, et avec un peu plus de code, je pourrais certainement m'assurer que cela ne se produise pas.

Conor Mancone
la source
2
Ce n'est pas vraiment un exemple de POO, au sens classique. En vrai POO, les données seraient combinées avec le comportement; ici, vous avez séparé les deux. Ce n'est pas nécessairement une mauvaise chose (je le trouve en fait plus propre), mais ce n'est pas ce que j'appellerais la POO classique.
Robert Harvey
0

Il ne me semble pas que SimpleProductManager soit enfant (étend ou hérite) de quelque chose.

Son implémentation juste de l'interface ProductManager qui est fondamentalement un contrat définissant quelles actions (comportements) l'objet doit faire.

Si ce devait être un enfant (ou mieux, une classe héritée ou une classe étendant une autre fonctionnalité de classe), il serait écrit comme suit:

class SimpleProductManager extends ProductManager {
    ...
}

Donc, fondamentalement, l'auteur dit:

Nous avons un objet dont le comportement est: setProducts, augmenter le prix, getProducts. Et peu importe si l'objet a aussi un autre comportement ou comment le comportement est mis en œuvre.

La classe SimpleProductManager l'implémente. Fondamentalement, il exécute des actions.

Il peut également être appelé PercentagePriceIncreaser car son comportement principal consiste à augmenter le prix d'une certaine valeur en pourcentage.

Mais nous pouvons également implémenter une autre classe: ValuePriceIncreaser dont le comportement sera:

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

Du point de vue externe, rien n'a changé, l'interface est la même, a toujours les mêmes trois méthodes mais le comportement est différent.

Comme il n'y a pas d'interfaces dans FP, il serait difficile à implémenter. En C, par exemple, nous pouvons tenir des pointeurs vers des fonctions et appeler une fonction appropriée en fonction de nos besoins. Au final, dans OOP cela fonctionne de manière très très similaire, mais c'est "automatisé" par le compilateur.

Fis
la source