Come sarebbe programmato in non OO? [chiuso]


11

Leggendo un articolo caotico sugli aspetti negativi di OOP a favore di qualche altro paradigma, ho incontrato un esempio di cui non riesco a trovare troppa colpa.

Voglio essere aperto alle argomentazioni dell'autore, e anche se in teoria posso comprenderne i punti, in particolare un esempio sto facendo fatica a immaginare come sarebbe meglio implementato, per esempio, in un linguaggio FP.

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

Nota che non sto cercando una confutazione a favore o contro gli argomenti dello scrittore per "C'è qualche ragione razionale per cui questi 3 comportamenti dovrebbero essere collegati alla gerarchia dei dati?".

Quello che sto chiedendo in particolare è come sarebbe modellato / programmato questo esempio in un linguaggio FP (codice reale, non teoricamente)?


44
Non puoi ragionevolmente aspettarti di confrontare i paradigmi di programmazione su esempi così brevi e inventati. Chiunque qui può presentare requisiti di codice che rendono il proprio paradigma preferito un aspetto migliore rispetto al resto, soprattutto se ne implementano altri in modo improprio. Solo quando hai un progetto reale, grande e mutevole puoi ottenere intuizioni su punti di forza e di debolezza di diversi paradigmi.
Euforico,

20
Non c'è nulla nella programmazione OO che impone che quei 3 metodi vadano insieme nella stessa classe; allo stesso modo non c'è nulla nella programmazione OO che imponga che il comportamento debba esistere nella stessa classe dei dati. Vale a dire, con la programmazione OO è possibile inserire i dati nella stessa classe del comportamento oppure è possibile suddividerli in un'entità / modello separato. in entrambi i casi, OO non ha davvero nulla da dire su come i dati debbano essere correlati a un oggetto, poiché il concetto di un oggetto si occupa fondamentalmente del comportamento di modellazione raggruppando i metodi logicamente correlati in una classe.
Ben Cottrell,

20
Ho ottenuto 10 frasi in quel rant di un articolo e ho rinunciato. Non prestare attenzione all'uomo dietro quella tenda. In altre notizie, non avevo idea che i veri scozzesi fossero principalmente programmatori OOP.
Robert Harvey,

11
Ancora un altro sfogo da parte di qualcuno che scrive codice procedurale in un linguaggio OO, quindi si chiede perché OO non funzioni per lui.
TheCatWhisperer,

11
Anche se è senza dubbio vero che OOP è un disastro di passi falsi di progettazione dall'inizio alla fine - e sono orgoglioso di farne parte! - questo articolo è illeggibile e l'esempio che stai dando è sostanzialmente l'argomentazione secondo cui una gerarchia di classi mal progettata è mal progettata.
Eric Lippert,

Risposte:


42

In stile FP, Productsarebbe una classe immutabile, product.setPricenon cambierebbe un Productoggetto ma restituirebbe un nuovo oggetto e la increasePricefunzione sarebbe una funzione "autonoma". Usando una sintassi simile alla tua (come C # / Java), una funzione equivalente potrebbe assomigliare a questa:

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

Come vedi, il core non è molto diverso qui, tranne il codice "boilerplate" dell'esempio OOP inventato è omesso. Tuttavia, non lo vedo come prova che OOP porta a un codice gonfio, ma solo come prova del fatto che se si costruisce un esempio di codice sufficientemente artificiale, è possibile provare qualsiasi cosa.


7
Modi per rendere questo "più FP": 1) Usa i tipi Forse / Opzionale invece di nullability per rendere più semplice la scrittura di funzioni totali invece di funzioni parziali e utilizzare le funzioni di supporto di ordine superiore per astrarre "if (x! = Null)" logica. 2) Utilizzare gli obiettivi per definire un prezzo crescente per un singolo prodotto in termini di applicazione di un aumento percentuale nel contesto di un obiettivo sul prezzo del prodotto. 3) Usa l'applicazione / composizione / curry parziale per evitare un lambda esplicito per la mappa / Seleziona chiamata.
Jack,

6
Devo dire che odio l'idea di una collezione potrebbe essere nulla invece che semplicemente vuota dal design. I linguaggi funzionali con supporto nativo per la tupla / raccolta funzionano in questo modo. Anche in OOP odio tornare nulldove una raccolta è il tipo di ritorno. / rant over
Berin Loritsch,

Ma questo può essere un metodo statico come in una classe di utilità in linguaggi OOP come Java o C #. Questo codice è in parte più breve perché si chiede di passare nell'elenco e di non tenerlo da soli. Il codice originale contiene anche una struttura di dati e il semplice spostamento di esso renderebbe il codice originale più breve senza una modifica dei concetti.
Segna

@Mark: certo, e penso che l'OP lo sappia già. Capisco la domanda come "come esprimerla in modo funzionale", non obbligatoria in un linguaggio non OOP.
Doc Brown,

@Mark FP e OO non si escludono a vicenda.
Pieter B,

17

Quello che sto chiedendo in particolare è come sarebbe modellato / programmato questo esempio in un linguaggio FP (codice reale, non teoricamente)?

In "un" linguaggio FP? Se è sufficiente, scelgo Emacs lisp. Ha il concetto di tipi (tipo di, tipo di), ma solo quelli incorporati. Quindi il tuo esempio si riduce a "come moltiplicare ogni elemento in un elenco per qualcosa e restituire un nuovo elenco".

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

Ecco qua. Altre lingue saranno simili, con la differenza che si ottiene il beneficio di tipi espliciti con la solita semantica funzionale "corrispondente". Dai un'occhiata a Haskell:

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

(O qualcosa del genere, sono passati secoli ...)

Voglio essere aperto agli argomenti dell'autore,

Perché? Ho provato a leggere l'articolo; Ho dovuto rinunciare dopo una pagina e ho scansionato rapidamente il resto.

Il problema dell'articolo non è che è contro OOP. Né sono ciecamente "pro OOP". Ho programmato con paradigmi logici, funzionali e OOP, abbastanza spesso nella stessa lingua quando possibile, e spesso senza nessuno dei tre, puramente imperativo o addirittura a livello di assemblatore. Non direi mai che nessuno di quei paradigmi è enormemente superiour all'altro in ogni aspetto. Direi che mi piace la lingua X meglio di Y? Certo che lo farei! Ma non si tratta di questo articolo.

Il problema dell'articolo è che usa un'abbondanza di strumenti retorici (errori) dalla prima all'ultima frase. È del tutto inutile persino iniziare a descrivere tutti gli errori che contiene. L'autore chiarisce abbondantemente che non ha alcun interesse per la discussione, è su una crociata. Allora perché preoccuparsi?

Alla fine, tutte queste cose sono solo strumenti per svolgere un lavoro. Potrebbero esserci lavori in cui OOP è migliore e potrebbero esserci altri lavori in cui FP è migliore o in cui entrambi sono eccessivi. L'importante è scegliere lo strumento giusto per il lavoro e farlo.


4
"abbondantemente chiaro che non ha alcun interesse per la discussione, è su una crociata" Avere un voto per questa gemma.
Euforico

Non hai bisogno di un vincolo Num sul tuo codice Haskell? come puoi chiamare (*) altrimenti?
jk.

@jk., sono state le epoche che ho fatto a Haskell, solo per soddisfare il vincolo del PO per la risposta che stava cercando. ;) Se qualcuno vuole correggere il mio codice, sentiti libero. Ma certo, lo passerò a Num.
AnoE,

7

L'autore ha fatto un'ottima osservazione, quindi ha scelto un esempio poco brillante per tentare di eseguirne il backup. Il reclamo non riguarda l'implementazione della classe, ma l'idea che la gerarchia dei dati sia indissolubilmente accoppiata con la gerarchia delle funzioni.

Ne consegue quindi che per comprendere il punto dell'autore, non sarebbe utile vedere solo come avrebbe implementato questa singola classe in uno stile funzionale. Dovresti vedere come progetterebbe l' intero contesto di dati e funzioni attorno a questa classe in uno stile funzionale.

Pensa ai potenziali tipi di dati coinvolti nei prodotti e nei prezzi. Per brainstorming alcuni: nome, codice upc, categoria, peso di spedizione, prezzo, valuta, codice sconto, regola di sconto.

Questa è la parte facile del design orientato agli oggetti. Facciamo solo una classe per tutti gli "oggetti" sopra e siamo bravi, giusto? Fare una Productclasse per combinarne alcuni insieme?

Ma aspetta, puoi avere raccolte e aggregati di alcuni di questi tipi: imposta [categoria], (codice sconto -> prezzo), (quantità -> importo sconto) e così via. Dove si inseriscono quelli? Ne creiamo uno separato CategoryManagerper tenere traccia di tutti i diversi tipi di categorie o tale responsabilità appartiene alla Categoryclasse che abbiamo già creato?

E le funzioni che ti offrono uno sconto sul prezzo se hai una certa quantità di articoli di due diverse categorie? Questo va nella Productclasse, nella Categoryclasse, nella DiscountRuleclasse, nella CategoryManagerclasse o abbiamo bisogno di qualcosa di nuovo? È così che finiamo con cose del genere DiscountRuleProductCategoryFactoryBuilder.

Nel codice funzionale, la gerarchia dei dati è completamente ortogonale alle funzioni. Puoi ordinare le tue funzioni in qualunque modo abbia senso semantico. Ad esempio, potresti raggruppare tutte le funzioni che cambiano insieme i prezzi dei prodotti, nel qual caso avrebbe senso scomporre le funzionalità comuni come mapPricesnell'esempio Scala seguente:

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)

Probabilmente potrei aggiungere altre funzioni di prezzo legate qui, come decreasePrice, applyBulkDiscountecc

Poiché utilizziamo anche una raccolta di Products, la versione OOP deve includere metodi per gestire quella raccolta, ma non volevi che questo modulo riguardasse la selezione dei prodotti, volevi che riguardasse i prezzi. L'accoppiamento funzione-dati ti ha costretto a lanciare anche la piastra di gestione della raccolta.

Puoi provare a risolvere questo problema mettendo il productsmembro in una classe separata, ma poi finisci con classi molto strettamente accoppiate. I programmatori OO ritengono che l'accoppiamento funzione-dati sia molto naturale e persino vantaggioso, ma comporta un costo elevato in termini di perdita di flessibilità. Ogni volta che crei una funzione, devi assegnarla a una e una sola classe. Ogni volta che si desidera utilizzare una funzione, è necessario trovare un modo per ottenere i dati associati al punto di utilizzo. Tali restrizioni sono enormi.


2

Semplicemente separando i dati e la funzione come alludeva l'autore potrebbe apparire così in F # ("un linguaggio 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 }

In questo modo è possibile eseguire un aumento di prezzo in un elenco di prodotti.

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

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

Nota: se non si ha familiarità con FP, ogni funzione restituisce un valore. Proveniente da un linguaggio di tipo C, puoi trattare l'ultima istruzione in una funzione come se avesse un returndavanti.

Ho incluso alcune annotazioni di tipo, ma dovrebbero essere inutili. getter / setter non sono necessari qui poiché il modulo non possiede i dati. Possiede la struttura dei dati e le operazioni disponibili. Questo può essere visto anche con List, che espone mapl'esecuzione di una funzione su ogni elemento nell'elenco e restituisce il risultato in un nuovo elenco.

Si noti che il modulo Prodotto non deve sapere nulla sul looping, poiché tale responsabilità spetta al modulo List (che ha creato la necessità del looping).


1

Consentitemi di prefigurarlo con il fatto che non sono un esperto di programmazione funzionale. Sono più una persona OOP. Quindi, anche se sono abbastanza sicuro che di seguito è come ottenere lo stesso tipo di funzionalità con FP, potrei sbagliarmi.

Questo è In Typescript (quindi tutte le annotazioni sui tipi). Typescript (come javascript) è un linguaggio multi-dominio.

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

Nel dettaglio (e ancora, non un esperto di FP), la cosa da capire è che non ci sono molti comportamenti predefiniti. Non esiste un metodo di "aumento del prezzo" che applica un aumento di prezzo in tutto l'elenco, perché ovviamente questo non è OOP: non esiste una classe in cui definire tale comportamento. Invece di creare un oggetto che memorizza un elenco di prodotti, è sufficiente creare una matrice di prodotti. È quindi possibile utilizzare le procedure FP standard per manipolare questo array nel modo desiderato: filtro per selezionare elementi particolari, mappa per regolare gli interni, ecc ... Si termina con un controllo più dettagliato sull'elenco dei prodotti senza essere limitato dal API fornita da SimpleProductManager. Questo può essere considerato un vantaggio da alcuni. È anche vero che non non doversi preoccupare di alcun bagaglio associato alla classe ProductManager. Infine, non ci sono preoccupazioni per "SetProducts" o "GetProducts", perché non esiste alcun oggetto che nasconda i tuoi prodotti: invece, hai solo l'elenco dei prodotti con cui stai lavorando. Ancora una volta, questo può essere un vantaggio o uno svantaggio a seconda delle circostanze / persona con cui stai parlando. Inoltre, ovviamente non esiste una gerarchia di classi (che è ciò di cui si stava lamentando) perché non ci sono classi in primo luogo. questo può essere un vantaggio o uno svantaggio a seconda delle circostanze / persona con cui stai parlando. Inoltre, ovviamente non esiste una gerarchia di classi (che è ciò di cui si stava lamentando) perché non ci sono classi in primo luogo. questo può essere un vantaggio o uno svantaggio a seconda delle circostanze / persona con cui stai parlando. Inoltre, ovviamente non esiste una gerarchia di classi (che è ciò di cui si stava lamentando) perché non ci sono classi in primo luogo.

Non ho avuto il tempo di leggere tutto il suo sfogo. Uso le pratiche FP quando è conveniente, ma sono decisamente più un tipo OOP. Quindi ho pensato che da quando ho risposto alla tua domanda, avrei anche fatto alcuni brevi commenti sulle sue opinioni. Penso che questo sia un esempio molto ingegnoso che evidenzia gli "aspetti negativi" di OOP. In questo caso particolare, per la funzionalità mostrata, OOP probabilmente è over-kill e FP sarebbe probabilmente una soluzione migliore. Inoltre, se questo fosse per qualcosa come un carrello della spesa, proteggere l'elenco dei prodotti e limitarne l'accesso è (penso) un obiettivo molto importante del programma e FP non ha modo di far valere tali cose. Ancora una volta, può darsi che non sia un esperto di FP, ma avendo implementato i carrelli della spesa per i sistemi di e-commerce, preferirei di gran lunga utilizzare OOP piuttosto che FP.

Personalmente faccio fatica a prendere sul serio qualcuno che fa una discussione così forte per "X è semplicemente terribile. Usa sempre Y". La programmazione ha una varietà di strumenti e paradigmi perché ci sono molti problemi da risolvere. FP ha il suo posto, OOP ha il suo posto, e nessuno sarà un grande programmatore se non riescono a capire gli svantaggi e i vantaggi di tutti i nostri strumenti e quando usarli.

** nota: ovviamente c'è una classe nel mio esempio: la classe Product. In questo caso, però, è semplicemente un contenitore di dati stupido: non credo che il mio uso violi i principi di FP. È più un aiuto per il controllo del tipo.

** nota: non ricordo la parte superiore della mia testa e non ho verificato se il modo in cui ho usato la funzione mappa avrebbe modificato i prodotti sul posto, cioè ho inavvertitamente raddoppiato il prezzo dei prodotti nei prodotti originali Vettore. Questo ovviamente è il tipo di effetto collaterale che FP cerca di evitare e con un po 'più di codice potrei sicuramente assicurarmi che non accada.


2
Questo non è proprio un esempio di OOP, nel senso classico. Nella vera OOP, i dati sarebbero combinati con il comportamento; qui, hai separato i due. Non è necessariamente una cosa negativa (in realtà la trovo più pulita), ma non è ciò che definirei OOP classico.
Robert Harvey,

0

Non mi sembra che SimpleProductManager sia figlio (si estende o eredita) di qualcosa.

È solo l'implementazione dell'interfaccia ProductManager che è fondamentalmente un contratto che definisce quali azioni (comportamenti) l'oggetto deve fare.

Se fosse un bambino (o meglio, classe ereditata o classe che estende un'altra funzionalità di classe) sarebbe scritto come:

class SimpleProductManager extends ProductManager {
    ...
}

Quindi, in sostanza, l'autore dice:

Abbiamo qualche oggetto il cui comportamento è: setProducts, aumentaPrice, getProducts. E non ci importa se l'oggetto ha anche un altro comportamento o come viene implementato il comportamento.

La classe SimpleProductManager lo implementa. Fondamentalmente, esegue azioni.

Può anche essere chiamato PercentagePriceIncreaser poiché il suo comportamento principale è quello di aumentare il prezzo di un valore percentuale.

Ma possiamo anche implementare un'altra classe: ValuePriceIncreaser quale comportamento sarà:

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

Da un punto di vista esterno, nulla è cambiato, l'interfaccia è la stessa, hanno ancora gli stessi tre metodi ma il comportamento è diverso.

Dato che non esistono interfacce in FP, sarebbe difficile da implementare. In C, ad esempio, possiamo tenere puntatori a funzioni e chiamare quello appropriato in base alle nostre esigenze. Alla fine, in OOP funziona in modo molto simile, ma è "automatizzato" dal compilatore.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.