Come risolvere la violazione di LSP in base al presupposto minimo

8

Ho una classe Party con un costruttore che accetta Collection<Foo> . Ho in programma di avere due sottoclassi NpcParty e PlayerParty . Tutte le istanze di Party hanno un limite superiore per la dimensione della collezione di input (6). Tuttavia, un NpcParty ha un limite inferiore di 1 , mentre un PlayerParty ha un limite inferiore di 0 (o piuttosto, vi è un limite minimo implicito perché un List non può avere una dimensione negativa). Questa è una violazione di LSP perché NpcParty rafforza le precondizioni in termini di dimensioni della collezione di input.

Un NpcParty è inteso essere immutabile; il che significa che l'unico Foo s che avrà mai, sono quelli che sono specificati nel costruttore. La PlayerParty s di Foo può essere modificata in fase di runtime, sia in ordine, sia come riferimento / valore.

public class Party {

    // collection must not be null
    // collection size must not exceed PARTY_LIMIT
    public Party(List<Foo> foos) {
        Objects.requireNonNull(foos);

        if (foos.size() > PARTY_LIMIT) {
            throw new IllegalArgumentException("foos exceeds party limit of " + PARTY_LIMIT);
        }
    }
}

public class NpcParty extends Party {

    // additional precondition that there must be at least 1 Foo in the collection
    public NpcParty(List<Foo> foos) {
        super(foos);

        if (foos.size() < 1) {
            throw new IllegalArgumentException("foos must contain at least 1 Foo");
        }
    }
}

public class PlayerParty extends Party {

    // no additional preconditions
    public PlayerParty(List<Foo> foos) {
        super(foos);
    }
}

In che modo posso risolvere questa violazione, in modo che un NpcParty possa avere un limite minimo?

EDIT : darò un esempio di come potrei testarlo.

Supponiamo di avere una classe AbstractPartyUnitTests che ha testato il contratto minimo di tutte le implementazioni Party .

public class AbstractPartyUnitTests {

    @Test(expected = NullPointerException.class)
    public void testNullConstructor() {
        createParty(null);
    }

    @Test
    public void testConstructorWithEmptyList() {
        party = createParty(new ArrayList<Foo>());

        assertTrue(party != null);
    }

    @Test(expected = IllegalArgumentException.class)
    public void testConstructorThatExceedsMaximumSize() {
        party = createParty(Stream.generate(Foo::new)
                                  .limit(PARTY_LIMIT + 1)
                                  .collect(Collectors.toList()));
    }

    protected abstract Party createParty(List<Foo> foos);

    private Party party;
}

Con sottoclassi per PlayerParty e NpcParty

public class PlayerPartyUnitTests extends AbstractPartyUnitTests {

    @Override
    protected Party createParty(List<Foo> foos) {
        return new PlayerParty(foos);
    }
}

e

public class NpcPartyUnitTests extends AbstractPartyUnitTests {

    @Test
    public void testConstructorThatMeetsMinimumSize() {
        party = createParty(Stream.generate(Foo::new)
                                  .limit(1)
                                  .collect(Collectors.toList());

        assertTrue(party != null);
    }

    @Test(expected = IllegalArgumentException.class)
    public void testConstructorThatDoesNotMeetMinimumSize() {
        party = createParty(new ArrayList<Foo>());
    }

    @Override
    protected Party createParty(List<Foo> foos) {
        return new NpcParty(foos);
    }
}

Tutti i casi di test passeranno, ad eccezione di testConstructorWithEmptyList durante l'esecuzione come NpcPartyUnitTests . Il costruttore fallirebbe e, come tale, il test fallirebbe.

Ora, potrei rimuovere quel test dalla classe AbstractPartyUnitTests , in quanto non si applica a tutti i tipi di Party ; ma poi ovunque io abbia un Party , potrei non essere in grado di avere una sostituzione 1: 1 di NpcParty .

    
posta Zymus 22.04.2016 - 08:33
fonte

2 risposte

8

Nel tuo esempio sopra non c'è in realtà alcuna violazione dell'LSP. Ogni oggetto NpcParty è ancora un oggetto Party valido e può essere utilizzato per lo scambio. L'LSP non tratta lo scambio di classe A per classe B, si tratta di scambiare oggetti di tipo A con oggetti di tipo B. Così, costruttori e vincoli che sono solo controllati lì, non sono soggetti alla LSP .

Ad esempio, si assume che Player abbia una proprietà aggiuntiva "Size". Cerchi di scrivere un caso di test in cui inietti il giocatore dall'esterno e vuoi verificare se quella dimensione è sempre nei limiti consentiti:

 void TestSize(Player p)
 {
       AssertTrue(p.Size>=0);
       AssertTrue(p.Size<=PARTY_LIMIT);

       // ??? try to write a test for "Size" which does not fail for
       // ordinary Player objects, or PlayerParty objects, but fails for NpcPLayer
       // -> not possible without explicitly checking the type
 }

Come vedi, qualunque cosa passi in quel metodo di test, il test non fallisce. Non c'è nulla qui in cui non sia possibile sostituire gli oggetti normali del Player con NpcPlayer Objects.

Se, tuttavia, la classe Party memorizzerebbe il suo input da qualche parte e fornisce metodi per modificare il numero di elementi in seguito (controllando un vincolo che il numero di elementi abbia zero), quindi avere una classe derivata con un più strong i vincoli violerebbero il LSP, poiché non è possibile utilizzare un oggetto NpcParty come uno scambio per Party .

Per risolvere questo problema, implementa Party più generale - inserisci i limiti minimi e massimi per la raccolta da parte del costruttore e memorizzali da qualche parte. In questo modo, ogni utente di un oggetto Party non deve assumere che questi limiti siano zero e PARTY_LIMIT - ciò rende NpcParty uno scambio valido per Party .

    
risposta data 22.04.2016 - 08:51
fonte
1

Il tuo codice come dichiarato non è necessariamente una violazione del Principio di sostituzione di Liskov: puoi introdurre un vincolo aggiuntivo nel costruttore finché gli oggetti istanziati NpcParty non assumono che il loro foos le raccolte hanno sempre almeno un membro.

Non sembra che sarà così, e probabilmente vorrai fare affidamento su quella proprietà di NpcParty oggetti nella logica di business della tua applicazione. Se ho ragione, hai ragione che, in linea di principio, si tratta di una violazione del Principio di sostituzione di Liskov.

Puoi correggere questa violazione spostando qualsiasi comportamento che si basa sui vincoli delle dimensioni della parte nelle sottoclassi: PlayerParty dovrebbe definire un vincolo sulla sua dimensione di partito proprio come NpcParty fa ora, e Party non dovrebbe fare affermazioni riguardo dimensione del gruppo (o, per lo meno, solo affermazioni valide per tutti i suoi sottotipi).

    
risposta data 22.04.2016 - 10:07
fonte