Come sarebbe programmato in non-OO? [chiuso]

11

Leggendo un articolo severo sugli aspetti negativi di OOP a favore di qualche altro paradigma ho trovato un esempio che non riesco a trovare.

Voglio essere aperto agli argomenti dell'autore, e sebbene io possa teoricamente comprendere i loro punti, un esempio in particolare sto facendo fatica a immaginare come sarebbe meglio implementato, per esempio, in un linguaggio FP.

Da: link

// 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.

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

Quello che sto specificatamente chiedendo è come questo esempio sarà modellato / programmato in un linguaggio FP (codice reale, non in teoria)?

    
posta Danny Yaroslavski 10.05.2017 - 21:11
fonte

6 risposte

42

Nello stile FP, Product sarebbe una classe immutabile, product.setPrice non muterà un oggetto Product ma restituirà invece un nuovo oggetto e la funzione increasePrice sarebbe una funzione "autonoma". Usando una sintassi simile all'aspetto (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 nucleo non è molto diverso qui, tranne il codice "boilerplate" dall'esempio OOP inventato è omesso. Tuttavia, non vedo questo come prova che l'OOP porti a un codice gonfiato, solo come prova del fatto che se si costruisce un esempio di codice sufficientemente sufficientemente artificiale, è possibile provare qualsiasi cosa.

    
risposta data 10.05.2017 - 22:12
fonte
17

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

In "a" linguaggio FP? Se è sufficiente, seleziono la lisp di Emacs. Ha il concetto di tipi (tipo, tipo di), ma solo quelli built-in. Quindi il tuo esempio si riduce a "come fai a moltiplicare ogni elemento in una lista per qualcosa e restituisci una nuova lista".

(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 di "matching" funzionale. Controlla Haskell:

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

(O qualcosa del genere, sono anni ...)

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

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

Il problema dell'articolo non è che sia 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 anche a livello di assemblatore. Vorrei mai dire che nessuno di questi paradigmi è enormemente superiour l'altro in ogni aspetto. Direi che mi piace lingua X meglio di Y? Certo che lo farei! Ma non è quello di cui tratta questo articolo.

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

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

    
risposta data 11.05.2017 - 02:01
fonte
6

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

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

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

Questa è la parte facile del design orientato agli oggetti. Facciamo una lezione per tutti gli "oggetti" di cui sopra e stiamo bene, giusto? Crea una classe Product 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? Creiamo un CategoryManager separato per tenere traccia di tutti i diversi tipi di categorie o questa responsabilità appartiene alla classe Category che abbiamo già creato?

Ora che ne dici di funzioni che ti danno uno sconto se hai una certa quantità di articoli da due diverse categorie? Passa alla classe Product , alla classe Category , alla classe DiscountRule , alla classe CategoryManager o abbiamo bisogno di qualcosa di nuovo? Questo è il modo in cui finiamo con cose come DiscountRuleProductCategoryFactoryBuilder .

Nel codice funzionale, la gerarchia dei dati è completamente ortogonale alle tue funzioni. È possibile ordinare le proprie funzioni in qualsiasi modo abbia senso semantico. Ad esempio, puoi raggruppare tutte le funzioni che cambiano i prezzi dei prodotti insieme, nel qual caso avrebbe senso tratteggiare funzionalità comuni come mapPrices nel seguente esempio 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)

Potrei probabilmente aggiungere altre funzioni relative ai prezzi qui come decreasePrice , applyBulkDiscount , ecc.

Poiché utilizziamo anche una raccolta di Products , la versione OOP deve includere metodi per gestire quella raccolta, ma non vuoi che questo modulo riguardi la selezione dei prodotti, vuoi che si riferisca ai prezzi. L'accoppiamento dati-funzione ti ha costretto a lanciare anche la gestione delle collezioni. Anche

Puoi provare a risolvere questo problema inserendo il membro products in una classe separata, ma poi finisci con classi molto strettamente accoppiate. I programmatori OO pensano che l'accoppiamento dati-funzione sia molto naturale e persino vantaggioso, ma c'è un alto costo associato ad esso nella perdita di flessibilità. Ogni volta che crei una funzione, devi assegnarla a una sola classe. Ogni volta che si desidera utilizzare una funzione, è necessario trovare un modo per ottenere i dati accoppiati al punto di utilizzo. Queste restrizioni sono enormi.

    
risposta data 11.05.2017 - 07:56
fonte
2

Separando semplicemente i dati e la funzione a cui alludeva l'autore potrebbe essere simile a questo 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 puoi aumentare il prezzo su un elenco di prodotti.

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

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

Nota: se non hai familiarità con FP, ogni funzione restituisce un valore. Venendo da un linguaggio simile a C, puoi trattare l'ultima istruzione in una funzione come se avesse una return davanti a essa.

Ho incluso alcune annotazioni di tipo, ma non dovrebbero essere necessarie. 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 map per eseguire una funzione su ogni elemento nell'elenco e restituisce il risultato in un nuovo elenco.

Si noti che il modulo Product non deve sapere nulla sul looping, poiché tale responsabilità rimane con il modulo List (che ha creato la necessità di eseguire il looping).

    
risposta data 10.05.2017 - 22:32
fonte
1

Lasciatemi prefigurare questo con il fatto che non sono un esperto di programmazione funzionale. Sono più di una persona OOP. Quindi, mentre sono abbastanza sicuro che il seguito è come si otterrebbe lo stesso tipo di funzionalità con FP, potrei sbagliarmi.

Questo è In Typescript (quindi tutte le annotazioni di tipo). 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 );

In dettaglio (e ancora, non un esperto di FP), la cosa da capire è che non c'è un sacco di comportamento predefinito. Non esiste un metodo di "aumento prezzo" che applichi un aumento di prezzo su tutta la lista, 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, basta creare una serie di prodotti. È quindi possibile utilizzare le procedure FP standard per manipolare questo array nel modo desiderato: filtrare per selezionare elementi particolari, mappare per regolare gli interni, ecc ... Si termina con un controllo più dettagliato sul proprio elenco di prodotti senza dover essere limitati dal API fornita da SimpleProductManager. Questo può essere considerato un vantaggio da alcuni. È anche vero che non devi preoccuparti di alcun bagaglio associato alla classe ProductManager. Infine, non ci sono preoccupazioni per "SetProducts" o "GetProducts", perché non c'è nessun oggetto che nasconde i tuoi prodotti: invece, hai solo l'elenco di prodotti con cui stai lavorando. Di nuovo, questo può essere un vantaggio o uno svantaggio a seconda delle circostanze / persona con cui stai parlando. Inoltre, non esiste ovviamente una gerarchia di classi (che è ciò di cui si lamentava) perché non ci sono classi in primo luogo.

Non mi sono preso il tempo di leggere tutta la sua sfuriata. Uso le pratiche FP quando è conveniente, ma sono decisamente più un tipo di OOP. Così ho pensato dal momento che ho risposto alla tua domanda, vorrei anche fare alcuni brevi commenti sulle sue opinioni. Penso che questo sia un esempio molto elaborato che evidenzia gli "aspetti negativi" di OOP. In questo caso particolare, per la funzionalità mostrata, OOP probabilmente è over-kill e FP probabilmente sarebbe una soluzione migliore. Poi di nuovo, se questo fosse per qualcosa come un carrello della spesa, proteggere la lista dei prodotti e limitarne l'accesso è (penso) un obiettivo molto importante del programma, e FP non ha modo di far rispettare tali cose. Ancora una volta, potrebbe essere solo che non sono un esperto di FP, ma avendo implementato carrelli per i sistemi di e-commerce, preferirei utilizzare OOP piuttosto che FP. I principi di incapsulamento in OOP ti danno molto più controllo per assicurarti che le cose siano modificate correttamente, che tu passi sempre da uno stato valido a un altro, e che il tuo carrello sia sempre correttamente aggiornato.

Personalmente ho difficoltà a prendere sul serio chiunque faccia una discussione così strong per "X è semplicemente terribile. Usa sempre Y". La programmazione ha una varietà di strumenti e paradigmi perché ci sono una grande varietà di problemi da risolvere. FP ha il suo posto, OOP ha il suo posto, e nessuno sarà un grande programmatore se non può 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ò, si tratta semplicemente di un contenitore di dati stupido: non credo che il mio uso di esso violi i principi di FP. È più di un aiuto per il controllo dei tipi.

** nota: non mi ricordo in cima alla mia testa e non ho controllato se il modo in cui ho usato la funzione mappa avrebbe modificato i prodotti sul posto, cioè ho inavvertitamente raddoppiato il prezzo dei prodotti in l'array di prodotti originali. 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.

    
risposta data 10.05.2017 - 22:17
fonte
0

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

La sua semplice implementazione dell'interfaccia di ProductManager che è fondamentalmente un contratto che definisce quali azioni (comportamenti) l'oggetto deve fare.

Se sarebbe un bambino (o meglio, una classe o una classe iherited che estende un'altra funzionalità di classe) sarebbe scritta come:

class SimpleProductManager extends ProductManager {
    ...
}

Quindi sostanzialmente, l'autore dice:

Abbiamo qualche oggetto quale comportamento è: setProducts, increasePrice, getProducts. E non ci interessa 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 qualche valore percentuale.

Ma possiamo anche implementare un'altra classe: ValuePriceIncreaser che behavor sarà:

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

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

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

    
risposta data 10.05.2017 - 23:47
fonte

Leggi altre domande sui tag