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.