Abstraction Can Make Your Code Worse

628,470
0
Published 2022-11-20

All Comments (21)
  • The one design principle every programmer learns is "Don't Repeat Yourself", and it's not very helpful on its own. I wouldn't broadly say that abstraction increases coupling, but you can't make design decisions simply by looking for repeated code. Your abstractions should represent real concepts in the system that allow you to speak about them using natural language. Done properly, this actually decreases coupling, in a way that not only makes it easy to add new behavior, but also makes it easy to isolate each component for testing.
  • @sfulibarri
    To me this is the most significant difference between new programmers and very experienced ones. Being able to identify and choose the most valuable abstractions in a given context is what drives business value from raw development. Its troublesome because the emphasis on OOP in programming education leads many newer devs to implicitly value abstraction above all while not understanding the complexity they may be incurring. Choosing the right abstractions takes a lot of experience and must be informed by context. Even an apparently 'correct' abstraction may not be the right choice when considering the business context, general experience of the team, and even delivery deadlines; software engineering doesn't happen in a vacuum.
  • In my experience. The fun thing about programming, is that it does not matter how much you are aware of. Or how many suggestions you have gotten. You will still end up making regretable mistakes reguardless. Whether this is poorly abstracted elements or just flat out wrong architechture, it always happens. I struggle more with accepting that my code is good enough and actually finish the project when I know I could improve it. Rather than the coupling issues themselves.
  • @nikogj9495
    This video made me recall a book from Sandi Metz "Practical Object Oriented Design in Ruby" where she wrote: "Code duplication is far cheaper (in terms of technical debt) than a wrong abstraction". Even if the book has been published years ago, I still find this sentence particulary true and relevant today. (I advice any curious programmer about OO to have a look at the book, even if you don't code in Ruby, that's my case, or even videos of her).
  • @TinusBruins
    In my experience abstraction isn't about removing repetition, though it does work for it. I think its most important job is to remove dependances. As is in your example with the IntervalSaver. The actual saving might happen in a different library. And you don't want the main code to be dependent on all the possibilities of saving.
  • @ShilohFox
    In “bad” abstraction like you had shown with the Save classes, coupling can become a problem. The purpose of abstraction is to remove coupling where there could be quicker, easier ways to process and store data as well as provide ways to represent concepts in code. The byproduct of abstraction is removing repetition in data structures. In a case where you’re holding multiple blobs of data, and they all have similar (but not the same) shapes, and the way you interact with them is all the same, abstraction can actually remove coupling by making the handling of that data independent of what the data under the hood actually is. Interfaces are a good example of abstraction that does not care for what kind of data you are handling. In Java, if you want to get a list of objects, then using the abstracted “List” as a return type versus specifying an ArrayList or LinkedList makes the handling implementation agnostic to the underlying implementation, and making changes under the hood does not affect how the code is written in the handling of that data.
  • @64BitLamp
    In my opinion abstraction isn't necessarily about code reusability. I would argue it's really about shared generics. The big trope that most programmers never learn is abstract to interface coupling. What I mean is defining an interface for an abstraction, and using interface methods within implemented abstract classes. By defining an interface for abstract classes, you can call upon non existent implementations (that the implementor later has to make). Think of it more as "hot swapping black box functionally".
  • @freedom13245
    3:53 is a good example of polymorphism, a good abstraction that’s worth it would be a Save interface which both classes implement with their different way of saving, and you’d call the save method on the interface so you don’t have to add and “if” statement check the type and using a different implementation for each type
  • @AlanD20
    I want to say, I agree small codes don't need to be abstracted to interfaces and many inherited classes, such as a todo app don't need 10 interfaces with 20 classes just to achieve the goal. Most people do abstractions because it makes the code maintainable and easier to read with consistent naming conventions. Let's say in your example that you want to add saving as JSON. When someone decides to add this class. First off, you think, what if we need to add another file mode? such as CSV? pdf? HTML? or any other file modes. When you work in a team of people, and each of those implementations has to be done by other people, You are going to use an interface or a parent class, that has all the functionality, and the one who implements the new mode knows what the new file mode must implement and where to implement their code without breaking any changes. Everyone on the team had agreed that the interface has to have a function that returns the file name, another function to save the file, and so on... In summary, it's more about maintainability and working on one large codebase with more than 3 people. Because it will be hard to make communication as someone might use saveAsJSON, someone else might use save_as_json, and someone else goes for saveJsonFile. There will be no consistency and an interface gives the developer a contract to follow those rules without breaking any other codes with no communication, other developers understand how the new file mode must have a function name called save which returns in different file modes. You have to know when and where not to abstract, everything doesn't need abstraction but on a large scale, you have to abstract. Otherwise, you will make the entire codebase a place where no one likes to debug nor to add new functionality because there are so many duplications and you don't even know if those are really duplicate or if they may do a different task but with the exact same process that looks duplicated.
  • I think the major problem is that it seems the code wrote on the video favors inheritance over composition. Had the FileSaver class follow a bridge or strategy pattern, with the expected interface of the implementation component being something like "Save(fileName)", It would have worked better. Then, SaveToJSON would be one possible implementation, and the user would use FileSaver directly with either a default implementation or one of his choosing from the library we created. With dependency injection, he could create his own implementation to save files in any format he desires
  • @tonnoz
    I usually try to avoid inheritance as much as possible and use composition instead. By embedding functionality you can still retain the meaning of "shared functionality" while separating the differences in separate classes. Of course only if it's worth it. The code you shown are a good example (more than two choices etc.).
  • @vikingthedude
    I love this style of video where you walk us through various ways the code could be written and weigh their pros and cons. So many teachers either show only contrived examples, or just one big "correct" implementation. But your exploratory way of teaching is so helpful. Please keep making these in this style!
  • @erlend1587
    Good video, but there are other ways of abstraction then inheritance, you could use composition, and create a component based system. You also don't need to make classes for everything, you could just make a function to handle some specific parsing. Not thinking too much ahead and being pragmatic about the code you write (keep it simple), often makes the code easier to reason about and later refactor when it changes.
  • I think you're talking about inheritance more than abstraction. Interfaces or abstract classes are a way to hide implementation details and actually decrease coupling. Inheritance / extending classes is what increases coupling.
  • The base class for the filename does not create coupling to a filename, that was already there, if you didn't want the saver to save to a file its not the base class' fault. If anything, if when trying to abstract you think it creates a problem it means you already had a problem before. in case anyone didn't get it the problem is that XML has little to do with the file, in other words a saver needs a saving strategy like file or db and that strategy needs an optional format. say `new Saver(new DBSavingStrategy(db_handle))` or `new Saver(new FileSavingStrategy(file_handle, file_format))`.
  • @ktvx.94
    There're times when coupling is good. Sometimes you want to update something that changes all children classes simultaneously, instead of keeping track of which ones need the new code and manually changing it every time. If you forget, different entities that should have the same behavior start behaving differently. That said, I fully agree with everything else.
  • @gr0nis
    I would suggest using composition over inheritance and also use more functional style programming. It’s much easier to have abstraction that way without locking yourself too much into coupling imo.
  • @klikkolee
    I think separating the ideas of abstraction and repetition helps a lot to choose good abstractions. Abstractions are supposed to guide your field of view to whatever is relevant to a given part of code. If one part of your code needs a means of saving a game, but doesn't need to know how it's done, then an abstraction for saving a game is beneficial. From the perspective of what code changes guarantee the need for other changes as a matter of being able to compile the code, an abstraction will always create coupling. But if the abstraction was a good choice, then from the perspective of what code changes are required for the code to continue being sensible, that coupling already existed. The abstraction just enforces it. A good choice of abstraction can also reduce coupling of both kinds from other parts of the code -- a Saver interface means that code that uses Savers can have just one variable and from its perspective one code path regardless of how many types of Saver exist. Lastly, a good choice of abstraction pushes coupling into islands of relevance where you can readily see what depends on what and why. Code that just needs a Saver and doesn't care which Saver still needs to get a Saver from somewhere, and that somewhere needs to contend with the different reasons one might be chosen over another and needs to contend with the needs of creating individual savers. That is coupling, but it's coupling that would've been diffused through the code base without the abstraction. I am struggling to make sense of your Saver example. I just cannot think of a situation where I would have multiple methods of saving something but would not have a part of the code that shouldn't care which method is employed. That is a situation that is impractical to accommodation without an abstraction. This situation also means the example of wanting to change the interface of just one does not make sense. If the code using savers does not need these additional features, then adding those features to any savers is unused code. If the code using savers does need these additional features, it probably needs them regardless of which kind of saver it is told to use, meaning that it needs to be added to all of the savers -- regardless of whether there is an actual in-language interface that enforces the change. I don't choose what I save and how I save based on the serialization format -- I choose the serialization format based on what I save and how I save. If a given serialization format or some other aspect of a saver isn't compatible with the new needs of the rest of the code, that saver is getting the boot.
  • One thing I find myself having to explain to people over and over is that lexical repetition is not the kind of repetition to avoid. Avoid distributing a single implementation detail across multiple locations in the codebase, because they will have to be manually kept in sync with each other. That's the bad kind of repetition. DRY only applies to code that is already coupled in some way.
  • @codingbloke
    Nice one! Completely agree with this! There is a difference between "repeating the same logic" and "by coincidence having the same logic currently". Spotting the difference can be hard but if you eliminate the apparent repetition of the latter you can find the code becoming less malleable . It can perversely be a source of bugs. Class A shares some logic with Class B. Class A doesn't behave correctly because there is a "bug" in the shared logic, however this "bug" is only truly a bug from the point of view of Class A, for class B it is perfectly correct. So you fix the bug, now you have broken Class B and its hard to know you have done that unless you check also Class B (and C, D...). Even with strong automated testing that newly introduced bug in Class B may go unnoticed. Also when shared logic is no longer compatible with all consumers there is a tendency for ugly `bool` values to appear in parameter lists and the code becomes harder to understand and even more bug prone.