Il modello statale viola il principio di sostituzione di Liskov?


17

Questa immagine è tratta dall'applicazione di modelli e schemi basati su dominio: con esempi in C # e .NET

inserisci qui la descrizione dell'immagine

Questo è il diagramma di classe per il modello di stato in cui a SalesOrderpuò avere stati diversi durante la sua vita. Sono consentite solo determinate transizioni tra i diversi stati.

Ora la OrderStateclasse è una abstractclasse e tutti i suoi metodi sono ereditati dalle sue sottoclassi. Se consideriamo la sottoclasse Cancelledche è uno stato finale che non consente alcuna transizione verso nessun altro stato, dovremo applicare overridetutti i suoi metodi in questa classe per generare eccezioni.

Ora, ciò non viola il principio di sostituzione di Liskov poiché una sottoclasse non dovrebbe alterare il comportamento del genitore? Cambiare la classe astratta in un'interfaccia risolve questo?
Come si puo aggiustare?


Modifica OrderState in una classe concreta che genera eccezioni "NotSupported" in tutti i suoi metodi per impostazione predefinita?
James

Non ho letto questo libro, ma mi sembra strano che OrderState contenga Cancel () e quindi ci sia stato Cancelled, che è un sottotipo diverso.
Euforico

@Euforico perché è strano? la chiamata cancel()modifica lo stato in annullato.
Songo,

Come? Come si può chiamare Annulla su OrderState, quando l'istanza di stato in SalesOrder dovrebbe cambiare. Penso che l'intero modello sia sbagliato. Vorrei vedere la piena attuazione.
Euforico

Risposte:


3

Questa particolare implementazione, sì. Se rendi gli stati classi concrete anziché implementatori astratti, ti allontanerai da questo.

Tuttavia, il modello di stato a cui ti riferisci che è effettivamente una progettazione di macchine a stati è in generale qualcosa con cui non sono d'accordo dal modo in cui l'ho visto crescere. Penso che vi siano motivi sufficienti per dichiararlo una violazione del principio della responsabilità singola perché questi modelli di gestione dello stato finiscono per essere repository centrali per la conoscenza dello stato attuale di molte altre parti di un sistema. Questo pezzo di gestione statale in fase di centralizzazione richiederà il più delle volte regole di business pertinenti a molte parti diverse del sistema per coordinarle ragionevolmente.

Immagina se tutte le parti del sistema che hanno a cuore lo stato si trovassero in servizi diversi, processi diversi su macchine diverse, un gestore dello stato centrale che dettaglia lo stato di ciascuno di quei posti strozzando efficacemente l'intero sistema distribuito e penso che il collo di bottiglia sia un segno di una violazione di SRP nonché un design generalmente errato.

Al contrario, suggerirei di rendere gli oggetti più intelligenti come nell'oggetto Modello nel modello MVC in cui il modello sa come gestire se stesso, non ha bisogno di un orchestratore esterno per gestire i suoi meccanismi interni o la ragione di ciò.

Anche inserendo un modello di stato come questo all'interno di un oggetto in modo che si gestisca solo da solo, sembra che lo si trasformerebbe in un oggetto troppo grande. I flussi di lavoro dovrebbero essere fatti attraverso la composizione di vari oggetti auto-responsabili, direi, piuttosto che con un singolo stato orchestrato che gestisce il flusso di altri oggetti o il flusso di intelligenza al suo interno.

Ma a quel punto è più arte che ingegneria e quindi è decisamente soggettivo il tuo approccio ad alcune di queste cose, che ha detto che i principi sono una buona guida e sì l'implementazione che elenchi è una violazione di LSP, ma potrebbe essere corretta per non esserlo. Fai solo molta attenzione all'SRP quando usi uno schema di questo tipo e probabilmente sarai al sicuro.


Fondamentalmente, il grosso problema è che l'orientamento agli oggetti è buono per l'aggiunta di nuove classi ma rende difficile l'aggiunta di nuovi metodi. E nel caso dello stato, come hai detto, non è probabile che dovrai estendere il codice con nuovi stati molto spesso.
hugomg

3
@Jimmy Hoffa Scusa, ma cosa intendi esattamente con "Se rendi gli stati classi concrete piuttosto che astrattori astratti, ti allontanerai da questo." ?
Songo,

@Jimmy: ho provato a implementare lo stato senza il modello di stato facendo in modo che ogni componente conoscesse solo il proprio stato. Ciò ha comportato grandi cambiamenti per rendere il flusso significativamente più complesso da implementare e mantenere. Detto questo, penso che usare una macchina a stati (idealmente la biblioteca di qualcun altro per costringerla a essere trattata come una scatola nera) piuttosto che il modello di progettazione dello stato (quindi, gli stati sono solo elementi all'interno di un enum piuttosto che classi complete e le transizioni sono semplicemente stupide bordi) offre molti dei vantaggi del modello statale pur essendo ostile ai tentativi da parte degli sviluppatori di abusarne.
Brian,

8

una sottoclasse non dovrebbe alterare il comportamento del genitore?

Questa è una comune interpretazione errata di LSP. Una sottoclasse può modificare il comportamento del genitore, purché rimanga fedele al tipo di genitore.

C'è una buona lunga spiegazione su Wikipedia , che suggerisce le cose che violeranno LSP:

... ci sono una serie di condizioni comportamentali che il sottotipo deve soddisfare. Questi sono dettagliati in una terminologia simile a quella del design secondo la metodologia del contratto, portando ad alcune restrizioni su come i contratti possono interagire con l'eredità:

  • Le condizioni preliminari non possono essere rafforzate in un sottotipo.
  • Le postcondizioni non possono essere indebolite in un sottotipo.
  • Gli invarianti del supertipo devono essere conservati in un sottotipo.
  • Vincolo storico (la "regola della storia"). Gli oggetti sono considerati modificabili solo attraverso i loro metodi (incapsulamento). Poiché i sottotipi possono introdurre metodi che non sono presenti nel supertipo, l'introduzione di questi metodi può consentire cambiamenti di stato nel sottotipo che non sono ammessi nel supertipo. Il vincolo storico lo proibisce. Era il nuovo elemento introdotto da Liskov e Wing. Una violazione di questo vincolo può essere esemplificata definendo un MutablePoint come sottotipo di un ImmutablePoint. Questa è una violazione del vincolo storico, perché nella storia del punto Immutabile, lo stato è sempre lo stesso dopo la creazione, quindi non può includere la storia di un MutablePoint in generale. I campi aggiunti al sottotipo possono tuttavia essere modificati in modo sicuro perché non sono osservabili attraverso i metodi del supertipo.

Personalmente, trovo più semplice semplicemente ricordare questo: se sto osservando un parametro in un metodo con tipo A, qualcuno che passa un sottotipo B mi causerebbe sorprese? Se lo facessero, allora c'è una violazione di LSP.

Lanciare un'eccezione è una sorpresa? Non proprio. È qualcosa che può accadere in qualsiasi momento, sia che io stia chiamando il metodo Ship su OrderState o Granted o Shped. Quindi devo renderlo conto e non è davvero una violazione di LSP.

Detto questo , penso che ci siano modi migliori per gestire questa situazione. Se lo scrivessi in C #, utilizzerei le interfacce e verificherei l'implementazione di un'interfaccia prima di chiamare il metodo. Ad esempio, se l'attuale OrderState non implementa IShippable, non chiamare un metodo Ship su di esso.

Ma poi non userei nemmeno il modello statale per questa particolare situazione. Il modello di stato è molto più appropriato allo stato di un'applicazione che allo stato di un oggetto di dominio come questo.

Quindi, in poche parole, questo è un esempio mal concepito del modello di stato e non un modo particolarmente buono per gestire lo stato di un ordine. Ma probabilmente non viola LSP. E il modello statale, in sé e per sé, certamente no.


Mi sembra che tu stia confondendo LSP con Principio di minima sorpresa / stupore, credo che LSP abbia confini più chiari in cui ciò li sta violando, anche se stai applicando il POLA più soggettivo che si basa su aspettative soggettive ; cioè ogni persona si aspetta qualcosa di diverso dal prossimo. LSP si basa su garanzie contrattuali e ben definite fornite dal fornitore, non soggettivamente valutate aspettative indovinate dal consumatore.
Jimmy Hoffa,

@JimmyHoffa: hai ragione, ed è per questo che ho spiegato il significato esatto prima di dichiarare un modo più semplice di ricordare LSP. Tuttavia, se ho una domanda nella mia mente che cosa significhi "sorpresa", tornerò indietro e controllerò le regole esatte di LSP (puoi davvero ricordarle a piacimento?). Le eccezioni che sostituiscono la funzionalità (o viceversa) sono difficili perché non violano alcuna clausola specifica di cui sopra, il che è probabilmente un indizio che dovrebbe essere gestito in un modo completamente diverso.
pdr

1
Un'eccezione è una condizione prevista definita nel contratto nella mia mente, penso a un NetworkSocket, potrebbe avere un'eccezione prevista per l'invio se non è aperta, se l'implementazione non genera quell'eccezione per un socket chiuso stai violando LSP , se il contratto stabilisce l'opposto e il sottotipo genera l'eccezione, allora stai violando LSP. È più o meno il modo in cui classifico le eccezioni in LSP. Per quanto riguarda il ricordare le regole; Potrei sbagliarmi su questo e sentitevi liberi di dirmelo, ma la mia comprensione di LSP vs POLA è definita nell'ultima frase del mio commento sopra.
Jimmy Hoffa l'

1
@JimmyHoffa: non avevo mai considerato POLA e LSP nemmeno lontanamente connessi (penso a POLA in termini di interfaccia utente) ma, ora che l'hai sottolineato, lo sono in qualche modo. Non sono sicuro che siano in conflitto, o che uno sia più soggettivo dell'altro. LSP non è una descrizione iniziale di POLA in termini di ingegneria del software? Allo stesso modo in cui mi sento frustrato quando Ctrl-C non è Copia (POLA), mi sento anche frustrato quando un ImmutablePoint è mutabile perché in realtà è una sottoclasse MutablePoint (LSP).
pdr

1
Considererei una CircleWithFixedCenterButMutableRadiusprobabile violazione di LSP se il contratto del consumatore per ImmutablePointspecifica che se X.equals(Y), i consumatori possono sostituire liberamente X con Y e viceversa, e devono quindi astenersi dall'utilizzare la classe in modo tale che tale sostituzione possa causare problemi. Il tipo potrebbe essere in grado di definire legittimamente in modo equalstale che tutte le istanze vengano confrontate come distinte, ma tale comportamento probabilmente ne limiterebbe l'utilità.
supercat

2

(Questo è scritto dal punto di vista di C #, quindi nessuna eccezione verificata.)

Secondo l' articolo di Wikipedia su LSP , una delle condizioni di LSP è:

Nessuna nuova eccezione dovrebbe essere generata dai metodi del sottotipo, tranne nel caso in cui tali eccezioni siano esse stesse sottotipi di eccezioni generate dai metodi del supertipo.

Come dovresti capirlo? Cosa sono esattamente le "eccezioni generate dai metodi del supertipo" quando i metodi del supertipo sono astratti? Penso che quelle siano le eccezioni documentate come le eccezioni che possono essere generate dai metodi del supertipo.

Ciò significa che se OrderState.Ship()è documentato come qualcosa di simile "genera InvalidOperationExceptionse questa operazione non è supportata dallo stato corrente", allora penso che questo progetto non rompa LSP. D'altra parte, se i metodi del supertipo non sono documentati in questo modo, LSP viene violato.

Ma questo non significa che questo sia un buon design, non dovresti usare eccezioni per il normale flusso di controllo, a cui questo sembra molto vicino. Inoltre, in un'applicazione reale, probabilmente vorrai sapere se un'operazione può essere eseguita prima di tentare di eseguirla, ad esempio per disabilitare il pulsante "Spedisci" nell'interfaccia utente.


In realtà questo è esattamente il punto che mi ha fatto pensare. Se le sottoclassi generano eccezioni non definite nel padre, deve essere una violazione di LSP.
Songo,

Penso che in una lingua in cui le eccezioni non vengono verificate, lanciarle non sia una violazione di LSP. È solo il concetto di un linguaggio stesso, che in qualsiasi momento e in qualsiasi momento è possibile generare qualsiasi eccezione. Quindi qualsiasi codice client dovrebbe essere pronto per questo.
Zapadlo,
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.