¿Cómo se programaría esto en no OO? [cerrado]

11

Leyendo un artículo mordaz sobre las desventajas de OOP a favor de algún otro paradigma, me he encontrado con un ejemplo con el que no puedo encontrar demasiadas fallas.

Quiero estar abierto a los argumentos del autor, y aunque teóricamente puedo entender sus puntos, un ejemplo en particular me está costando mucho imaginar cómo se implementaría mejor, por ejemplo, en un lenguaje 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.

Tenga en cuenta que no estoy buscando una refutación a favor o en contra de los argumentos del escritor para "¿Hay alguna razón racional por la que estos 3 comportamientos deberían estar vinculados a la jerarquía de datos?".

Lo que pregunto específicamente es cómo se modelaría / programaría este ejemplo en un lenguaje FP (código real, no teóricamente).

Danny Yaroslavski
fuente
44
No puede esperar razonablemente comparar ningún paradigma de programación en ejemplos tan breves y artificiales. Cualquier persona aquí puede presentar requisitos de código que hagan que su propio paradigma preferido se vea mejor que el resto, especialmente si implementan otros incorrectamente. Solo cuando tiene un proyecto real, grande y cambiante puede obtener información sobre las fortalezas y debilidades de diferentes paradigmas.
Eufórico
20
No hay nada en la programación OO que exija que esos 3 métodos se unan en la misma clase; de manera similar, no hay nada en la programación OO que exija que el comportamiento deba existir en la misma clase que los datos. Es decir, con la Programación OO puede colocar datos en la misma clase que el comportamiento, o puede dividirlos en una entidad / modelo separado. De cualquier manera, OO no tiene nada que decir realmente sobre cómo los datos deberían relacionarse con un objeto, ya que el concepto de un objeto está fundamentalmente relacionado con el comportamiento de modelado al agrupar métodos relacionados lógicamente en una clase.
Ben Cottrell
20
Obtuve 10 oraciones en ese comentario de un artículo y me di por vencido. No prestes atención al hombre detrás de esa cortina. En otras noticias, no tenía idea de que los verdaderos escoceses eran principalmente programadores de OOP.
Robert Harvey
11
Otra queja de alguien que escribe código de procedimiento en un lenguaje OO, luego se pregunta por qué OO no está trabajando para él.
TheCatWhisperer
11
Aunque es indudablemente cierto que OOP es un desastre de errores de diseño de principio a fin, ¡y estoy orgulloso de ser parte de ello! - este artículo es ilegible, y el ejemplo que da es básicamente el argumento de que una jerarquía de clases mal diseñada está mal diseñada.
Eric Lippert

Respuestas:

42

En el estilo FP, Productsería una clase inmutable, product.setPriceno mutaría un Productobjeto, sino que devolvería un nuevo objeto, y la increasePricefunción sería una función "independiente". Usando una sintaxis similar a la suya (C # / Java like), una función equivalente podría verse así:

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

Como puede ver, el núcleo no es realmente diferente aquí, excepto que se omite el código "repetitivo" del ejemplo artificial de OOP. Sin embargo, no veo esto como evidencia de que la POO conduzca a un código hinchado, solo como evidencia del hecho de que si se construye un ejemplo de código que sea suficientemente artificial, es posible probar cualquier cosa.

Doc Brown
fuente
77
Formas de hacer esto "más FP": 1) Use los tipos tal vez / opcionales en lugar de nulabilidad para facilitar la escritura de funciones totales en lugar de funciones parciales y use funciones auxiliares de orden superior para abstraer "if (x! = Null)" lógica. 2) Use lentes para definir un precio creciente para un solo producto en términos de aplicar un aumento porcentual en el contexto de una lente al precio del producto. 3) Use la aplicación / composición / currículum parcial para evitar una lambda explícita para el mapa / llamada de selección.
Jack
66
Debo decir que odio la idea de que una colección pueda ser nula en lugar de simplemente vacía por diseño. Los lenguajes funcionales con soporte de colección / tupla nativa funcionan de esa manera. Incluso en OOP, odio regresar nulldonde una colección es el tipo de retorno. / despotricar
Berin Loritsch
Pero este puede ser un método estático como en una clase de utilidad en lenguajes OOP como Java o C #. Este código es más corto en parte porque solicita pasar la lista y no mantenerla usted mismo. El código original también tiene una estructura de datos y solo moverlo acortaría el código original sin un cambio en los conceptos.
Mark
@ Mark: claro, y creo que el OP ya lo sabe. Entiendo la pregunta como "cómo expresar esto de una manera funcional", no obligatorio en un lenguaje que no sea OOP.
Doc Brown
@ Mark FP y OO no se excluyen entre sí.
Pieter B
17

Lo que pregunto específicamente es cómo se modelaría / programaría este ejemplo en un lenguaje FP (código real, no teóricamente).

En lenguaje "a" FP? Si alguno es suficiente, entonces elijo Emacs lisp. Tiene el concepto de tipos (tipo de, tipo de), pero solo los incorporados. Por lo tanto, su ejemplo se reduce a "¿cómo multiplica cada elemento de una lista por algo y devuelve una nueva lista".

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

Ahí tienes. Otros lenguajes serán similares, con la diferencia de que obtendrá el beneficio de los tipos explícitos con la semántica de "coincidencia" funcional habitual. Echa un vistazo a Haskell:

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

(O algo así, han pasado siglos ...)

Quiero estar abierto a los argumentos del autor,

¿Por qué? Traté de leer el artículo; Tuve que rendirme después de una página y escaneé rápidamente el resto.

El problema del artículo no es que esté en contra de la POO. Tampoco estoy ciegamente "pro OOP". He programado con paradigmas lógicos, funcionales y OOP, a menudo en el mismo lenguaje cuando sea posible, y con frecuencia sin ninguno de los tres, puramente imperativo o incluso a nivel de ensamblador. Yo no decir que ninguno de esos paradigmas es mucho superiour al otro en todos los aspectos. ¿Diría que me gusta más el lenguaje X que el Y? ¡Claro que si! Pero de eso no se trata ese artículo.

El problema del artículo es que usa una gran cantidad de herramientas retóricas (falacias) desde la primera hasta la última oración. Es completamente inútil incluso comenzar a describir todos los errores que contiene. El autor deja muy claro que no tiene ningún interés en la discusión, está en una cruzada. ¿Entonces, para qué molestarse?

Al final del día, todas esas cosas son solo herramientas para hacer un trabajo. Puede haber trabajos donde OOP es mejor, y puede haber otros trabajos donde FP es mejor, o donde ambos son excesivos. Lo importante es elegir la herramienta adecuada para el trabajo y hacerlo.

AnoE
fuente
44
"abundantemente claro que no tiene ningún interés en la discusión, está en una cruzada" Tenga un voto positivo para esta gema.
Eufórico
¿No necesita una restricción Num en su código Haskell? ¿Cómo puedes llamar (*) de lo contrario?
jk.
@jk., han pasado años que hice con Haskell, eso fue solo para satisfacer la restricción del OP para la respuesta que está buscando. ;) Si alguien quiere arreglar mi código, siéntase libre. Pero claro, lo cambiaré a Num.
AnoE
7

El autor hizo una muy buena observación y luego eligió un ejemplo mediocre para intentar respaldarlo. La queja no es con la implementación de la clase, es con la idea de que la jerarquía de datos está inextricablemente unida a la jerarquía de funciones.

Entonces se deduce que para comprender el punto del autor, no sería útil ver cómo implementaría esta clase individual en un estilo funcional. Tendría que ver cómo diseñaría todo el contexto de datos y funciones en torno a esta clase en un estilo funcional.

Piense en los tipos de datos potenciales involucrados en productos y precios. Para hacer una lluvia de ideas: nombre, código upc, categoría, peso de envío, precio, moneda, código de descuento, regla de descuento.

Esta es la parte fácil del diseño orientado a objetos. Acabamos de hacer una clase para todos los "objetos" anteriores y estamos bien, ¿verdad? ¿Hacer una Productclase para combinar algunos de ellos?

Pero espere, puede tener colecciones y agregados de algunos de esos tipos: Conjunto [categoría], (código de descuento -> precio), (cantidad -> cantidad de descuento), y así sucesivamente. ¿Dónde encajan esos? ¿Creamos por separado CategoryManagerpara hacer un seguimiento de todos los diferentes tipos de categorías, o esa responsabilidad pertenece a la Categoryclase que ya creamos?

Ahora, ¿qué pasa con las funciones que le dan un descuento en el precio si tiene una cierta cantidad de artículos de dos categorías diferentes? ¿Eso va en la Productclase, la Categoryclase, la DiscountRuleclase, la CategoryManagerclase o necesitamos algo nuevo? Así es como terminamos con cosas como DiscountRuleProductCategoryFactoryBuilder.

En el código funcional, su jerarquía de datos es completamente ortogonal a sus funciones. Puede ordenar sus funciones de cualquier manera que tenga sentido semántico. Por ejemplo, puede agrupar todas las funciones que cambian los precios de los productos, en cuyo caso tendría sentido factorizar la funcionalidad común como mapPricesen el siguiente ejemplo de Scala:

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)

Probablemente podría agregar otras funciones relacionadas con el precio aquí decreasePrice, como applyBulkDiscount, etc.

Debido a que también usamos una colección de Products, la versión OOP debe incluir métodos para administrar esa colección, pero no quería que este módulo se tratara de la selección de productos, quería que se tratara de los precios. El acoplamiento de datos de función también lo obligó a tirar la placa de control de la gestión de la colección.

Puede intentar resolver esto colocando al productsmiembro en una clase separada, pero luego termina con clases muy estrechamente acopladas. Los programadores de OO piensan que el acoplamiento de datos de funciones es muy natural e incluso beneficioso, pero hay un alto costo asociado con la pérdida de flexibilidad. Cada vez que crea una función, debe asignarla a una sola clase. Cada vez que desee usar una función, debe encontrar una manera de llevar sus datos acoplados al punto de uso. Esas restricciones son enormes.

Karl Bielefeldt
fuente
2

Simplemente separando los datos y la función como el autor aludía podría verse así en F # ("un lenguaje 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 }

Puede realizar un aumento de precio en una lista de productos de esta manera.

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

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

Nota: Si no está familiarizado con FP, cada función devuelve un valor. Procedente de un lenguaje similar a C, puede tratar la última declaración en una función como si tuviera un returnfrente.

Incluí algunas anotaciones de tipo, pero deberían ser innecesarias. getter / setter son innecesarios aquí ya que el módulo no posee los datos. Posee la estructura de los datos y las operaciones disponibles. Esto también se puede ver con List, lo que expone mapa ejecutar una función en cada elemento de la lista y devuelve el resultado en una nueva lista.

Tenga en cuenta que el módulo Producto no tiene que saber nada sobre el bucle, ya que esa responsabilidad recae en el módulo Lista (que creó la necesidad de bucle).

Kasey Speakman
fuente
1

Permítanme presentar esto con el hecho de que no soy un experto en programación funcional. Soy más una persona OOP. Entonces, aunque estoy bastante seguro de que lo siguiente es cómo lograrías el mismo tipo de funcionalidad con FP, podría estar equivocado.

Esto está en mecanografiado (de ahí todas las anotaciones de tipo). El mecanografiado (como javascript) es un lenguaje multidominio.

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 detalle (y de nuevo, no es un experto en FP), lo que hay que entender es que no hay mucho comportamiento predefinido. No existe un método de "aumento de precio" que aplique un aumento de precio en toda la lista, porque, por supuesto, esto no es POO: no hay una clase en la que definir dicho comportamiento. En lugar de crear un objeto que almacene una lista de productos, simplemente cree una variedad de productos. A continuación, puede utilizar procedimientos FP estándar para manipular esta matriz de la forma que desee: filtrar para seleccionar elementos particulares, asignar para ajustar elementos internos, etc. Termina con un control más detallado sobre su lista de productos sin tener que estar limitado por el API que le proporciona SimpleProductManager. Esto puede ser considerado una ventaja por algunos. También es cierto que no No tiene que preocuparse por ningún equipaje asociado con la clase ProductManager. Finalmente, no hay que preocuparse por "SetProducts" o "GetProducts", porque no hay ningún objeto que oculte sus productos: en su lugar, solo tiene la lista de productos con los que está trabajando. Nuevamente, esto puede ser una ventaja o desventaja dependiendo de las circunstancias / persona con la que está hablando. Además, obviamente no hay una jerarquía de clases (que es de lo que se estaba quejando) porque no hay clases en primer lugar. Esto puede ser una ventaja o desventaja dependiendo de las circunstancias / persona con la que está hablando. Además, obviamente no hay una jerarquía de clases (que es de lo que se estaba quejando) porque no hay clases en primer lugar. Esto puede ser una ventaja o desventaja dependiendo de las circunstancias / persona con la que está hablando. Además, obviamente no hay una jerarquía de clases (que es de lo que se estaba quejando) porque no hay clases en primer lugar.

No me tomé el tiempo de leer toda su diatriba. Utilizo prácticas de FP cuando es conveniente, pero definitivamente soy más del tipo OOP. Así que pensé que desde que respondí tu pregunta, también haría algunos breves comentarios sobre sus opiniones. Creo que este es un ejemplo muy artificial que destaca los "inconvenientes" de la POO. En este caso en particular, para la funcionalidad que se muestra, OOP probablemente está sobre-asesinado, y FP probablemente sería una mejor opción. Por otra parte, si esto fuera por algo así como un carrito de compras, proteger su lista de productos y limitar el acceso a ella es (creo) un objetivo muy importante del programa, y ​​FP no tiene forma de hacer cumplir tales cosas. De nuevo, puede ser que no soy un experto en FP, pero habiendo implementado carritos de compras para sistemas de comercio electrónico, preferiría usar OOP que FP.

Personalmente, me cuesta tomar en serio a cualquiera que tenga un argumento tan fuerte para "X es simplemente terrible. Siempre usa Y". La programación tiene una variedad de herramientas y paradigmas porque hay una gran variedad de problemas para resolver. FP tiene su lugar, OOP tiene su lugar, y nadie será un gran programador si no puede entender los inconvenientes y las ventajas de todas nuestras herramientas y cuándo usarlas.

** nota: Obviamente hay una clase en mi ejemplo: la clase de Producto. En este caso, aunque es simplemente un contenedor de datos tonto: no creo que mi uso viole los principios de FP. Es más útil para la verificación de tipos.

** nota: no recuerdo la parte superior de mi cabeza y no verifiqué si la forma en que utilicé la función de mapa modificaría los productos en el lugar, es decir, ¿doblé inadvertidamente el precio de los productos en los productos originales? formación. Obviamente, ese es el tipo de efecto secundario que FP intenta evitar, y con un poco más de código ciertamente podría asegurarme de que no suceda.

Conor Mancone
fuente
2
Este no es realmente un ejemplo de OOP, en el sentido clásico. En la verdadera OOP, los datos se combinarían con el comportamiento; Aquí, has separado a los dos. No es necesariamente algo malo (en realidad lo encuentro más limpio), pero no es lo que yo llamaría OOP clásica.
Robert Harvey
0

No me parece que SimpleProductManager sea hijo (se extienda o herede) de algo.

Es solo la implementación de la interfaz ProductManager, que es básicamente un contrato que define qué acciones (comportamientos) debe realizar el objeto.

Si fuera un niño (o mejor dicho, una clase heredada o una clase que extiende la funcionalidad de otra clase) se escribiría como:

class SimpleProductManager extends ProductManager {
    ...
}

Básicamente, el autor dice:

Cuando tenemos algún objeto cuyo comportamiento es: setProducts, raisePrice, getProducts. Y no nos importa si el objeto también tiene otro comportamiento o cómo se implementa el comportamiento.

La clase SimpleProductManager lo implementa. Básicamente, ejecuta acciones.

También se le puede llamar PercentagePriceIncreaser ya que su comportamiento principal es aumentar el precio en algún valor porcentual.

Pero también podemos implementar otra clase: ValuePriceIncreaser cuyo comportamiento será:

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

Desde el punto de vista externo, nada ha cambiado, la interfaz es la misma, todavía tiene los mismos tres métodos pero el comportamiento es diferente.

Como no existen interfaces en FP, sería difícil de implementar. En C, por ejemplo, podemos mantener punteros a las funciones y llamar al apropiado según nuestras necesidades. Al final, en OOP funciona de manera muy muy similar, pero es "automatizado" por el compilador.

Fis
fuente