---
title: 'You Think This Is Good OOP… It’s Not'
source: 'https://youtube.com/watch?v=RqcEK7sWesQ'
video_id: 'RqcEK7sWesQ'
date: 2026-08-28
duration_sec: 1162
channel: 'ArjanCodes'
---

# You Think This Is Good OOP… It’s Not

> Source: [You Think This Is Good OOP… It’s Not](https://youtube.com/watch?v=RqcEK7sWesQ)

## Summary

This video by ArjanCodes critiques six common object-oriented programming (OOP) practices that developers often mistake for good design. It explains why each pattern causes problems, such as coupling, rigidity, and violations of principles like Liskov substitution, and provides concrete refactoring alternatives using composition, protocols, and plain functions.

### Key Points

- **Mistake 1: Inheriting for Implementation Details** [00:02] — A class inherits from both a database service and a logger to reuse methods, but this violates the is-a relationship. The fix is to use composition: pass dependencies via an initializer and store them as instance attributes.
- **Mistake 2: Turning Values into Types** [03:01] — Creating subclasses like GermanCheckout or ReliableGermanCheckout to change tax rates or retry counts leads to combinatorial explosion. Instead, model variations as data (e.g., a CheckoutConfig data class) and use factory functions.
- **Mistake 3: Modeling Independent Features with a Hierarchy** [06:24] — Adding retry and validation as subclasses (e.g., ValidatingRetryingProductCatalogSynchronizer) couples independent features. Better to use a SyncOptions class and a single sync function that branches on options.
- **Mistake 4: Creating a God Base Class** [08:41] — A base integration class with many methods (upload images, download inventory, etc.) forces stamp coupling. Fix by using small protocols (e.g., Authenticated, InventorySource) and passing only the required capability to functions.
- **Mistake 5: Subtypes That Violate Liskov Substitution** [12:36] — A ReadOnlyOrderQueue subclass that raises errors on write methods breaks the parent's contract. Solution: separate contracts into OrderReader and OrderWriter protocols and use distinct objects for reading and writing.
- **Mistake 6: Premature Abstraction** [15:23] — Creating a base importer class when customer and order importers share little meaning leads to loss of type information. Instead, write two plain functions that handle each type specifically.
- **What Good OOP Looks Like** [17:56] — Good OOP uses objects to protect invariants, combine state with behavior, and expose clear boundaries. In Python, prefer composition, protocols, and plain functions when appropriate.

### Conclusion

The video emphasizes that OOP is not about using inheritance everywhere; it's about designing for clarity and flexibility. By avoiding these six mistakes, developers can write more maintainable and testable code.

## Transcript

that inherits both from a database service and a logger because it wants service and a logger because it wants one useful method from each. This one and this one. Now that looks object-oriented inheritance. We used two
well-named base classes, but don't ever do this. And let me be honest, I actually did these things in the past as well. But today, I'll show you six things developers commonly mistake for good object-oriented programming, why
they cause problems, and what to do instead. If you want to go beyond individual rules like prefer composition and learn a systematic way to make better software design decisions, especially with AI
generating all the code nowadays, check out my brand new software design mastery program at karn.codes/mastery. This teaches you everything that you these YouTube videos. The link is in the video description. So like I said, we
have a daily sales report class here that inherits from both database server and a logger because it needs these methods from these objects. As you can see above here, I have the original classes. Now there's not much special in
them, but they do have the methods that this particular class needs. When I run this then, this is what we get as a result. The problem with this approach is that in inheritance typically, the relationship is called an
typically, the relationship is called an is-a relationship. And in this case, a daily sales report is not a database server, and it's also not a logger. So we have this hierarchy, but it doesn't actually represent the relationships
properly. If you have this kind of setup in your code, well the easiest way to change it is to actually switch to composition. And what you can do in order to achieve that is to add an initializer.
And then inside that initializer, you pass the things that the class needs. Like the database server and the logger.
you store those in the instance. relationship, but we simply call them directly like so.
need to do is pass the instances to the report. If we run this code again, you see it does exactly the same, but now we are
inheritance. So, this inheritance relationship is gone. It just depends on database servers and the logger. You can replace them if you need to write tests
because we explicitly patch things up in the main function right here. So, this is much easier to use. And this actually points to a simple rule. If you inherit because you need some implementation detail, you probably wanted composition
instead. Second mistake is to turn values into types. This is something I see quite often in code where, you know, we have a in this case a checkout class that's a standard checkout which has a particular kind of tax rate and a retry
count. And then it has some methods that use these values somehow. But then, as you can see, we have other classes like a German checkout that has a 19% tax rate. Or we have a reliable German checkout that has a higher retry count.
And by the way, I never had problems with German checkouts, but it's just an example, right? Now, these subclasses here, German checkout and reliable introduce different behavior. They just change a few values. They change the tax
rate and the retry count. So, it feels object-oriented because the configuration gets meaningful names. We know, oh, this is about Germany and this is a reliable version of that same checkout. But each
dimension that you add in this way, you create more and more combinations because what if we have a French checkout? And then you would have a reliable French checkout and you have all the combinations of the reliable and
standard checkout for each country in the world. It doesn't It doesn't work. Instead, what you want to do is to model these variations, countries, tax rates, etc. as data. So, for example, you could add a data class.
Let's also make that frozen. Uh let's call that checkout config. And this has things like the tax rate
let's call it just checkout because it's always standard, and this is going to always standard, and this is going to have the config config right here. And then instead of having all of these subclasses, I can
now create, let's say, a function create reliable create reliable German
with a checkout config and this then contains the tax rate And now we no longer have these things. And I mean, of course, you can create
multiple functions or factories that can create different objects for you, but we're not using inheritance for these. So, here what I'm doing is then simply calling the function like so. And I haven't run this example, so let's
And I haven't run this example, so let's run this and then we of course get some error as usual and that's of course because we need to access the config to get into the domain of the law of Demeter violation. So, this is probably
not something that you want to do. So, if you need access to this information, you can create a config in a separate function or add some methods or properties to check out that gives you access to the values. Essentially, when
and subclasses, it should represent different behavior or constraints or meaning, not every possible combination of settings. Third mistake is to model independent features with a hierarchy. If you take a look at this example,
which is about syncing to a catalog, we have a catalog API and we have a product catalog synchronizer that uses a catalog API and then syncs the API with a list
But then, there's actually multiple features that are needed and that's inheritance, which is that we need retry behavior. So, that's actually what's happening here with the for loop.
But we also have another class, which is a validating retrying product catalog synchronizer, which needs to retry but also needs to do validation. So, these are two independent features that need to be part of the object and these are
being added here by simply inheriting from the product catalog synchronizer happening here is that the second feature, validation, is just bolted onto the first feature subclass. And then, if you need some other option, well, you're
etc. And now, instead of using inheritance to add all these independent features, we can also do it differently, where actually we create a sync options class that contains the configuration for this particular operation. So, there
is validating, yes or no, and there is the maximum attempts that we should take. And then, this returns a sync result with a report of what actually happened. On top of that, we don't need all of these different classes at all.
We can simply switch to a function to sync the catalog that gets the API, the products, and the options used for syncing. And then, depending on the values of those options, it's going to do different things. The rule here is if
you have features that can be switched or combined independently, don't encode every combination as a subclass. But also here, we're introducing another trade-off, namely in the form of coupling, which is that an option now
the catalog, whether there should be validation or not. So, you can also opt to pull validation totally out of syncing the catalog because perhaps it doesn't need to be validated here. It can simply get a list of validated
products instead. That's a design exercise you could use on this code yourself to further improve it. Mistake number four is creating a base class that forces one shape on everything. Here's an example of what I mean. So, we
have a supplier session data class, and we have an inventory item. And then, it there is some store that the code works with, and that's modeled in this particular base class. So, this is the base integration, which has all of these
different things, like uploading product images, downloading the inventory, downloading the reserved SQ, subscribe to order web hooks, request a return label, all sorts of different things. And then, we have a warehouse supplier
that provides implementations for the things that are relevant for a warehouse. So, there's authentication, there's downloading inventory, and a few other methods as well, as well as a couple of helpers.
And then, we have connect to supplier that gets the integration, which gets a base store integration. So, it works with not just warehouses, but with all sorts of other types of stores, and it gives a supplier session, and
then finally we have sync inventory that gets that same integration and the session, and then returns a list of inventory items. And this is what we get when I run this code. Now, the problem is that the inventory workflow that we
have right here, it needs authentication plus a couple of inventory queries like download the inventory or download reserved SKUs, but the base class also has a bunch of other things like uploading products images, for example.
So, there's a lot of things that are being added here and are not being used at all in the rest of the code, and this is what we call stamp coupling. So, we're providing this entire thing here to this particular function, while it
actually only needs something that has an authenticate function, and that's it. And that's dangerous because it means you can accidentally access methods here that you're not supposed to, and then it introduces
cohesion problems and coupling problems, and things in the future are going to that happens. Even if you look at warehouse supplier, well, essentially warehouse supplier, well, essentially this only supports a few in inventory
things. So, all of the other methods are basically not implemented. Instead of creating this huge god base class, you can also simply split the capabilities by introducing abstractions for them, and then pass those to the workflows
that need them. For example, here is another approach where we still have these classes because they're being used, but then we have an authenticated method. And we have an inventory source protocol
that has download inventory and download reserved SKUs. The warehouse supplier in this case has no inheritance relationship. It just has the methods that it needs, which is authentication and the inventory methods.
Connecting to supplier gets an authenticated instance. It doesn't get a huge god class thing. It just gets an object that has an authenticate method and that's all it knows. Sync inventory gets a source which is the inventory
source and like I showed you that is another protocol that contains just these two methods. So that sync inventory also gets the object with only the methods that it actually uses. But the interesting thing is in the main
function we still just create a warehouse supplier and pass it to these two functions without them knowing that they're getting a bigger object. This version before but it doesn't have a huge god class. It doesn't have methods
that are going to raise not implement errors etc. etc. So the functions in this case is simply request exactly the capability that they use and this is what we also call interface segregation.
In a pythonic form that is we have small structural protocol interfaces rather than some complicated tree of classes and sub classes. Fifth mistake is to create sub types that can't behave like their parents.
order class which for now just has an ID. But then I have an order queue class that allows me to add orders to a queue and retrieve those orders. But then
in some cases maybe the developer realized they need a read only queue nobody actually writes to the queue. So they make a sub class of order queue called read only queue and then when
this method is called actually you get a runtime error instead of this particular behavior, right? And the way that it's used here is that we have printing the order history needs a read only order queue and
another method at that has an order needs a regular order queue. And then this is how we use it. So we have the order history, we have the order queue and then we call these functions. And when I run this then this is what we
get as a result. But the problem here is that read-only order queue is still accepted anywhere where an order queue is accepted. So, for example, in add expedited order, I can actually pass in the order history.
order queue is a subclass of order queue. So, that works totally fine. But when I run this, of course, now we're getting a runtime error that the the queue is read-only. What you're seeing here is a practical example of
validating the so-called Liskov substitution principle, which means that if you have a subtype, it shouldn't contradict the guarantees of the parent. In this case, the parent says, "You can add things to a queue." But the subtype
actually overrides that behavior and changes it. And that's why we have this problem right here. How do you solve that? Well, very simple, by separating the contract. So, here's another example where we have two protocols, an order
reader and an order writer. And then I have my order queue and I have my order history, which are, in this case, two different objects. And they behave different, they're different things. And then, print order history needs a order
reader cuz it needs to access orders and add expedited order needs an order writer. Now, the contracts are clearly separated and we have separate objects for the history and the queue. That also means if an outside pass the order
history as an argument to add expedited order, you see, in the IDE, we already allowed. And by the way, if you enjoy practical design videos like this one, give this video a like, subscribe to the channel,
hype the video, even though I'm not 100% sure what that does, but it's free and it probably helps me out a lot, so thank you so much. And then the final mistake, abstracting something before you understand the similarity. In other
words, premature abstraction. Here's an example. We have a customer and we have an imported order. And then I have some base importer class that imports And it does that by loading something and then applying some transformation,
checking that the raw record is valid, and it has couple of abstract methods. Then I have a [clears throat] customer CSV importer, which loads customers from CSV file. Well, kind of not really, but in a fake or simplified
way. And then it implements these methods. And we have an orders importer that also has load method and it has different validation and transformation different validation and transformation methods. Now, when I run this,
look at this particular abstraction, you see that it returns see that it returns objects, kind of meaningless things. So, basically, the useful types that we have, like customers, etc., they're
basically being lost. And that's not desirable here because one of these importers create customers and another one creates imported orders, which are two completely unrelated types. So, also, there's like loads of
differences. The way of loading these things via CSV or an API is going to be different. Validation is different, transformation is different. So, everything is different. So, that's not enough shared meaning to
justify a base class to abstract from it. Instead, don't use an abstraction here, but simply write two functions that do the specific thing for the specific type. It's okay, you don't need abstractions everywhere. For example,
here I have import customers from a CSV file, which does exactly that. And here we have import paid orders that come from an API. Well, both get list of records or rows, but overall, actually, the behavior of these two things is
quite different. So, it's okay if these are two different functions. In fact, what we have now is yes, there is a small amount of duplication, but it's also very visible and local. But, if you have a wrong
abstraction like this base importer class, it doesn't really add much because it just uses these very generic objects, and it doesn't really help you write clearer code because it's kind of unclear whether you're getting an object
like a customer or an imported order or something completely different. So, don't abstract something because the code looks similar. Abstract when it has the same meaning and it's likely going to change for the same reason. So, what
does good object-oriented programming then actually look like? Now, of course, none of these mistakes mean that classes, inheritance, abstract base classes are bad or something. I mean, people have been using object-oriented
programming for a very long time, but if you're using it right, then you're using your objects mainly to protect invariants, to combine state with meaningful behavior on that state, and expose clear
boundaries. In particular, in Python, classes shouldn't be a default solution for everything. You can use composition if things need to vary independently. You can use protocols to define interfaces. Use plain functions if
there's no meaningful state or identity. And honestly, while I was writing the script for the video, most examples came from my own experience where I did something like this in the past, and I was thinking like, "Hey,
why doesn't this work? What's the problem here? What mistake did I make?" So, I tried to translate them into practical examples that hopefully you can apply to your own code as well. But, I'd like to hear what you think. Have
you done some of these things in the past? It's okay. Are there other things you see people do with object-oriented programming that you think should be mentioned in a list like this as well, let me know in the comments.
YouTube thinks you'll enjoy this video next. Thanks for watching, and see you next. Thanks for watching, and see you next time.
