Vraag Zijn de methoden van Collections.unmodifiableXXX in strijd met LSP?


Liskov Vervangingsprincipe is een van de principes van SOLIDE. Ik heb dit principe nu een aantal keer gelezen en geprobeerd het te begrijpen.

Hier is wat ik er van maak,

Dit principe houdt verband met een sterk gedragscontract bij de   hiërarchie van klassen. De subtypen moeten kunnen worden vervangen door   supertype zonder het contract te schenden.

Ik heb een ander gelezen artikelen en ik ben een beetje verdwaald na te denken over deze vraag. Do Collections.unmodifiableXXX() methoden zijn niet in strijd met LSP?

Een uittreksel uit het artikel hierboven gelinkt:

Met andere woorden, bij gebruik van een object via de interface van de basisklasse,   de gebruiker kent alleen de randvoorwaarden en de postcondities van de basis   klasse. Dus, afgeleide objecten mogen niet verwachten dat dergelijke gebruikers gehoorzamen   randvoorwaarden die sterker zijn dan die vereist door de basisklasse

Waarom ik denk van wel?

Voor

class SomeClass{
      public List<Integer> list(){
           return new ArrayList<Integer>(); //this is dumb but works
      }
}

Na

class SomeClass{
     public List<Integer> list(){
           return Collections.unmodifiableList(new ArrayList<Integer>()); //change in implementation
     }
}

Ik kan de implentatie van niet veranderen SomeClass om onbestelbare lijst in de toekomst te retourneren. De compilatie zal werken, maar als de client op de een of andere manier geprobeerd heeft de List geretourneerd dan zou het tijdens runtime mislukken.

Is dit waarom Guava gescheiden is gemaakt? ImmutableXXX interfaces voor collecties?

Is dit niet een directe schending van LSP of heb ik het helemaal verkeerd?


32
2018-02-26 19:06


oorsprong


antwoorden:


LSP zegt dat elke subklasse dezelfde contracten moet gehoorzamen als de superklasse. Of dit al dan niet het geval is Collections.unmodifiableXXX() dus hangt af van hoe dit contract luidt.

De objecten geretourneerd door Collections.unmodifiableXXX() werp een uitzondering als men probeert om een ​​modificerende methode aan te roepen. Bijvoorbeeld, als add() wordt genoemd, een UnsupportedOperationException zal worden gegooid.

Wat is het algemene contract van add()? Volgens de API-documentatie het is:

Zorgt ervoor dat deze verzameling het opgegeven element bevat (optioneel   operatie). Retourneert true als deze verzameling is gewijzigd als gevolg van de   noemen. (Geeft false als deze verzameling geen duplicaten en   bevat al het opgegeven element.)

Als dit het volledige contract was, dan kon de niet-wijzigbare variant inderdaad niet worden gebruikt op alle plaatsen waar een verzameling kan worden gebruikt. De specificatie gaat echter verder en zegt ook dat:

Als een verzameling om welke reden dan ook weigert een bepaald element toe te voegen   anders dan dat het al het element bevat, moet het een gooien   uitzondering (in plaats van false te retourneren). Dit bewaart de invariant   dat een verzameling hierna altijd het opgegeven element bevat   oproep terug.

Hiermee kan een implementatie expliciet code bevatten die geen argument toevoegt add naar de verzameling, maar resulteert in een uitzondering. Uiteraard omvat dit ook de verplichting voor de cliënt van de verzameling dat zij die (wettelijke) mogelijkheid in aanmerking nemen.

Dus gedrags-subtypen (of de LSP) is nog steeds vervuld. Maar dit toont aan dat als men van plan is om ander gedrag te hebben in subklassen die ook moeten worden voorzien in de specificatie van de klasse op topniveau.

Goede vraag trouwens.


30
2018-02-26 19:20



Ja, ik geloof dat je het juist hebt. Kortom, om de LSP te vervullen, moet je alles kunnen doen met een subtype dat je zou kunnen doen met de supertype. Dit is ook de reden waarom het Ellips / Cirkelprobleem op de proppen komt met de LSP. Als een Ellips een heeft setEccentricity methode, en een Circle is een subklasse van Ellipse, en de objecten moeten mutable zijn, er is geen manier waarop Circle de setEccentricity methode. Er is dus iets dat je kunt doen met een ellips die je niet kunt doen met een cirkel, dus LSP wordt geschonden. † Evenzo kun je iets doen met een gewone cirkel List dat kun je niet doen met een omwikkeld door Collections.unmodifiableList, dus dat is een LSP-overtreding.

Het probleem is dat er hier iets is dat we willen hebben (een onveranderlijke, niet-wijzigbare, alleen-lezen lijst) die niet wordt vastgelegd door het type systeem. In C # zou je kunnen gebruiken IEnumerable die het idee van een reeks vastlegt die je kunt herhalen en lezen van, maar niet kunt schrijven naar. Maar in Java is dat alleen List, die vaak wordt gebruikt voor een veranderlijke lijst, maar die we soms zouden willen gebruiken voor een onveranderlijke lijst.

Nu, sommigen zullen misschien zeggen dat Circle kan implementeren setEccentricity en gooi gewoon een uitzondering, en op dezelfde manier werpt een niet-veranderbare lijst (of een onveranderlijke lijst van Guava) een uitzondering op wanneer je het probeert te wijzigen. Maar dat betekent niet echt dat het is een Lijst vanuit het oogpunt van het LSP. Allereerst schendt het op zijn minst het principe van de minste verrassing. Als de beller een onverwachte uitzondering krijgt bij het proberen een item aan een lijst toe te voegen, is dat nogal verrassend. En als de aanroepende code stappen moet ondernemen om onderscheid te maken tussen een lijst die hij kan wijzigen en een die hij niet kan wijzigen (of een vorm waarvan hij de excentriciteit kan instellen, en een die hij niet kan), dan is hij niet echt substitueerbaar voor de andere .

Het zou beter zijn als het systeem van het Java-type een type had voor een reeks of verzameling die alleen itereren toestond, en een andere die wijziging toestond. Misschien kan Iterable hiervoor worden gebruikt, maar ik vermoed dat het enkele functies mist (zoals size()) die men echt zou willen. Helaas denk ik dat dit een beperking is van de huidige Java collections API.

Verschillende mensen hebben opgemerkt dat de documentatie voor Collection staat een implementatie toe om een ​​uitzondering te maken van de add methode. Ik veronderstel dat dit betekent dat een lijst die niet kan worden gewijzigd, de letter van de wet gehoorzaamt als het gaat om het contract voor add maar ik denk dat men zijn code moet onderzoeken en zien hoeveel plaatsen er zijn die oproepen beschermen tegen muterende methoden van Lijst (add, addAll, remove, clear) met try / catch-blokken voordat wordt beweerd dat de LSP niet wordt geschonden. Misschien is het dat niet, maar dat betekent dat alle code die roept List.add op een lijst die het heeft ontvangen als een parameter is verbroken.

Dat zou zeker veel zeggen.

(Vergelijkbare argumenten kunnen aantonen dat het idee dat null is een lid van elk type is ook een overtreding van het Liskov Substitutie Principe.)

† Ik weet dat er andere manieren zijn om het Ellipse / Circle-probleem aan te pakken, bijvoorbeeld door ze onveranderbaar te maken of door de methode setCentricity te verwijderen. Ik heb het hier alleen over het meest voorkomende geval, als een analogie.


10
2018-02-26 19:27



Ik geloof niet dat het een overtreding is, omdat het contract (dat wil zeggen de List interface) zegt dat de mutatieoperaties optioneel zijn.


4
2018-02-26 19:11



Ik denk dat je dingen hier niet mengt.
Van LSP:

Liskovs notie van een gedragssubtype definieert een begrip van   substitueerbaarheid voor veranderlijke objecten; dat wil zeggen, als S een subtype van T is,   dan kunnen objecten van het type T in een programma worden vervangen door objecten van   type S zonder de gewenste eigenschappen ervan te wijzigen   programma (bijv. correctheid).

LSP verwijst naar subklassen.

Lijst is een interface geen superklasse. Het specificeert een lijst met methoden die een klasse biedt. Maar de relatie is niet gekoppeld zoals bij een ouderklasse. Het feit dat klasse A en klasse B dezelfde interface implementeren, biedt geen garantie voor het gedrag van deze klassen. Eén implementatie kan altijd true retourneren en de andere een uitzondering werpen of altijd false retourneren of wat dan ook, maar beiden houden zich aan de interface terwijl ze de methoden van de interface implementeren, zodat de beller de methode op het object kan aanroepen.


1
2018-02-26 23:22