I answered a question regaurding an ImmutableMap
. I suggested using the Proxy pattern.
The problem with this is that Map
contains a put
method, which would throw an UnsupportedOperationException
. Replacing other instances of Map
with ImmutableMap
would break the Liskov Subsitution principle. Not only that, the need to declare put
and putAll
[violates the interface segregation principle]
Technically, there is no way to replace a Map
instance with ImmutableMap
, since Map
is simply an interface. So my question is:
Would creating an ImmutableMap
using the Map
interface be considered breaking the LSP, since Map
contains a put
and putAll
method? Would not implementing Map
be considered an "Alternative Classes with Different Interfaces" code smell? How would one create an ImmutableMap
that abides by the LSP yet doesn't contain code smells?
In my view, an ImmutableMap
should implement Map
. It would be a bad idea not to implement Map
as there are many methods that accept a Map
as an argument and only use it in a read-only sense. I don't believe this does violate the Liskov Subsitution principle because the contract for Map
makes it clear that put
is an optional operation.
It is not ideal that classes implementing Map
have to implement put
, but the alternative would have been to have a complex hierarchy of interfaces each only including a subset of the possible optional methods. If there are n
optional methods, there would have to be 2^n
interfaces to cover all the combinations. I don't know the value of n
, but there are some non-obvious optional operations, such as whether or not the Iterator
returned by map.entrySet().iterator()
supports the setValue
operation. If you combined this hierarchy with the hierarchy of interfaces and abstract classes that actually exists already (including AbstractMap
, SortedMap
, NavigableMap
, ConcurrentMap
, ConcurrentNavigableMap
...) you would have a total mess.
So there is no perfect answer to this, but in my view the best solution is to make ImmutableMap
implement Map
and ensure that every method you write with a Map
as an argument clearly documents any properties the Map
must have and the exceptions thrown if the wrong type of Map
is passed.