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'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