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?
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?
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.
java.util.ServiceLoader.
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).
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.
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).
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.
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.