Object-oriented approaches are legitimate. Non-OOP approaches are legitimate. But mixing them like this does not seem wise.
You have noticed that all CategoryB objects also have all methods of CategoryA objects. But does this mean that that CategoryB is a subclass of CategoryA? If it is a subclass, then wherever your code expects a CategoryA instance you can provide a CategoryB object instead, and everything should work. This is also known as the “Liskov Substitution Principle” (LSP).
Yet, you find that this doesn't hold – your CategoryB objects need to be used differently. They require different preprocessing. To me, this indicates that you should not use inheritance here.
Sometimes, things look related but aren't. Principles like Don't Repeat Yourself (DRY) are rough guidelines. But often, coupling things that aren't related is worse than a bit of repetition.
Of course, it can reasonably happen that two classes happen to share the same implementation, but that they shouldn't be subclasses of each other in the LSP sense. Then, instead of using inheritance class CategoryB(CategoryA)
, it could be better to just call a common function:
class CategoryA:
def some_function(self, x):
return _some_shared_function(x, self.a)
class CategoryB:
def some_function(self, x):
return _some_shared_function(x, self.b)
def _some_shared_function(x, y):
...
So far, I have assumed that CategoryA and CategoryB should not be related. But what if your subclassing is actually legitimate? Then the object in question should provide all type-specific operations that are necessary. You should tell the object what to do, and the object will know how to do that. You shouldn't ask the object for its specific type. Here, you might need a method for preprocessing:
class CategoryA:
def preprocess(self):
do_some_kind_of_preprocessing()
class CategoryB(CategoryA):
def preprocess(self):
do_some_other_kind_of_preprocessing()
Now, instead of having to remember a specific order of instanceof() tests, the inheritance resolves this.
The drawback of this approach is that your objects will accumulate a lot of behaviour, potentially leading to a “god object” with a large, ill-defined public interface. If this becomes unmanageable, the Visitor Pattern may provide an alternative. The Visitor allows you to define type-dependent operations externally, and still let the object select its correct implementation. Here, it would look like this:
class CategoryA:
def accept_visitor(self, visitor, *args, **kwargs):
return visitor.visit_category_a(self, *args, **kwargs)
class CategoryB:
def accept_visitor(self, visitor, *args, **kwargs):
return visitor.visit_category_b(self, *args, **kwargs)
class PreprocessingVisitor:
def visit_category_a(visitor, self):
do_some_kind_of_preprocessing()
def visit_category_b(visitor, self):
do_some_other_kind_of_preprocessing()
some_algorithm_instance.accept_visitor(PreprocessingVisitor())
elif
? In real life it will probably be a lot more obvious than in your simplified nomenclature that B algorithms are the more specific ones, so it won't be a big burden to remember to check for B first.