Skip to content

Instantly share code, notes, and snippets.

@rssh
Last active April 25, 2024 10:15
Show Gist options
  • Save rssh/5375f30e4dbd9643bf88bac3444e7759 to your computer and use it in GitHub Desktop.
Save rssh/5375f30e4dbd9643bf88bac3444e7759 to your computer and use it in GitHub Desktop.

Let we have a simple system:

Interface MyAPI  {
    def iterator():  Iterator
}

With typical usage:

  myAPI.iterator().take(10)

This 'before change'.

Now, later we introduce CloseableIterator and change the interface with the next version:

Interface MyAPI {
    def iterator(): Iterator
    def closeableIteraror():  CloseableIterator
}

With typical usage:

  myAPI.iterator().take(10)

And

  val it = myAPI.closeableIterator() 
  try {
     it.take(10)
  } finally {
     it.close()
  }

Then, if we preserve the semantics of method wich returns Iterator — i.e., myAPI.iterator() returns an Iterator that should not be disposed of by the client. (To do this, in the iterator, we can do defensive copying or allocate CloseableIteratir with ReferenceQueue and poll for reference queue to cleanup in a separate thread—it does not matter.) If we have method takeFirst10(it: Iterator) then we can use it, not caring about: it's CloseableIterator or not, i.e. for closeable iterator we should call

val it = myAPI.closeableIterator()
Try {
   takeFirst10(it)
} finally {
   it.close()
}

but semantics of takeFirst10 is not changed. Maybe we have method takeFirst10AndClose(it: CloseableIterator). What we can say about Liskov Substitution Principle here: If the source code does not belong to iterator behavior (my Interpretation), then LSP is not violated. If we receive CloseableIterator, we know that it should be cleaned.

If the source code belongs to the iterator behavior (your interpretation), then if you change

Interface MyAPI {
    def iterator(): Iterator
    def closeableIteraror():  CloseableIterator
}

to

Interface MyAPI {
    def iterator(): CloseableIterator
    def closeableIteraror():  CloseableIterator
}

Then semantics will be changed, and you receive an LSP violation.

So, we have two interpretations:

  1. API, which we provide, is not behavior but like an environment. We keep LSP, preserving the semantics of old methods and assuming that we do not change an old API. (It's why I say that we can add CloseableIterator without violation of LSP)
  2. The API we provide is part of the behavior. If we change the base class to a subclass in the system's source code, then LSP is violated. (It's why you say that we can not add CloseableIterator without violation of LSP)

Note that here I tell nothing about design preferences. (It depends, and this is another big theme). But hope, that two interpretations of LSP are clear now.

@rssh
Copy link
Author

rssh commented Apr 25, 2024

It might be better to consider this as an example of an extension rather than a modification. If we consider adding 'close' as an extension, then CloseableIterator is a valid design; as a modification, not.

@DGolubets
Copy link

Sorry I should have specified what I was arguing about in the first place: the usefulness of such implementation.
I.e. this:

Why ? I can't uderstand you claim that This means you can't use any base Iterator method..

You got the answer in the OddNumbersApi example: you lose all the power of base methods and now you have to re-implement each of them instead.

Regarding LSP.
I think it doesn't hold either.
The quote from Wiki:

Liskov's notion of a behavioural subtype defines a notion of substitutability for objects; that is, if S is a subtype of T, then objects of type T in a program may be replaced with objects of type S without altering any of the desirable properties of that program (e.g. correctness).

Now how I read and apply this to your example: you should be able to keep your original interface and just return an instance of CloseableIterator without adding a second specialized method.

But CloseableIterator has additional requirement on the client - to close it. Which it won't be able to do because it works with Iterator interface.

So the behavior of the program changes from "non-leaking resources" to "leaking resources".

@rssh
Copy link
Author

rssh commented Apr 25, 2024

If we keep the original interface without another method, then LSP is clearly violated. It's obvious.

The discussion was: Does extending Iterator to CloseableIterator necessarily violate LSP? I have shown that we have a way of extending without violating LSP, with a note that we don't include the source code of the existing system (i.e., implementation of MyAPI) to the set of characteristics that should be invariant.
A second method is necessary for this (and also not written, MyAPI interfaces need to be final due to covariant return types or e should have a convention not use covariant return types for MyAPI.iterator method).

@DGolubets
Copy link

The discussion was: Does extending Iterator to CloseableIterator necessarily violate LSP? I have shown that we have a way of extending without violating LSP, with a note that we don't include the source code of the existing system (i.e., implementation of MyAPI) to the set of characteristics that should be invariant.

Liskov substitution principle is about substitution, i.e. using an instance of a subtype in place of the original type.
When you extend your API you don't substitute anything, so you don't test LSP.

@rssh
Copy link
Author

rssh commented Apr 25, 2024

It's the point of difference:
Interpretation 1: API. should be excluded. [Because API is how we define a system for the client. Client code does not contain API]
Interpretation 2: API should be included.

Or we can look on the scopt [1 = LSP in Client System], [ 2 = LSP in (Client System + API Implementation) ]

And the answer is 'yes' in the first case, and 'no' - in the second.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment