Quando nella tua classe le classi sono anche oggetti, il principio di sostituzione di Liskov si applica alle loro interfacce?

8

Secondo Wikipedia il principio di sostituzione di Liskov afferma che

objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program

Normalmente il principio di sostituzione di Liskov non si applica alle classi perché non sono oggetti.

Ora in Ruby, anche le classi sono oggetti. La classe più semplice da cui ereditano tutte le altre classi è BasicObject . BasicObject consente il metodo di classe .new (il "costruttore", assegna un oggetto ed esegue il metodo di istanza #initialize su di esso) per essere chiamato senza argomenti.

In questo caso sospetto che il principio di Liskov implichi che ora ogni sottoclasse deve anche supportare .new senza argomenti, perché la sottoclasse è un oggetto e se non lo supporta, la sottoclasse non sarebbe un sostituire la sua classe genitore. Se applicato letteralmente, una conseguenza sarebbe che ogni costruttore deve supportare il fatto di essere chiamato senza argomenti.

Questo sembra strano. Ma mi chiedo se non abbia senso entrare in quella direzione? O è solo un'incompatibilità del principio di Liskov (che presuppone che l'insieme di tutti gli oggetti e l'insieme di tutte le classi siano disgiunti) e Ruby (dove l'insieme di tutte le classi è un sottoinsieme dell'insieme di tutti gli oggetti).

Questo mi ricorda stranamente l'antinomia di Russell , un paradosso che, per quanto ho capito, è stato risolto separando classi e istanze (renderle disgiunte).

    
posta aef 11.04.2016 - 22:02
fonte

4 risposte

3

Le lingue variano notevolmente nel modo in cui le classi si manifestano (vale a dire al runtime). In alcuni sistemi linguistici,

  • non esiste alcuna manifestazione di runtime di una classe, quindi Liskov non è semplicemente applicabile alle classi.

  • la manifestazione di runtime di tutte le classi è un'istanza diretta (di alcune classi come TYPE o CLASS, ad esempio) e tutto ciò che esse sono è un token che rappresenta una classe reale specifica, ma altrimenti non partecipa sottoclasse loro stessi. Quindi Liskov non si applica ancora (in quanto questi sono solo token, ovvero istanze di una sola classe piuttosto che di una gerarchia di classi).

  • Tuttavia, in altri sistemi, la manifestazione di runtime della classe è un'istanza di un meccanismo di metaclassi formale. In un tale sistema linguistico, ci si aspetterebbe di consentire la specifica utente delle metaclassi e quindi di consentire una normale sottoclasse definita dall'utente tra i metaclassi.

In questi sistemi, ci aspetteremmo che vengano applicate normali regole e problemi di sottoclassi. In quei sistemi potremmo creare sottoclassi di metaclassi che dovrebbero seguire Liskov, proprio come possiamo creare sottoclassi regolari che dovrebbero seguire Liskov. (Vedi nota sotto sulla definizione Liskov.)

Inoltre, in alcuni di questi sistemi, i metodi di istanza di una metaclasse forniscono i metodi statici per le sue classi; il che rende una forma abbastanza regolare in questo: l'aggiunta di un metodo di istanza a una metaclass di base renderebbe tale metodo di istanza disponibile per le sottoclassi di tale metaclass, pertanto rendono tali metodi disponibili come metodi statici su qualsiasi istanza di classi di quei meta-sub- classi. Come sempre quando la sottoclasse, Liskov può essere correttamente seguito o no.

Non so quale delle categorie precedenti (se esiste) Ruby cade sotto.

In this case I suspect that the Liskov principle would imply that now each subclass must also support .new without any arguments, because the subclass is an object and if it wouldn't support it, the subclass wouldn't be a substitute for it's parent class.

Sono d'accordo. (ma questo non significa necessariamente che Ruby!)

If applied literally, one consequence would be that every constructor has to support being called without any arguments.

Non è che ogni costruttore debba essere richiamabile senza argomenti, ma invece che ogni classe deve includere un costruttore senza parametri. (Molte lingue consentono a una determinata classe di avere più costruttori con diverse firme. Non so di Ruby.)

NOTA: Ricorda che Liskov ha a che fare con qualcosa di più della disponibilità del metodo in sottoclassi, ma anche sui comportamenti attesi di questi metodi tra la base e le sottoclassi. Non penso che potresti avere Liskov in una sottoclasse che non fornisce i metodi della classe base, quindi penserei che farlo sia un prerequisito di Liskov, eppure mentre necessario, fornire gli stessi metodi e firme non è sufficiente per Liskov (Anche il comportamento di runtime deve corrispondere). (Un certo numero di lingue imporrà in fase di compilazione tramite il loro sistema di tipi che le sottoclassi forniscono metodi di classe base, ma non conosco alcun linguaggio che applichi il comportamento previsto è sostanzialmente più soggettivo.)

Ad esempio, una sottoclasse che fornisce il metodo della classe base, ma genera sempre un'eccezione (ad es. metodo non consentito o imprevisto) viola Liskov (supponendo che sia previsto un comportamento più normale nella base o in altre sottoclassi).

    
risposta data 11.04.2016 - 23:27
fonte
2

In this case I suspect that the Liskov principle would imply that now each subclass must also support .new without any arguments

Il tuo sospetto è sbagliato.

Il problema è che stai inferendo dall'implementazione della classe base quale sia il contratto della classe base. Ma sono due cose distinte, anche se non scritte letteralmente separatamente. In questo caso, il contratto della classe base è che tu puoi costruire cose con new , e se gli argomenti non sono corretti, potresti errore (ad esempio lancio).

Il fatto che nella classe base, gli argomenti zero siano accettabili, non richiede immediatamente che nel contratto, gli argomenti zero siano accettabili e devono essere consentiti da tutte le classi derivate.

È semplicemente un'implementazione predefinita di un contratto. Può permettere cose al di fuori di quel contratto. Può offrire cose che sono opzionalmente parte di quel contratto. Non esiste una relazione 1: 1 rigida tra l'implementazione della classe base e il contratto. Non si può presumere che solo perché l'implementazione della classe base permette qualcosa che è immediatamente una parte obbligatoria del contratto.

Tuttavia, in risposta alla parte più generale della tua domanda, allora si. I metatipi di classe sono istanze come le altre e rispettano le stesse regole di qualsiasi altra gerarchia di ereditarietà, incluso LSP.

    
risposta data 11.04.2016 - 23:52
fonte
1

La risposta giusta implica molto, "Non proprio", con una spolverata di "Hai un punto."

Per prima cosa prendiamo un'interpretazione letterale. Ruby ha metodi di classe e metodi di istanza. I metodi di classe sono i metodi dell'oggetto di classe. I metodi di istanza sono i metodi delle istanze di quella classe. L'ereditarietà è una relazione tra oggetti di classe: chiamare .superclass su una classe ti dà un altro. Comunque quella relazione tra le classi NON è propriamente una relazione di sottotipo. E quindi il Principio di sostituzione di Liskov si applica solo ai metodi di istanza, non ai metodi di classe. Quindi avere diversi metodi di classe per le classi in un albero di ereditarietà non è una violazione del principio.

Puoi dimostrarlo a te stesso creando sottoclassi indipendenti di Class , creando un oggetto foo di una classe. Crea un oggetto dell'altra classe, usando MyClass2.new(foo) quando lo crei per farlo ereditare dal primo. E ora vedi in modo esplicito che una classe può ereditare da un'altra classe mentre quegli oggetti di classe sono tipi diversi di cose.

Questa è la risposta "non proprio". Ora per l'aspersione.

Nell'uso originale, un "tipo" è un contratto per le operazioni supportate su un oggetto. Nei linguaggi tipizzati staticamente, i contratti sono espliciti - il compilatore non ti permetterà di provare a passare un oggetto del tipo sbagliato a funzioni che sanno quello che vogliono.

Tuttavia Ruby usa la digitazione anatra. Puoi passare qualsiasi cosa ovunque e fintanto che risponde ai giusti metodi o fa la cosa giusta, le cose funzioneranno. Ciò rende implicito il contratto di un "tipo", non esplicito. In particolare "tipo" non significa sempre classe. (Lo fa spesso, ma non sempre.) E la vera relazione di un tipo si basa su quali operazioni ci sono nel codice.

Quindi se il tuo codice specifica un contratto per ciò che fa un plugin, qualsiasi classe che implementa quel contratto è del tipo giusto. Se il tuo codice esegue la meta-programmazione e chiama .class per ottenere la classe, inizia a fare cose con la classe, i metodi di classe sull'oggetto SONO parte di ciò che rende un tipo un tipo. Se il tuo codice utilizza l'introspezione per scoprire tutte le classi il cui nome segue una determinata convenzione e fa qualcosa con loro (le persone fanno cose del genere con framework di test unitari) allora il modello di denominazione della classe IS fa parte del tuo tipo.

Pensa a questo, "Un tipo è un contratto e il contratto è implicitamente definito da quali operazioni si trovano nel codice."

Se ci pensi in questo modo, le funzionalità dinamiche di Ruby consentono praticamente a qualsiasi cosa di diventare parte del tipo. E ciò a cui si applica il Principio di sostituzione di Liskov dipende dalle caratteristiche dinamiche che si sceglie di utilizzare. Inoltre, in qualsiasi parte complessa del software Ruby - comprese le sue classi principali, è facile scrivere un nuovo codice che scopre che stai violando il Principio di sostituzione di Liskov.

Questo suona spaventoso. Cosa significa in pratica?

Significa che devi fare attenzione quando aggiungi metaprogrammazione complessa a un sistema esistente. Significa anche che dovresti essere chiaro per te stesso quali tipi di contratti software tu mantenga. E, supponendo che tu non stia eseguendo metaprogrammazione complessa, devi prestare maggiore attenzione al Principio di sostituzione di Liskov sui metodi di istanza di quelli di classe.

    
risposta data 12.04.2016 - 21:14
fonte
0

In this case I suspect that the Liskov principle would imply that now each subclass must also support .new without any arguments

Questo non è corretto.

Si richiede qui che il metaoggetto di una classe derivata da BaseObject debba supportare nuovo niladico, perché il metaoggetto di BaseObject lo fa.

Il LSP afferma che gli oggetti di una sottoclasse devono supportare la stessa interfaccia della classe base.

La disconnessione qui è che i metaoggetti di Derived e BaseObject sono entrambe le istanze di Class , non di Derived e BaseObject rispettivamente, quindi l'LSP non si applica. Si potrebbe sostenere che come istanze della stessa classe, dovrebbero comunque avere la stessa interfaccia, ma a) che non fa parte dell'LSP, e b) non è così che funziona Ruby.

    
risposta data 12.04.2016 - 09:08
fonte

Leggi altre domande sui tag