Il modello di stato viola il principio di sostituzione di Liskov?

17

Questa immagine è tratta da Applicazione di design e pattern basati sul dominio: con esempi in C # e .NET

Questoèildiagrammadiclasseperil Pattern di stato dove un SalesOrder può avere stati diversi durante la sua vita. Sono consentite solo determinate transizioni tra i diversi stati.

Ora la classe OrderState è una classe abstract e tutti i suoi metodi sono ereditati dalle sue sottoclassi. Se consideriamo la sottoclasse Cancelled che è uno stato finale che non consente transizioni ad altri stati, dovremo a override tutti i suoi metodi in questa classe per generare eccezioni.

Ora questo non viola il principio di sostituzione di Liskov dal momento che un sottolivello non dovrebbe alterare il comportamento del genitore? Cambiare la classe astratta in un'interfaccia risolve questo?
Come può essere risolto?

    
posta Songo 08.01.2013 - 19:30
fonte

3 risposte

3

Questa particolare implementazione, sì. Se rendi le classi concrete degli stati piuttosto che implementatori astratti, ti libererai da questo.

Tuttavia, lo schema di stato a cui ci si riferisce è in effetti un progetto di macchina di stato è in generale qualcosa con cui non sono d'accordo dal modo in cui l'ho visto crescere. Penso che ci siano motivi sufficienti per denunciare questo come una violazione del principio di responsabilità unica 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 elemento di gestione dello stato che viene centralizzato più spesso non richiede regole di business rilevanti per molte diverse parti del sistema per coordinarle ragionevolmente.

Immaginate se tutte le parti del sistema che si preoccupano dello stato fossero in servizi diversi, processi diversi su macchine diverse, un gestore di stato centrale che dettagli lo stato di ciascuno di quei luoghi effettivamente colli di bottiglia l'intero sistema distribuito e penso che il collo di bottiglia è un segno di una violazione SRP e di una progettazione generalmente errata.

Al contrario, suggerirei di rendere gli oggetti più intelligenti come nell'oggetto Model nel pattern MVC in cui il modello sa come gestirsi da solo, non ha bisogno di un orchestratore esterno per gestirne il funzionamento interno o il motivo.

Anche inserendo uno schema di stato come questo all'interno di un oggetto in modo che sia solo la gestione di se stesso, sembra che tu possa rendere quell'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 dentro di sé.

Ma a quel punto è più arte che ingegneria e quindi è decisamente soggettivo il tuo approccio ad alcune di quelle cose, che ha detto che i principi sono una buona guida e sì l'implementazione che elimini è un LSP violazione, ma potrebbe essere corretto non essere. Basta stare molto attenti all'SRP quando si usano schemi di questa natura e si rischia di essere al sicuro.

    
risposta data 08.01.2013 - 19:50
fonte
7

a sublcass shouldn't alter the behavior of the parent?

Questa è una comune interpretazione errata di LSP. Una sottoclasse può alterare il comportamento del genitore, a patto che rimanga fedele al tipo genitore.

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

... there are a number of behavioral conditions that the subtype must meet. These are detailed in a terminology resembling that of design by contract methodology, leading to some restrictions on how contracts can interact with inheritance:

  • Preconditions cannot be strengthened in a subtype.
  • Postconditions cannot be weakened in a subtype.
  • Invariants of the supertype must be preserved in a subtype.
  • History constraint (the "history rule"). Objects are regarded as being modifiable only through their methods (encapsulation). Since subtypes may introduce methods that are not present in the supertype, the introduction of these methods may allow state changes in the subtype that are not permissible in the supertype. The history constraint prohibits this. It was the novel element introduced by Liskov and Wing. A violation of this constraint can be exemplified by defining a MutablePoint as a subtype of an ImmutablePoint. This is a violation of the history constraint, because in the history of the Immutable point, the state is always the same after creation, so it cannot include the history of a MutablePoint in general. Fields added to the subtype may however be safely modified because they are not observable through the supertype methods. One may derive a CircleWithFixedCenterButMutableRadius from ImmutablePoint without violating LSP.

Personalmente, trovo più semplice ricordare semplicemente questo: se guardo un parametro in un metodo che ha tipo A, qualcuno che passa un sottotipo B mi causerebbe qualche sorpresa? Se poi ci fosse una violazione di LSP.

Lanciare un'eccezione è una sorpresa? Non proprio. È qualcosa che può accadere in qualsiasi momento, sia che chiami il metodo Ship su OrderState sia Grant o Shipped. Quindi devo renderlo conto e non è in realtà una violazione di LSP.

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

Ma anche io non utilizzerei il pattern State per questa particolare situazione. Lo schema di stato è molto più appropriato allo stato di un'applicazione rispetto allo stato di un oggetto di dominio come questo.

Quindi, in breve, questo è un esempio poco elaborato del modello di stato e non un modo particolarmente valido per gestire lo stato di un ordine. Ma probabilmente non viola LSP. E lo schema di stato, in sé e per sé, certamente non lo è.

    
risposta data 08.01.2013 - 20:02
fonte
2

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

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

No new exceptions should be thrown by methods of the subtype, except where those exceptions are themselves subtypes of exceptions thrown by the methods of the supertype.

Come dovresti capirlo? Quali 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 come "genera InvalidOperationException se questa operazione non è supportata dallo stato corrente", allora penso che questo progetto non interrompa LSP. D'altra parte, se i metodi dei supertipi non sono documentati in questo modo, LSP viene violato.

Ma questo non significa che questo è un buon design, non si dovrebbero usare eccezioni per il normale flusso di controllo, il che sembra molto vicino. Inoltre, in una vera applicazione, vorresti probabilmente sapere se un'operazione può essere eseguita prima se tenti di farlo, ad esempio per disattivare il pulsante "Spedisci" nell'interfaccia utente.

    
risposta data 08.01.2013 - 21:54
fonte