4

As an example, in PHP you can run

gettype($myVariable);

to obtain the type of a variable $myVariable.

Is such functionality antithetical to OOP principles?

asked Jun 16, 2020 at 14:09
2
  • 3
    yes but there is also the expression problem eli.thegreenplace.net/2016/… Commented Jun 16, 2020 at 15:12
  • 1
    Note that making your programs completely pure OOP may be not ideal itself. Often a mixture of paradigms is better, as some problems are easier solved with other approaches. So even if type testing is bad OOP, it could be good in a particular place. Commented Jun 22, 2020 at 8:53

4 Answers 4

6

It depends, but often it is, especially when the type is then used in a type comparison using a chain of if-statements or a switch or select. It means that we're not using polymorphism and we're probably violating the Open-Closed Principle (OCP) and the Single Responsibility Principle (SRP).

The effect of this is that when you add a new type next to the existing type, you also have to go to all places in the code where the type check occurs and change those as well. It violates the SRP because you are changing more than one place in the code. And it violates the OCP because you have to change existing code for adding a new feature rather than simply adding new code.

That said, there are situations where using this can make sense, for example, when you have naturally parallel type hierarchies for design patterns like strategy. But in that case, it's better to still avoid a direct type check and instead use the type as a key to a map, where the behavior that differs between types is the value, and a mechanism that populates the map automatically so that you still get the maintainability benefits from the OCP and the SRP. In Java, such a mechanism is provided by java.util.ServiceLoader. I'm sure that a similar mechanism exists in PHP as well.

Last but not least, switching the paradigm away from OOP to some other paradigm will not invalidate the SRP and OCP, and will not remove the maintainability question at hand.

answered Jun 16, 2020 at 15:08
3
  • 1
    I'm not really clear how a map using the type as keys is different from a switch or chain of ifs, and when it would be necessary to use instead of polymorphism. The "mechanism to populate the map automatically" seems to be the important part, but you don't really explain how that would work - are you talking about some kind of registry that types can be added to? Commented Jun 22, 2020 at 9:51
  • A map with keys is different from a switch or chain of ifs because it is more maintainable and less redundant. The switch or chain of ifs may be required in more than one place of the code. The map would simply be used, the initialization of the map would be in a central place. A possible mechanism to populate the map is described in my answer, it is java.util.ServiceLoader. Commented Jul 24, 2020 at 13:50
  • OK, so you've edited in a class name, but that means very little to anyone who's never used Java, and it still feels like the emphasis of the answer is on the wrong part. Commented Jul 24, 2020 at 15:34
4

Yes...

I firmly stand on the side of disliking upcasting (i.e. from a base type to a derived type) as it is predominantly a code smell and sign of polymorphism abuse.

The most commonly used example of a LSP violation shows you exactly why relying on upcasting upcasting is a bad idea.

void MakeDuckSwim(IDuck duck)
{
 if (duck is ElectricDuck)
 ((ElectricDuck)duck).TurnOn();
 duck.Swim();
}

Others have explained the issue better than I could, so I won't delve into it here.

This is also why I'm not a big fan of e.g. the new C#9 switch examples that in their announcement examples explicitly show code that figures out which derived type a variable of a base type can be converted to1.

public static decimal CalculateToll(object vehicle) =>
 vehicle switch
 {
 DeliveryTruck t when t.GrossWeightClass > 5000 => 10.00m + 5.00m,
 DeliveryTruck t when t.GrossWeightClass < 3000 => 10.00m - 2.00m,
 DeliveryTruck _ => 10.00m,
 _ => throw new ArgumentException("Not a known vehicle type", nameof(vehicle))
 };

Not that there aren't possible fringe cases where this could be useful, but using it as the guiding example on how to use a language feature is going to telegraph to developers that this sort of logic is the norm, which it really shouldn't be.

As a code reviewer, I will always flag upcasting logic as an issue unless there is a clear reason why upcasting is warranted (or the lesser of the evils).


But ...

There are fringe cases where this is valid. I can think of three main examples, which I'll list. Other cases may exist but they don't come to mind right now.

1

Exceptions are, funnily enough, an exception to my dismissal of upcasting logic. try catch logic specifically exists to figure out the type of an exception to then handle that exception appropriately according to its type.
But this is a matter of logical consequence. You could avoid needing to upcast exceptions, but then you'd be forced to define different ways for different exception types to bubble up, which mostly defeats the purpose of having a streamlined throwing and catching process that ensure the thrower doesn't need to worry about the catcher.

2

A second exception here are cases where the derived type is only going to be used as e.g. a string value (for its name). Here, the subtype isn't really used in a "typed" sense, it's a simple scalar value and it doesn't affect the logic of the application. For example:

public interface INamedObject { string Name { get; set; } }
public void HandleNamedObject(INamedObject o)
{
 string typeName = o.GetType().Name;
 _log.Debug($"Handling named object of type {typename} with name {o.Name}");
 // business logic
}

That's completely harmless and the application's logic doesn't rely on this derived type in any way.

3

When serializing an object that you intend to deserialize in the future, you're going to have to keep track of the exact type of an object so that you can deserialize it back to that type. If your serialization logic is written to handle a base type, you're going to have to figure out the derived type to ensure you can deserialize the object in the future.

public IFoo Example(IFoo myFoo)
{
 Type exactType = myFoo.GetType();
 string serialized = Serialize(myFoo);
 IFoo deserialized = Deserialize(serialized, exactType);
}

This is a bit of an oversimplification, but this is how serialized data stores usually work when a particular store isn't inherently bound to a concrete type (with no further subtypes).

As a real world example, we store events in a single table, but since each event is a type of its own, we store the event using two columns: the type (name string) and the event data (json string). This allows us to cast these events back to their specific type when we retrieve the data.

Note: Some serialization libraries can be configured to automatically serialize the object's type so that you don't need to manually track the type. Newtonsoft is one such library.


1 As @IMSoP points out in the comments, the C# 9 switch example may have been a feature developed for FP rather than OOP, which would nullify my objections to the feature itself.

answered Jun 22, 2020 at 8:31
12
  • Regarding pattern-matching switch, I believe it has come from an FP, rather than OOP, background, which explains why it feels contrary to polymorphism. It makes particular sense when the set of types is a closed list (e.g. algebraic data types, sealed classes) since then you can prove the switch is exhaustive without adding a default branch. Commented Jun 22, 2020 at 9:58
  • @IMSoP: Fair enough. I'm not FP-experienced enough to make that judgment, I'll amend my answer with your comment. Commented Jun 22, 2020 at 10:13
  • @Flater moment^^, i think you mix here two things into one pot, that not belongs together. Yeah the example with the duck is a classical break with the idea from baraara liskov. the invariants are changed from the subclass. And thats the violation. To make these invariants right again by caling the right methods in the subclass and before by checking the type if it is the subclass give us the right result, but with a much bad design^^. So what i want to say: To fix something (the really bad designed subclass) with casting is bad, but is that tool at all bad? Commented Jan 26, 2022 at 15:26
  • 1
    @MehdiCharife: It hinges on your definition of equality, and the point of making that method overridable is specifically to allow you to redefine it - I can't answer this definitively. What I would say is this: is you consider myBaseObject1 and myBaseObject2 (different instances of the same class) to be equal because all of their property values match, i.e. you are defining equality to look at values, not instances; then in my opinion myBaseObject1 and myDerivedObject1 should also be equal (assuming that all of their MyBase properties contain the same values as each other). Commented Aug 28, 2023 at 0:22
  • 1
    @MehdiCharife: ... The reason I say this because I think that not doing the latter would then be a violation of LSP. However, equality is an interesting case in point because it's so difficult to establish precisely how and why you are defining your equality type a certain way, so there is leeway in this answer. I'd urge you to consider LSP but I'm open to justifiable reasoning in either direction. Commented Aug 28, 2023 at 0:24
2

Yes it is.

Sometimes you're stuck between a rock and a hard place, though: if the objects that your code needs to deal with have differences in behavior which would break your code if you don't handle them specially. For example, if a library that you're using has a known bug that won't be fixed, and it causes your program to crash when you send bar to an object of type Foo, you might be forced to check the type even though that's unclean.

Of course, your own code should never force its users to perform such workarounds.

answered Jun 16, 2020 at 15:12
-2

Not really. For example, Objective-C and Swift let you put anything into an array: var a = [Any]; a.add(1); a.add("hello"); a.add(someObject())

In Swift you don’t check for the type of an item, but do an optional cast:

If let i = a[0] as? Int {
} else if let s = a[0] as? String {
} else if let o = a[0] as? SomeClass {
}

It’s not object oriented but then not everything has to be object oriented. Importantly, everything is typechecked. i is 100% an Int, s is 100% a String, o is 100% SomeClass. You use whatever is best.

answered Jun 16, 2020 at 15:50
1
  • 1
    Polymorphism is a staple of OO, which means that your example inherently applies to every OO language (i.e. you can have a collection with base-typed elements even if the elements themselves are of a derived type). But, as as Ian Malcolm said... Your answer is showing that it can be done (which OP's question also shows), it doesn't explain why it should(n't) be done, which is the core question here. Commented Jun 22, 2020 at 8:37

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

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

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.