0

As a design priciple I was taught:

Programe para una interfaz, no para una implementación. Es decir, no se deben declarar las variables con el tipo de los herederos sino con el tipo de los supertipos.

Translated:

Code for an interface, not for an implementation. That is to say, variables should not be declared with the type of the subtypes but with the type of the supertypes.

But that easily runs into problems of type slicing when I actually have to obtain the type of the subclass to perform specific operations.

How is the problem of type erasure normally solved in reality?

9
  • 4
    Why aren't those specific operations (of the subtype) defined as part of the interface? Then no downcasting needed. However, if you've got several classes implementing the same interface but all having different "specific operations", you may be defining your interfaces - or your implementing classes - incorrectly.
    – mmathis
    Sep 30, 2022 at 2:05
  • "Why aren't those specific operations (of the subtype) defined as part of the interface? Then no downcasting needed." Because defining specific operations as part of the interface requires changing the interface when new requirements are added and is against the "design for change" and "open to extension closed to modification" principles. Sep 30, 2022 at 12:51
  • You mention “type slicing”. Could you clarify what kind of language you are using – a language with value types like C++, or a language with reference types like Java or C#? Slicing or erasure is generally not a problem in languages that use reference types by default.
    – amon
    Oct 1, 2022 at 17:07
  • @amon The problem presents itself as object slicing in C++ and (in collections) as type erasure in Java, both of which I am interested in. Oct 1, 2022 at 18:05
  • There is a lot of noise in the answers given. All this is really is a misrepresentation of the dependency inversion principle. Oct 2, 2022 at 5:41

4 Answers 4

2

Code for an interface, not for an implementation. That is to say, variables should not be declared with the type of the subtypes but with the type of the supertypes.

But that easily runs into problems of type slicing when I actually have to obtain the type of the subclass to perform specific operations.

How is the problem of type erasure normally solved in reality?

(Understood "type erasure" in this context, but actually it is used in Java for generic type parameters as used in collections.)

Apparently the subject is having a collection, say a list of an interface.

The principle interface over class has several well founded reasons. For the reader still not familiar with those, read about them, they improve expressive coding enormously.

1 All items the same class

The case where all list items are actually of the same class means that the interface is not complete. Again in java you can have an interface extending more than one interface. Or you can look for a more specific interface. If you have an interface Set implemented as class TreeSet but expect that the items are ordered like in TreeSet pick the interface SortedSet. The operational requirements must be satisfied by the typing. This might mean using a class instead. For a class bound to a local application there may very well be no reason to artificially split it in interface and implementing class = a wrong legalistic approach, costing time.

2 Items with very different classes

The case where all list items are actually of the different case classes which later need to be handled per case class, is painful. One may assume that the use of the specific class may not be captured in a more extended interface. That would be easy. So actually you threw together things that still needed to be kept separate. Keep the items in separate lists (too). As already mentioned for the case of lists of same class implementations they may expose the class not the interface. Keep the cutlery separate.

3 Different classes, but you must live with them

The last case concerns having to live with mixture of things, needed to be treated differently. An example (in java):

List<Animal> animals = ...
List<Swimming> swimmers = animals
    .stream()
    .filter(a -> a instanceof Swimming)
    .map(Swimming.class::cast)
    .collect(Collectors.toList())
;
List<Flying> flyers = animals
    .stream()
    .filter(a -> a instanceof Flying)
    .map(Flying.class::cast)
    .collect(Collectors.toList())
;

This case not necessarily is based on inheritance (as here with instanceof). You might have:

interface/class Animal {
    Optional<Swimming> asSwimming();
    <T> Optional<T> as(Class<T> type);
    ...

Following the single responsibility principle: then keep code for every implementation class in a separate source. There is not much more you can do about that. It happens.

5
  • The problem is not having a collection. From the design perspective, an example: Lets say Photo has submodule RGBPhoto, BWPhoto, and both have specific methods for dealing with specific transformations, then to a process(i Photo) (in a module Processing) a Photo is passed, it checks which submodule it is and runs the correct processing. When I convert that into implementation, and submodules are classes, type erasure breaks things AFAIK. Sep 30, 2022 at 12:00
  • Instead of Processor.process(Photo) thinkable would be the inverse-of-control Photo.onProcess(Processor) (= overridable in both classes). Of course you could do if (Photo instanceof BWPhoto bw) { ... bw.improveGrayScale(); The object's class is available at runtime.
    – Joop Eggen
    Sep 30, 2022 at 12:08
  • The problem with Photo.onProcess is that the module Photo has two secrets, the one about how a photo is (in memory), and what processes are done on each type of images. Im very sorry, the example I gave would have object slicing, not type erause. But type erasure could easily happen if the thing passed is a list of Photos. Sep 30, 2022 at 12:30
  • Fine, then there seems to be no definite conclusion. It was still nice for me to hold a small discourse. I hope there still comes some answer. The pragmatic solution is to "hack ahead" in different ways and see what can be done. With 3 you are right about asSwimming. However the method as suggests a lookup of a Map from Class (interface) to an instance. The interfaces could be registered from the outside without any import of those interfaces. stackoverflow.com/a/63407989/984823
    – Joop Eggen
    Sep 30, 2022 at 12:48
  • 1
    I think these solutions are useful at least as a resort to implement the type of modularity the teachers are proposing is a good idea, which at the time of asking thought was more generally used and so I didn't add many details relating to definitions and examples. Thanks! Sep 30, 2022 at 12:55
3

First, if you have to

obtain the type of the subclass to perform specific operations

it is a sign that your design may be wrong. This can have different causes:

  • there might be an operation missing in the interface supertype

  • or - more likely - the context requires to use the subtype, and the "design principle" (which is actually not more than a recommendation, a hint) should not be applied

  • or - even more likely to my experience - the whole usage of inheritance was wrong right from the start.

Of course, from time to time, you can run into situations where downcasting to a subtype is perfectly justified.

But that easily runs into problems of type slicing

This becomes only a problem when you start to copy the content of a supertype variable to another one, instead of passing references. In languages like Java, C#, Python or Perl, objects are reference types "by default", and you have to write extra code if you really want to copy an object of a supertype to another one (and so getting the risk of slicing a subtype). For example, when you create a container List<MySupertype> in C# or Java, you can test each element in that list if it has a certain subtype and then downcast.

In C++ (and maybe other languages, I don't know), however, objects usually provide copy mechanics "by default", and passing objects "by value" is far more easier than in other languages. This is sometimes beneficial, but has the drawback to make it somewhat easier to slice types accidentally. For example, by filling a std::vector<MySupertype> with different subtype objects, their original type will be erased.

Of course, in C++ the issue can be avoided similar to the former languages by using pointers, references or smart pointers - but one has to do some more extra design work here, or simply write more code. For example std::vector<MySupertype *> or std::vector<std::shared_ptr<MySupertype>> will solve the issues, but for the former you have to care about memory management and ownership, and for the latter you have to do more typing and probably more effort to resolve strange compiler error messages when you got some of the complex C++ syntax rules wrong.

2

You're overapplying the advice you've been given. For your interpretation, it might help to extend the advice further:

Code for an interface, not for an implementation. That is to say, variables should not be declared with the type of the subtypes but with the type of the supertype that correlates to the logic at hand.

For example, let's say you have a Person class, and both Student and Teacher derive from it.

If you were to write some logic that applies to all Person instances, your code should use Person (i.e. the supertype of students and teachers alike). This part you already understand.

However, the part you've glossed over is that when you're writing some logic that applies to all students (not teachers), your could should use Student. Not Person, because the code you're writing doesn't apply to all valid instanced of a Person (only students, not teachers).

Remembering what I just said, let's re-explore the problem you pointed out:

But that easily runs into problems of type slicing when I actually have to obtain the type of the subclass to perform specific operations.

If you need to perform a specific operation for which you need to know the concrete type (e.g. Student), then you shouldn't have casted your object into a Person, because you're clearly writing student logic, not person logic.

In essence, the advice you've been given doesn't apply in the problematic case you're pointing out. You should never cast to a type that is too generalized. You should only cast upwards until you reach the limit, whereby casting upwards again would lose information that you need to keep.

1

The English translation of that advice looks quite inappropriate. They are confusing "interface" and "abstract base class" most likely. Any class can implement an interface. A class can implement multiple interfaces.

For example, to handle UI objects with colours, you might have an interface that returns whether an instance is capable of having a colour, whether the colour is changeable, and a method to return the current colour and one to change the colour. Lots of totally unrelated classes can implement this interface.

So in this situation, your variables don't have any class type at all. They have an interface type. They definitely cannot have a base class type, because there is no common base class to all the classes supporting the interface which also supports the interface.

PS. There is some confusion because C++ doesn't have interfaces. Java, Objective-C, Swift and others do. And in those languages, classes and interfaces are very much unrelated.

2
  • "The interface of a module can be defined as the set of services the module exports. Another definition is as all the interactions of the module with its environment " source given is Fundamentals of software engineering (2nd ed.) and Documenting Software Architectures: Views and Beyond respectively for each. Sep 30, 2022 at 12:37
  • Right. The adage is "code to abstractions, not to concretions" and this the D in SOLID. It says nothing about inheritance or subtypes versus super types. See softwareengineering.stackexchange.com/questions/401769/… Oct 2, 2022 at 5:37

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.