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.
There's an interesting constraint that you can place on yourself that makes DRY good: don't use functions to DRY up your code. It sounds extremely counter-intuitive, but it forces the coder to think about queuing things up and doing them all at once further down the flow. There's a good talk by Mike Acton that can help understand the value in doing this.
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.
I don't believe experience will make a programmer being capable of using abstraction. It takes abstract thinking and it seems like a lot of people struggle with this. Most programmers might never in their live be able to use abstract programming, no matter how long they try. Some will be able to use it with very little experience in programming. It might sound harsh, but we all know that people are different and abstract programming isn't for everyone.
@@ecm9251 Abstraction is one of the 3 basic mechanisms of any programming language. In most common languages once a programmer learns how to write functions or classes they are already using abstraction. Programmers may be better or worse at leveraging abstraction to create better software but the idea that some programmers are somehow incapable of one of the most fundamental aspects of the craft is absurd.
Well written. Especially in those first classes in OOP, one often get the impression that everything should be an abstraction and you should do inheritance structures of everything just because you can. Very little is said about avoiding to create couplings and reducing complexity of your code.
@@sfulibarri Your comment is placed under a video that is considered popular. It's popular because a lot of programmers are struggeling with this basic case of a basic concept.
@@ecm9251 Well the video is about one of the pitfalls of making unnecessary abstractions, not about abstraction itself, a person searching for 'what is abstraction in programming' is probably not watching this video. I agree that some may struggle with abstraction in the sense that they may make abstractions that are counterproductive, but you seem to be suggesting that 'most' programmers fail to understand and/or use abstraction at all which is just categorically untrue; any programmer who has ever extracted a few lines of code into a function has used abstraction. In my career I have worked with many jr developers and I never met one who simply could not grasp how or why they should extract a function or class. I maintain that the difficulty of abstraction comes from knowing how and when to use it effectively, not understanding the foundational concept itself.
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.
I think clear requirements and thinking a bit about what the most fitting implementation seems to be before starting with it can help quite a bit. If you're working in a team and use a ticket system it's rather important in my experience to think tickets through and question them as well before working on the implementation and noticing logical inconsistencies or potential problems that were not touched upon. Thinking a bit about the near future/setting boundaries can help as well finding the balance between abstraction and a more concise/simple but tight code. You can't prevent needing to make changes later on or rewriting some stuff mid-implementation, though, that's par of the course.
@@dennisjungbauer4467 I'm guessing there also are significant differences between different branches of programming. I work with a small team of game devs. And in the beginning our programmers all discuss togather and agree on benefits and acceptable limits of a system. And then about half a year into development, it turns out our current system did not feel good enough to players. So the designers modify the core specifications 1 month before delivery. Usually with too major differences to justify just a rework. So we end up having to do a half-assed system temporarily and make it from scratch again later. So there are definitely also deminishing returns on planning ahead in my field. We basiclly just do uses imaginable expansions for the current system. Reworks are too unpredictable to support sadly. While I can imagine in system applications, the core behaviour is much more set in stone from the get go, and the front and back end can be dealt with separately.
@@AleksanderFimreite IMO, reworks should only be done if modifying the existing code will take a lot longer than doing a rework in a different, more modern/efficient language or tool.
I remember when I was taught programming long a ago, one of the first thing the professor told us is that sometimes, maybe often, it is better to start over. Learn to accept that. [edit] And to add to that, abstract classes, especially does that are written with reusability in mind tend to take some time to polish. However, I learned in practice that code often has an expiration date. This 'saver' class is a good example. The xml code gets ditched for json. Don't think the json will last forever. It too will be obsolete. And unless you are working on something like the space shuttle, it won't take decades.
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).
This is wise advice, code duplication is little more effort in most languages than Ctrl-C, Ctrl-V then change external names of the duplicated section. A well-designed abstraction on the other hand takes a deep knowledge of the problem domain and the intended end-user experience of the software. Often decisions about abstractions can only be made using feedback on the behaviour of the code in production. A premature abstraction will cost far more to unwind than the slight loss of efficiency caused by duplicate code. I design software for legal professionals at my current job and we are right now putting in some abstractions that have taken 2 years of use in production to figure out (i.e. will the time and money spent result in a meaningful improvement in workflow efficiency for users?) All textbooks ignore the two great limits in business: time and money. Outside the classroom you never have enough of either to do all the things, so often quick and dirty gets the product delivered to the paying customer and my paycheck delivered to my bank account. There is more inelegant dirty code that would fail any exam on software design out in the world than you will ever want to know about.
most of the time I am like: "FU I don't care if it's repetitive (with some minor differences), I am not paid enough to write an error-proof abstract class"
Sure, unless you're duplicating business logic, which leads to bugs when you update the logic in one place but forget about the one or many other places. That can be very, very expensive.
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.
I am mostly against this. Creating interfaces that won't have more than one implementation at the same time are a waste. Except in cases where the code is talking to an external system like an API or a DB. Removing dependencies just to create a mocking hell is not a good approach to me. Test the unit with its dependencies every time you can
@@leonardomangano6861 When you introduce dependency it goes down hill. Even if you only implement your interface in one class, it allows you to introduce loose-coupling in your codebase through Dependency Injection and Inversion of Control which make your project extendable and testable. In other words, when you start implementing new features, instead of coupling the actual implementation you can simply reference to its interface.
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.
Yep, abstractions inherently remove couple contrary to what this video says. The video just has bad and poorly thought out abstractions which is why there is coupling. I would have a save interface that takes no parameters. Other classes implement it and any parameters they need to actually do the save are passed in at construction to the subclass constructors. All coupling is removed
One mistake I commonly see is people try to abstract away pieces of code that only coincidentally repeat, but that isn't inherently equivalent. This is closely related to the single responsibility principle and separation of concerns. Code might be similar, but if they have different reasons to change, they do not belong under the same abstraction.
Wouldn't the whole point of the abstraction be, that you can just implement a new logic for the two different reasons to change, without modifying the code?
Sometimes it happens that I use almost same code in two places, but slightly different. But instead of creating a new function with two mods, I just leave it as it goes. This way I could even write function for "for loop" because they are the same in every place, so why shouldn't I have for() function for them? :-D
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
I think it's a good approach, but how will you handle the exception without violating Liskov subsitution principle ? Personally, i think the code smells overall... What I would do is create interface let's call it Saver, then use your GameConfig to store your Saver (new GameConfig() { saver = new SaveXML() }), finally all you have to do in that save method shown in the example is fetch the saver and call some save method with the state and name of the file. This removes the need for an exception to handle invalid save format.
@@SamuelLopez-mr5br handle what exception? Your XML saving class shouldn't be throwing XML specific errors back to the consumer of your GenericSaverInterface. It should be throwing a GenericICouldntDoIt exception. If your XML saver class can't handle your XML error, what makes you think your GameObject is going to fair any better? Problem solved, Liskov satifisfied.
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!
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.
Great comment. Interfaces also make unit testing much easier, some frameworks will automatically resolve mocked implementations of things like filesystem drivers to ensure safety.
@@rcnhsuailsnyfiue2 Agreed. Interfaces are very useful when you start implementing Service or Repository patterns in your project. Overall, it makes complex projects that require a lot of maintainabilities much easier to handle in the long term. One thing I forgot to talk about is that most applications nowadays are very complex and aren't meant to be deployed once and for all. One of the most important parts of current application development is maintainability, and how can you grow the application over time without wasting time on redundant complex logic that could've been avoided at the start with better project architecture. Also, another crucial and significant parts of any project's development is the extendability of prior features without breaking them.
Yes abstraction or interface, will help you to maintain the code in future. Yes it will be coupling but it is what we want right? If you get the handover source code from someone and you want to fix a bug that happened at Saving, you will look at FileSaver base class and see which are the child class, analysis the impact of changes. If your design s individual class as shown in the video, you need to change each of the individual classes' method one by one, which is what we saying DONT REPEAT YOURSELVES.
You could use a wrapper that transparently chooses between the different savers, you know. No need to create a skeleton abstract class without any meaningful shared logic
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.).
This. Inheritance is one small part of OOP programming that somehow became the dominant aspect instead of polymorphism, composition, and encapsulation which were the focus of its original advocates. I’m not a big Uncle Bob guy but he’s dead on about this.
There is nothing wrong with inheritance. Even a composition maybe a type of base class with a standard interface. The key is to decompose and organize functionality at a data/protocol level... that is work with properly describing the data and then focus on the protocol (i.e., abstractions etc). Just think OSI model... perfect example of proper design principles. All design principles come from the desire to elegantly design protocols.
@@JamesPhipps You can have "polymorphism" in Python, JS,Ruby,Lisps(Clojure), Smalltalk by just passing in functions and passing arguments through, ne need to use inheritance, classes, or even even interfaces. def save(saveFunc=Save.XML, *args)
@@cryp0g00n4 there is everything wrong with inheritance if you're writing a shader, and in a lot of cases with systems programming, hell even game programming inheritance adds extra bloat. Like anything else in life and programming, it all depends on the situation and talking about some general idea of "inheritance" and "abstraction" is really quite useless and only intermediate devs do it
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.
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".
The big trope is actually that people abstract far too much. Plenty new programmers hear a couple buzzwords and go ball-deep into making interfaces for every little thing, even if there's no good reason to believe it needs it.
@@gloweye I remember in college they enforce that every class needs like 5+ interfaces. I agree there is no need for this but saying abstraction should be avoided is kind of ignorant. I've worked on codebases that have lived for 20+ years with both scenarios and I can tell you too many are far easier than too few.
And this by itself takes testability to whole another level. Using example from the video: to test your high level functions you do not need to actually save the file. All you need to make sure (i.e. using mocks) that correct "saver" call was executed.
@@humanrye8696 exactly, fully agree. having proper interfaces almost eliminates the need for some fancy mocking frameworks that do some voodoo like shit (reflections and stuff). if you have an interface you can simply program your own mocks by impementing the interface.
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
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.
This is 100% right. The terrible thing that inheritance does to your mind is that it makes you want to cram everything into the same hierarchy of "things". (i.e. "File Savers") In this example, all the little pieces like the "periodic" file saver and the different file formats, and even the "streams" that the bytes are sent to from the conversion should all be interfaces that are composed together.
Then what is inheritance good for? Inheritance is talked about but then every other instance of inheritance seems like it’s poorly-thought-out interfaces or something akin to that.
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.
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.
All have their merits and knowing more ways can make the code better. Fully functional is only really worth if with a language/compiler that can use the fact that the code is functional to optimize the compilation. If your using a more traditional language, functional constructs can still help improve code but some of it can also be much less performant since real functional languages use many hidden optimizations that a non functional language cannot apply.
I was thinking doing it like this in Python/Ruby/JS (simple dependency injection?). def save(saveFunc=Save.XML, *args, **kwargs): ....# do some stuff with args and kwargs ....result = saveFunc(*args, **kwargs) # pass all other arguments straight to this func ....return somefunc(result) , the save method would do a bunch of stuff then call your saveFunc using keyword arguments (which are objects) that are automatically passed to your saveFunc. This reduces coupling, but you need intergration tests to make sure code is exercised but your program will probably have errors anyway if you don't do testing, I believe this is "message oriented" OOP programming.
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.
I just wanted to say that I really like your presentation style - working with examples from real use cases and showing how the code changes. That is very helpful and easy to follow, good job!
this video is only useful as an example of the kind of logical circles you can run around in if you don't understand SOLID principles. Please don't follow the advice in this video.
This is the situation at my work right now. Crazy balance between coupling and abstraction to deal with custom requirements growing. Every vendor has a different workflow and customers all want very unique things. It was once very coupled on the vendor side but we've broken it up a bit now.
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.
I think you are speaking about a language where an interface is not inherited. In Java for example it's the case. In c++ you have to inherit from an abstract class to make the contract of an "interface".
@@fellowkrieger457 That's the same thing - you're not inheriting an implementation, you're just enforcing method signatures. The reason it decreases coupling is because consumers are free to swap implementations as long as the class implements that interface.
This video decribes me toooo well. After learning OOP and design pattern stuff I am always thinking about "how" I can abstract something, not "when". And that indeed screw things up a lot later.
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))`.
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.
Great video. I think we as developers need to often stop and ask ourselves, "am I over-engineering this by implementing it this way?" Instead of just assuming they're doing the best work because it's a common practice. Otherwise it often causes immense headache when another developer (or even the person who designed something) need to refactor or change something. Thank you for bringing attention to this overlooked topic!
Abstraction doesn't increase coupling when used properly. Abstraction hides complex ideas behind simpler ones. There is nothing simpler about inheriting from a FileSaver class if the only thing you share is the filename. This poorly chosen example feels like too much of a straw-man and I think you're doing people a disservice by confusing an example of bad object-oriented design with the idea of abstraction, which has no inherent relation to OOP.
Exactly. In the video it says that inheriting from FileSaver sucks, because now you can't modify it that easily, because it must have a filename. Well in that case just delete the inheritance and you're good to go??
Most of my time in companies I worked for goes into convincing engineers not always repeating is bad! If you have only 2 cases for your found pattern, it doesn’t mean you need to create shared code! If you don’t have a complete insight over your problem or future requirements do not abstract!! Thank you for the video, The idea is to grow together and then we can do great things.
I have never liked reading heavily abstracted code. It makes reasoning about things more difficult. Ian Malcom said it best, "you were so preoccupied that you could, you never stoped to ask if you should".
Indeed. People mistake "complexity" with "tediousness." Sometimes the more tedious approach is the simpler approach. To a beginner, nothing's as complex as encapsulation & polymorphism. Back then it took me days to actually fully understand those concepts the first time I encountered them. I finished making an entire app not knowing what they truly are exactly. 😂
I just watched you videos on naming variables and nesting code. I ran over to see what other videos you had and realized how new this channel is. Keep it up! This will be super useful to anyone new to programming. You bring out subtleties and nuances in coding that I myself have struggled with, but never given voice to. It's reassuring to hear that repeating code is ok, because in some cases it feels much more natural to do so, but I tend to feel some bizzare responsiblity to generalize when possible. Even though I'm writing code solo, for myself.
Abstraction has loads of other benefits apart from reducing code repetition. It makes the code much more readable to other developers, easier and faster to integrate new components into the code, fault isolation, which eventually leads to faster integration, again, and easier to test, since you can just test components individually, which again helps in faster development. More abstraction does lead to coupling but after a bit of trial and error you get a sense of just how much abstraction is required. During development what I do is start out with no abstraction at all, all functions are their own source code with minimal component reuse. Then start abstraction from there, starting with the biggest, most reused components. Does take quite a bit of time to refactor the code but I'd say starting with abstracted code takes about equal amount of time in brainstorming the proper abstraction.
One consideration might be the skill level of other developers on your team. Delaying abstraction only until the benefits outweigh the coupling might mean you will encounter a huge mess the next time you need to work in this area of the code due to all the cumulative changes from the team. Setting up the abstraction early will encourage better design for future changes even if it may seem over engineered in the interim.
For instance, in a game, I might have to implement a global EXP multiplier for certain days of the week. Rather than having a variable be directly accessed in 60 different functions, I should just abstract the EXP variable to be handled by a class, allowing a global EXP multiplier to be more easily maintained within that single class. I think?
For some projects a senior dev who was very used to working with architectures used to lay the basics, he would then relay it to me for the implementation. I'd always be lost at first but I always ended up understanding the reasoning later, and the motivation too as some other projects were deeply ingrained in that whole coupling mess. So I'd totally agree with that, it's better to conceptualize and plan early so that you don't end up building on a mess
It's funny because most beginner code is decent code. It's when you start getting "mid-to-high" skilled developers that things go bad. It takes time to "unlearn" all of those "best practices". People tend to interpret complexity as sophistication.
@@matias-eduardo Well, in a programming class or when learning most of the time you work alone. Once you have a bunch of full stack programmers, each of which with different favorite/main languages and paradigms or simply different ways of thinking, that stuff goes south more than 0% of the time. Or when you have too much "democracy" and 20 different decisions that don't all fit together get made. So you have no coherent design of the whole programm, but a mess of concepts.
Great approach for the craft of programming! I really like the incorporation of the word "aesthetic" - I've found it myself that beautifully working code _is_ better. So, on the topic of abstraction - a broader generalization of what you've shown us, is that - if an abstraction is created to capture common ground from inside - implementation, tools, libraries, etc - it is usually unnecessary coupling. On the other hand - when abstraction tries to capture common view from outside - i.e. from those who _use_ the code, then it is usually OK, because it actually introduces a generalization, a true abstraction, and not just a code-duplication-removal.
If you start using langauges with optional classes (python, C++) or without classes (go, rust) all this becoming even more interesting! Basicaly, in most cases you shouldn't make abstractions at all, it's needed in rare cases. Procedure decomposition much better in many common cases, and OOP (especial with classes) can help in some corner cases only.
"identifying repetition and extracting it out" is honestly a junior/mid level understanding of abstraction. What you've described are the dangers of inheritance. The real power of abstraction is realized when you starting thinking about interfaces, layers, and domains. E.g. separation of concerns is achieved through abstraction ,and that decreases coupling.
First and foremost, I recently saw your videos, I love how you make them, I like the aesthetics of the edits and I like how you educate about these concepts, so kudos to you, I'm subscribed. Now, to me abstraction means more than just create classes and hide implementation details. Abstraction to me also means the usage of interfaces and the elimination of 'new' which ties your classes together (Tight coupling). Your FileSaver.Create method is still coupled to concretions like AWSSAver, SaveXML, etc. You could make the FileSaver where T is an IFile or a representation of something needed to save, that type T knows how to save itself because the "can do" contract is about implementing the Save method, after that you have different approaches, you can either use a factory pattern to make the concretion or better yet use a container that contains all the types something like services.Add() etc, and you can even change the abstraction on runtime, after that FileSaver can be much more, you can have an IEnumerable of files to save them, you can create a queue that saves them following a CRON or saves them using hangfire, etc, so you can use composition and have this IEnumerable as a property to do whatever you need. To me, the idea of abstraction should be there to talk about them as it is things in the real world (concepts or physical objects), if you can say them speaking using language then you can do it properly. Now, one rule of thumb I would use is... don't start with abstractions, start with the code all over a single piece of method and from there start the refactoring process, no duplicate code, separate things into methods, group behaviors and reasons to change into classes, etc., I don't think starting abstracting is a good idea, the main reason is because an implementation can change in the future because that Jira or whatever feature you are creating can change tomorrow.
I think this presentation leans into "abstraction" == "inheritance" == "coupling" a bit too much. Many abstractions exist with the goal of reducing coupling, but inheritance in particular is a highly coupled abstraction by its nature. For example, your "save" method probably has a lot of duplication in fetching the data it needs as it takes the game state and pulls from it. My instinct would be to invert this by separating how one gets the data for saving from how one actually stores that data. Maybe something like: GameState::save(SaveWriter &writer) { writer.savePlayer(this->player); writer.saveEnemies(getEnemyData()); writer.saveMoreStuff(...); } This way choosing what to save is left up to the GameState and how to save left up to the XmlSaveWriter and JsonSaveWriter, etc. In this way, the types of things you can save are highly coupled with the saver itself, but since you want all formats to be compatible anyway, so that is a feature. Meanwhile you reduce coupling of the game state on the destination of the save and leave that up to the caller. Adding new save formats is just extending the interface. The major point is that you are coupling the things that need to be the same (the data being saved) and decoupling the things that can vary (the save format). Exactly where that line is will depend on the project and desired goals. Maybe the SaveWriter should be more general with methods like saveInt(), saveList(), etc so that it is less tied specifically to saving game entities.
As I mainly write firmware for microcontrollers in C, my main criterion for abstracting or not a piece of code is whether this will save on memory. Maintainability is then a close second (may be prioritized over memory if the amount of memory saved otherwise is small). Elegance that's not essential to maintainability comes third. Embedded programming is quite a fun field!
Instead of coding in C, why not code in Rust or LISP and use Lisp macros as a super-powerful compiler generator... Also there is an option to inline functions or not, and often not inlining functions can let the code fit in cache.
I usually find that good abstraction reduces coupling. I always aim to reduce coupling in my refactors and in my design. Having that as a goal works way better than anything else.
The value in not repeating code is for the sake of maintainability. If you fix a bit of code that is shared across multiple functions you can be prone to miss all the spots that need updating. I typically dont let functions do more than "1 thing". I also am not doing OOP anymore. When a function does "1 thing" then its easy to generalize functions that can be generalized and functions that need to be hyper specific can be hyper specific
As a math student and a programmer, I'd say doing abstractions is like doing factorization When you see a few terms, like x²+2x, you can factor it into x(x+2), which makes some further actions simpler because you have separated components to work with. However, this isn't always the case to do. For example, x²+1 isn't that good to be factored to (x+i)(x-i). This only made your original polynomial messier. This is so similar to code abstraction in the way that, you have to identify what's worth abstracting, and how it can help in further applications. Doing abstractions in already very simplified code only gives coupling, just like the x²+1 case I've mentioned. You can just call it not factorable (not abstractable) and peace out.
As a physicist, sometimes (x+i)(x-i) is a useful factorization for (x^2)+1, but I agree that in most cases you're better off not factoring into imaginary components. And I think that's very apt for abstraction, even edge case abstraction can be useful, but outside of the edge case you designed it for the usefulness does not justify it.
I've definitely seen both scenarios, as well as the one where over-abstracted code losses all meaning. That said I've also had the experience where accession reduces coupling, because dependencies drop off. This is especially true when using DI.
I'm so glad I discovered this channel. I think I'm early, watched the two videos. I'd definitely love to see code architecture & structuring related videos.
What you're referring to here as abstraction seems to mostly just be inheritance. Abstraction deals more with making sense of raw data. EX: instead of remembering 3 numbers and an image, we group those together and call them a "GameObject," since it's easier to think about that way. Instead of calling 5 seemingly unrelated network functions, we wrap those up in an "UpdateServerState()," function because it makes more sense to read.
In oop terms, abstraction _is_ inheritance (and traits/interfaces, if they're separate in your language). Sure, we can call any struct or collection an "abstraction layer", but it doesn't help us reason about code architecture
@@Outfrost Reasoning about code architecture is literally what abstraction is. Abstraction is in every language. It's the act of naming and structuring things to give context to the code architecture in a way that "abstracts," the mental model away from the implementation, thus implementing a layer of reason into the system. In the context of OOP abstraction is sometimes defined as the act of exposing/hiding properties and methods within a class, which still has nothing to do with inheritance.
While inheritance is one form of abstraction, others forms exist. A DateTime class is an abstraction, regardless if it doesn’t inherit from anything. Private methods, with good names of course, can (but not always) be abstraction. Really the title of the video should be something like Bad Class Hierarchy Design Makes Your Code Worse. Which of course is true. And given there are lots more ways to design a bad hierarchy than good one, most of the ones developers deal with are closer to the bad end than good one.
The problem in the example is really that the different savers have too much responsibility, namely both serializing the game state to a certain format, and also writing it to some kind of storage. This would make much more sense if you had an XMLSerializer and JSONSerializer that only did just that: take some input data and convert it to the correct format, then return it either as a string or a stream. Then you would have a GameManager that uses one type of serializer (DI) to facilitate the whole logic of "saveGame", and one or more storage connection classes (also DI), for example. In the end, you would end up with two interfaces: Serializer, Storage And several single purpose classes: GameManager, XMLSerializer, JSONSerializer, FileStorage, SqliteStorage, etc.
Completely agree. And maybe use the strategy pattern if the serializers need separation between. ‘Walk the data structure’ and “emit the serialized blobs” code
What do you think about abstraction in the terms of readability? What I mean is when the naming is on point (on top of your logic in abstracting things), you can read the "headlines" of the code and it divides into very understandable blocks and chunks and functionalities for human brain, and you quickly get a grasp of the big picture. With a lot of repetitive coding you're a bit in the jungle trying to see the forest from the undergrowth, you're whacking with a machete left and right trying to piece together the general picture of repetitive sections, you're creating the library of structure in your head.
good video. one question you can always ask yourself is: what is the blast radius of this change. having code duplication can be a good thing if you want to reduce the blast radius. "do i want to break y when i fix this for x". sometimes, instead of abstracting something away to a library, just leave it where it is until you really need to abstract it. "YAGNI" can be perfectly applied to pre mature abstractions ("you aint gonna need it")
To be honest, good abstractions aren't made because you want to limit duplicate code. Sometimes two processes, even though they are completely unrelated, do things the same way. That doesn't mean that they should share an abstraction. Sometimes things just look alike. The point of abstraction has never been, and will never be, limiting code duplication. It is all about sharing contextual abstractions. Hey I need to read an XML file to this format. That is translated to wanting to read a file that is in a format, and translating it to specific type X. That is an abstraction. Abstracting an account and a vault because they both have passswords isn't. Sometimes two or more things just look alike, but they aren't the same, even though they do things in the same way. Never abstract to limit duplication, abstract because you want to introduce flexibility to your system.
I recommend Chris Keathly's talk on how applying S.O.L.I.D. increases interaction points. Both concepts are in direct tension. So like everything in programming, it depends...
I've never run into any system that suffers from overcoupling. Instead, I've often had to deal with systems that have copypasta code galore, dozens upon dozens of lines, and then when a feature needs changing, and it needs to happen everywhere, you gotta go and find all instances of this code and modify it X times, and test each one, because sometimes they have subtle differences and...
I feel you. Too much repetition hides the subtle differences and buries the underlying structure of the code - its business logic. Even if you only need to change one place, understanding the code and finding the right place to change is hard. Without a good structured/abstracted code, e.g. with everything inside one big KLOC function, you don't find what blocks are relevant for you or which ones to ignore, because it's too much to parse by your brain at once.
I've been writing the comment the second time since I don't know how to best put it. No. Coupling is an inherent trait of Inheritance. Not of abstraction. There is more than one way to abstract code then to subclass it. If your entire definition of abstraction reduces to subclasses, you miss a few other options. Like extract to functions. Or traits/interfaces. There is a rule I have: If the class only has one public method, it's a curried static function. Convert that to a static function first. That would have nipped the "FileSaver" class in the bud. Then have an ObjectSaver interface that calls the static function. Martin Fowler calls that a repository.
Interefaces are also important to consider. in this videos examples, you could have as many diofferent types of savers as you want, with one bit of logic to determine which to use, that all share a common interface, and then set up a way to check if its using interval at runtime and set the hook for that, for example. But, interface design is tricky, and you can end up with extra coupling where a certain method doesn't make sense for a particular implementation...
I really appreciate the thought that abstraction and coupling are opposing forces. I've never considered it from that perspective and I think this will help me when designing future systems.
Thinking about the data you have and what you need to do to it is more important than figuring out abstractions. Figure out what data you actually need to perform the work and write code to operate on that struct. This is the main idea behind data oriented design
You just coupled in my mind all the abstractions I did in the past by this abstract framework of abstraction-coupling. I will abstract no more in the same way, being now coupled to your ideas.
For me it's all about defining abstractions that couple the code in a useful way. In other words if there's code that when it changes, by definition (not by convenience) must change in all places then it should be coupled and therefore abstracted.
I agree. One way to think about it is that you're moving the coupling/complexity (or, more concretely, the things that need to happen and the order in which they need to happen to transform the data) to an isolated place (for example, a single enum) so that the rest of your code doesn't need to "think" about those things. It just needs to "do the thing". This is also why programming languages with proper reflection are so powerful, they can enforce things to happen in your code at compile-time so that the implementation itself does not need to "think" about it. To me this is what good de-coupling means. You move complexity to few places where you actually need to "think" about it, and the rest is just writing implementation code.
The coupling and associated limitations are a consequence of using inheritance to model relationships that are not is-a. Think of it another way: The SaveJson class has-a filesaver, now the filesaver can become a private member variable initialized at construction. This is the composition/dependency injection duo, and alleviates the problems you just mentioned.
Code is an art and a state of mind. I think that abstraction can't be learned, some feel it and some never, even with years of development. This is what differentiates an excellent programmer from others, an author of widely used libraries, frameworks, languages from others. Very good visual quality of the video, can't wait to see where this channel will take us.
@@02orochi Maybe at first, but over an entire career this will completely even out as you will be very slow at learning other things. And the huge majority you learn faster because you've already done something similar before. Search up the violin study on natural proficiency vs hard work.
I clicked the thumbnail thinking "what bs is this?" and finished the video and now I know where all my frustration comes from. Thanks for showing us something so easy to miss
NOPE NOPE NOPE NOPE. ABSOLUTELY not. Coupling is not the opposite of abstraction. OOP was literally invented to use abstraction to decrease coupling. There are tons and tons of ways to do abstraction while reducing coupling. Coupling as it was originally defined was defined as “when I change thing X, I also have to change thing Y”. Properly architected OOP eliminates coupling and increases cohesion. The issue here is that you’re using the abstractions *wrong*, not that the abstractions are bad. Inheritance is great, *when you use it right*. And none of these examples use it right. You should use classical inheritance when the objects are mostly the same but differ on one, and only one, main axis. And you should use hooks + abstract methods to allow the base class (the parent class) to control initialization, not the child. Interfaces are good! Use them! Use them more! Interfaces *decrease coupling*! If you have a method that accepts an interface instead of a class, you’ve made the method much more flexible! That’s good and one of the main goals of OOP. Before, the method refused to work with anything other than objects of one specific class. After adding an interface, it will work with *any object that implements the interface*. This allows you to achieve that one maxim: objects should be open to extension and closed to modification. You can extend the method’s behavior by implementing the interface on a different class. Boom, more flexible, easily-changeable software. Other commenters mentioned domain objects. DO NOT USE THESE! They’re VERY rarely the kinds of objects you want to build. This puts you in a trap where you try to stuff messages into classes where they’re not supposed to go. Instead, think about the *messages that need to be sent*, and then come up with the objects where those need to be put. Use DRY and the single responsibility principle to help you do the design. Use TDD to bring out the design incrementally. And remember: you never know less than you know now. For more stuff like this, I HIGHLY recommend reading Practical Object-Oriented Development in Ruby by Sandi Metz. It explains all of this and more a lot better and has a GREAT bit on abstraction in the prologue.
Abstraction *through inheritance* creates coupling. Once you mostly let go of class hierarchies and choose to program using data types and functions instead, most of these issues go away by themselves.
The "Rule of Three" is a good counterweight to "Don't Repeat Yourself" The idea is that you need at least three examples before you can decide what a good abstraction would be. If you try to write an abstraction too soon, you might abstract around concepts which turn out to be not very useful.
I think the most important lesson is "does this actually improve the code right now". Premature abstraction is mostly as bad as premature optimization, if you do it to early on your development you might get the wrong abstractions. For that reason, all abstractions should provide some real benefit right now. Sure, many times you see the potential for abstraction that might be useful BUT if you never get to need that abstraction, adding it is not only a waste of time but unnecessary added complexity. And in the video, sure adding a Save interface to hide the implementations would be good if its used in many places and your going to keep both or all of them, but if the goal is to phase out old code, you probably want to keep the switch simple so that once you remove the old one you do not have left any abstractions your no longer using. Its often harder to remove abstractions than to add them, especially if its many layers involved, it could be hard to even see that its no longer used.
Good abstraction can actually reduce coupling because your main code is no longer tied to the implementation details of some external class. It can help to prevent hours of suffering when you suddenly need to change third party dependencies. As Kleppmann writes, abstraction is one of the best tools we have to mitigate accidental complexity in our systems. I certainly prefer the abstraction of a higher level programming language over having to code in binary. Unfortunately, bad abstraction tends to be much more prevalent and can cause many more problems than they "fix." Great vid!
Cant wait until he discovers Dependency Injection and Interfaces, then learn to use it properly... until then, he will rediscover whole OOP piece by piece
If you are adding an abstraction that will add coupling, you might choose the wrong abstraction. What you need is a serializer that can transfer your game state into bytes (json, xml ...), and a saver that takes bytes to make it preserve to some storage (S3, GCS, file, database, memory ... ). IMO, abstraction is a great tool to eliminate coupling.
I learned to code on the job over the years, I don't have an academic CS background, and I've often noticed benefits of the academic training some of my colleagues received that make me kind of wish I had done a CS degree. That said, I have also notice a tendency in them to follow certain rules and conventions blindly, having been taught that that is the "right" way to do it. Everything you've said in this video just seems like common sense to me, it wouldn't occur to me that I "must" always avoid every scrap of repetition as a rule - repeting code is bad not just because it's "wrong", it's bad because I have to maintain more code, so the more I'm repeating the worse it is. But obviously, having the code defined separately in each class means I can change one without affecting the others. So I do whatever I feel is the best cost-benefit solution for my case. If you understand why these things are good or bad, you don't need formalised rules for when you should or shouldn't do something because it's common sense.
“Independent Evolution”. Just because 2 classes share some logic *now* does not mean they should *later*. Abstract *only* if you are certain those will always be in common (as best as you can predict/foresee). More often than not, answer for me is “no,” or “I guess we’ll see later.” So glad to see someone else has found words for this, thanks for the video
Cool stuff. I think one important thing to consider here is that abstraction doesn’t increase overall coupling per say. In your example, with no abstraction the coupling will now happen with the rest of your code interacting with your saver, and I don’t know if that’s a better scenario. Architecture flows so the whole breadth of how the system works matters. The goal is to lock your features for modification, so removing the XML saver for example won’t break logic in multiple other classes needlessly that were already tested. This is why the dependency rule is probably one of the most important rules to stick with. Your application should be split into layers, where there’s a saving top layer, for example. That layer should manage the bottom layers where you might have any variety of savers. That way, the rest of your code doesn’t care what happens when you save. It only knows to call a standard method on the top layer. That way, it truly doesn’t matter if you abstract your savers or not, because their lifecycle will be managed in a single place and any number of changes to the savers won’t result in changing code in unrelated features, which is often the way developers introduce bugs to existing functional features.
Not a programmer, but I think that json and xml have nothing in common, thus no reason to make em connected by common class. It's much better to divide generation of json or xml and saving itself. Thus methods template saveToJson(T where) and saveToXml(T where) are advantageous. They will generate text and call saver class. Classes are the perfect places for abstraction, and can serve file, pipe, socket, db and whatsoever.
Been debating about abstraction in my both my React and API code. On one half, I enjoy knowing how each function's flow. On the other, I like writing less. I haven't really touched OOP recently, but I'm trying to avoid it as functional programming is something I prefer working on in the future.
@@DanDanDandelion Yeah but it's less code regardless. I worked with functional programming on Scala and rarely passed 50+ lines of code for basic college assignments.
If it was me and I wasn't limited by OOP abstractions, I would create an interface (trait) "Saver" that contains an associated type "Resource", a "constructor(Resource)" and a "save(GameState)" method. And then just pass a specific implementation of "Saver" to the Game object.
I think the missing piece is that inheritance is the only method of abstraction presented, and no mention of composition. GameObject is not great because it is typically implemented with inheritance but a better solution is ECS which is a compositional / data-oriented pattern, the same concepts are abstracted but in different ways
Yup. If you’re in a large org writing shared libraries or frameworks and you don’t understand abstraction you’re not going to have a job for very long. If you’re on a small team and insist on “pre-factoring” so much I need to have six files open just to figure out how I’m going to handle the response from an API endpoint let alone update models/UI then you’re not gonna have any friends.
However, his examples use inheritance, which makes understanding code needlessly harder. Never ever EVER use inheritance if you can avoid it, use composition instead!
Avoiding repetition is rule of thumb that should help us to make the code easier to maintain. If using a bit of duplicate code is in the end increasing the maintainablility, because we don't have to use abstraction, that is a good thing.
"Don't repeat yourself" was never supposed to be about properties/fields, nor even code more generally. It's about logic. If you encode a single bit of logic twice, then you don't have a single source of truth and you're stuck having to make sure that any change to that logic is repeated in both places. Lots of classes will have a Name property, because names are useful. That doesn't mean you should create a NamedObject abstraction. And interfaces should not be created just because two or more things are able to implement the same interface. The interface is for the consumer(s) of that interface, not the implementers. In summary, the possibility of bad abstractions does not mean that abstractions are bad.
I would love a video about file system structure! I know its always going to be different between projects but I know there are common patters with /src, /docs, /bin, etc. Are there principles we should be following as to not get too nested or too shallow in our file structure?
Ui rules" apply here. If you have navigation menus, it will take a long time to find what you're looking for if you have a lot of levels with a lot of generic split paths that end up hard to navigate. On the other hand, good luck finding a file in a completely flat directory for a sizable project. This is a version of "the hardest problem in programming is naming things". Only nest when you have a logical group with an accurate name and doing so would make it less difficult to navigate. Don't nest things just to "organize". Start with very broad understandable groups. Keep things wide unless there's a good reason not to.
@@TruthAndLoyalty Awesome, that makes sense. I hate digging through pages and pages of game UI or inventory dividers but I like that all my weapons and armor are grouped. Thank you!
It works well when you program in a way where you only need to know about the interface. Saving by interface makes it way easier to transition to another save format later down the line. If done right, you can change one line of code and now your entire code base is using a different file format.
Just found your channel, you've a new subscriber, really like the dedication to some topics related to clean code. Could you do a video about your take in OOP, second level inheritance, MVC and how what's been considered good practices has been changing through the years ?
I disagree with the save example, because you want to provide a unified way of saving and loading your game to an arbitrary file format. The other components are not responsible for knowing how the game is saved, just that it's 'savable' (so interface)
This channel is great. I'm a hobby programmer and I find each of your videos very instructive and encouraging. Thank you. I'm looking forward to your next video!
How strange - you didn't talk about DI here. This is the most valuable reason for abstraction. You don't have to change the code in any shape or form if you work with interfaces and inject them directly. Overall great vid. Keep it up!
You got it wrong: you tried to implement 2 abstractions in 1. The first is what we call serialization. And the second just the transportation (file, network). The way to do the first is by using the Visitor Pattern. So you then have a json serializer and a xml serializer. The object to serialize should then implement the serializable interface (which can call save methods of the given serializer, that construct is called "double dispatch"). Use the Visitor Pattern in cases where 1: you have hierarchical data(structure) and 2: want to (depency) inject behaviour into the object.
One rule of thumb I recently found is that abstractions should not abstract away permutations. Different code can copy paste every once in a while if the permutations can be explicitly shown. Essentially, just abstract away the boilerplater, and only boilerplate. This sounds obvious, but it’s easy to get carried away with seeing some common code and you’d think “let’s abstract this away” without realizing that commonly repeating code isn’t always repeating code. I often ask myself (“is this code just handling some boilerplate on its own, or is it writing some permutations in some way?” This falls inline with the xml/json example you gave (different perspectives, but still agreeing on similar things). From my perspective, you shouldn’t abstract away the internal filenames or file saves because they are the differences between xml and json, hence the permutations, so its implementation should be explicitly different. Abstraction would then become preferrable when some code expects a certain behavior that fits neatly into a function name.
I honestly can't tell if this is a joke video or it was simply meant to bait people (consider me baited), with the video starting with "For every bit of abstraction you add, you've added more coupling". No, abstraction means literally the exact opposite. If your abstractions are making your code more coupled then you need to learn some better design patterns and stop using so much inheritance.
This got me curating coupling mechanisms more, rather than impulsive abstraction for concise presentation, despite presentation not benefitting from BEING concise. For some reason, I'd had a bias that abstraction provided sufficient plasticity and organization and was almost always preferred when a mechanism wasn't exclusive to coupling.
What the hell kind of question is that? I have even worked in actual companies and most companies especially at enterprise level have far worse code standards ranging especially from legacy code level and the way how teams are structured to deal with client feature requests. This video deals with just application of coding concepts while coding for company introduces complexities such as trade offs for code and analysis in juxtaposition with the companies resources and their project delivery structure which is a whole different thing.
This is a perfect example of misunderstanding abstractions... The real solution would be to separate out "convert between representations" and "write stuff to a persistent store" If you want to save to XML: first convert to a XML string and then save that string to a file. Similarly with JSON. Then you have properly separated your concerns and the code becomes much simpler to manage. Using inheritance almost always is the wrong tool to solve abstraction related problems.
Good looking Channel, short format, single subject, calm ton & relaxing background music, visual/ easy to understand/synthetic examples & explanations, this is probably gonna be a killer channel, glad to be here at the begining of the story, whish you the best :)
This came at a good time for me. I'm rewriting my game engine's code and I have felt the effects of coupling without really understanding what I was doing wrong.
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.
There's an interesting constraint that you can place on yourself that makes DRY good: don't use functions to DRY up your code. It sounds extremely counter-intuitive, but it forces the coder to think about queuing things up and doing them all at once further down the flow. There's a good talk by Mike Acton that can help understand the value in doing this.
Idk what you eat for breakfast but I want some.
@@matias-eduardo do you remember the title by any chance? I would love to listen!
@@zostaw9421 ua-cam.com/video/rX0ItVEVjHc/v-deo.html
@@zostaw9421 I revisit it every year and learn something new. Everything from min 40+ is timeless.
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.
I don't believe experience will make a programmer being capable of using abstraction. It takes abstract thinking and it seems like a lot of people struggle with this. Most programmers might never in their live be able to use abstract programming, no matter how long they try. Some will be able to use it with very little experience in programming. It might sound harsh, but we all know that people are different and abstract programming isn't for everyone.
@@ecm9251 Abstraction is one of the 3 basic mechanisms of any programming language. In most common languages once a programmer learns how to write functions or classes they are already using abstraction. Programmers may be better or worse at leveraging abstraction to create better software but the idea that some programmers are somehow incapable of one of the most fundamental aspects of the craft is absurd.
Well written. Especially in those first classes in OOP, one often get the impression that everything should be an abstraction and you should do inheritance structures of everything just because you can. Very little is said about avoiding to create couplings and reducing complexity of your code.
@@sfulibarri Your comment is placed under a video that is considered popular. It's popular because a lot of programmers are struggeling with this basic case of a basic concept.
@@ecm9251 Well the video is about one of the pitfalls of making unnecessary abstractions, not about abstraction itself, a person searching for 'what is abstraction in programming' is probably not watching this video. I agree that some may struggle with abstraction in the sense that they may make abstractions that are counterproductive, but you seem to be suggesting that 'most' programmers fail to understand and/or use abstraction at all which is just categorically untrue; any programmer who has ever extracted a few lines of code into a function has used abstraction. In my career I have worked with many jr developers and I never met one who simply could not grasp how or why they should extract a function or class. I maintain that the difficulty of abstraction comes from knowing how and when to use it effectively, not understanding the foundational concept itself.
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.
I think clear requirements and thinking a bit about what the most fitting implementation seems to be before starting with it can help quite a bit. If you're working in a team and use a ticket system it's rather important in my experience to think tickets through and question them as well before working on the implementation and noticing logical inconsistencies or potential problems that were not touched upon. Thinking a bit about the near future/setting boundaries can help as well finding the balance between abstraction and a more concise/simple but tight code.
You can't prevent needing to make changes later on or rewriting some stuff mid-implementation, though, that's par of the course.
@@dennisjungbauer4467 I'm guessing there also are significant differences between different branches of programming. I work with a small team of game devs. And in the beginning our programmers all discuss togather and agree on benefits and acceptable limits of a system.
And then about half a year into development, it turns out our current system did not feel good enough to players. So the designers modify the core specifications 1 month before delivery. Usually with too major differences to justify just a rework. So we end up having to do a half-assed system temporarily and make it from scratch again later.
So there are definitely also deminishing returns on planning ahead in my field. We basiclly just do uses imaginable expansions for the current system. Reworks are too unpredictable to support sadly.
While I can imagine in system applications, the core behaviour is much more set in stone from the get go, and the front and back end can be dealt with separately.
@@AleksanderFimreite IMO, reworks should only be done if modifying the existing code will take a lot longer than doing a rework in a different, more modern/efficient language or tool.
I'm just glad we are out of abstraction hell of 2010, my god every API and codebase back then.
I remember when I was taught programming long a ago, one of the first thing the professor told us is that sometimes, maybe often, it is better to start over. Learn to accept that.
[edit]
And to add to that, abstract classes, especially does that are written with reusability in mind tend to take some time to polish. However, I learned in practice that code often has an expiration date. This 'saver' class is a good example. The xml code gets ditched for json. Don't think the json will last forever. It too will be obsolete. And unless you are working on something like the space shuttle, it won't take decades.
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).
This is wise advice, code duplication is little more effort in most languages than Ctrl-C, Ctrl-V then change external names of the duplicated section.
A well-designed abstraction on the other hand takes a deep knowledge of the problem domain and the intended end-user experience of the software.
Often decisions about abstractions can only be made using feedback on the behaviour of the code in production.
A premature abstraction will cost far more to unwind than the slight loss of efficiency caused by duplicate code.
I design software for legal professionals at my current job and we are right now putting in some abstractions that have taken 2 years of use in production to figure out (i.e. will the time and money spent result in a meaningful improvement in workflow efficiency for users?)
All textbooks ignore the two great limits in business: time and money.
Outside the classroom you never have enough of either to do all the things, so often quick and dirty gets the product delivered to the paying customer and my paycheck delivered to my bank account.
There is more inelegant dirty code that would fail any exam on software design out in the world than you will ever want to know about.
most of the time I am like: "FU I don't care if it's repetitive (with some minor differences), I am not paid enough to write an error-proof abstract class"
Yeah, Ruby could well be as dead as a Dodo but Sandi Metz is still very much worth listening to.
Sure, unless you're duplicating business logic, which leads to bugs when you update the logic in one place but forget about the one or many other places. That can be very, very expensive.
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.
exactly, the class becomes reliant on an interface instead of the actual implementation, thus *reducing* coupling
This. Proper dependency extraction can lead to code that is simple to test. Testable code is good code.
100%
I am mostly against this. Creating interfaces that won't have more than one implementation at the same time are a waste. Except in cases where the code is talking to an external system like an API or a DB.
Removing dependencies just to create a mocking hell is not a good approach to me. Test the unit with its dependencies every time you can
@@leonardomangano6861 When you introduce dependency it goes down hill. Even if you only implement your interface in one class, it allows you to introduce loose-coupling in your codebase through Dependency Injection and Inversion of Control which make your project extendable and testable.
In other words, when you start implementing new features, instead of coupling the actual implementation you can simply reference to its interface.
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.
Yep, abstractions inherently remove couple contrary to what this video says. The video just has bad and poorly thought out abstractions which is why there is coupling. I would have a save interface that takes no parameters. Other classes implement it and any parameters they need to actually do the save are passed in at construction to the subclass constructors. All coupling is removed
One mistake I commonly see is people try to abstract away pieces of code that only coincidentally repeat, but that isn't inherently equivalent. This is closely related to the single responsibility principle and separation of concerns. Code might be similar, but if they have different reasons to change, they do not belong under the same abstraction.
Wouldn't the whole point of the abstraction be, that you can just implement a new logic for the two different reasons to change, without modifying the code?
Sometimes it happens that I use almost same code in two places, but slightly different. But instead of creating a new function with two mods, I just leave it as it goes. This way I could even write function for "for loop" because they are the same in every place, so why shouldn't I have for() function for them? :-D
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
I think it's a good approach, but how will you handle the exception without violating Liskov subsitution principle ? Personally, i think the code smells overall... What I would do is create interface let's call it Saver, then use your GameConfig to store your Saver (new GameConfig() { saver = new SaveXML() }), finally all you have to do in that save method shown in the example is fetch the saver and call some save method with the state and name of the file. This removes the need for an exception to handle invalid save format.
@@SamuelLopez-mr5br handle what exception? Your XML saving class shouldn't be throwing XML specific errors back to the consumer of your GenericSaverInterface. It should be throwing a GenericICouldntDoIt exception. If your XML saver class can't handle your XML error, what makes you think your GameObject is going to fair any better? Problem solved, Liskov satifisfied.
@@SamuelLopez-mr5br Catch the specialized errors inside each class and reraise them as an error common to both
yeah, "if" (and "switch") statements scary!
@@diskpoppyif you want to avoid if, switch statements, then you can think of strategy pattern or factory method for solving it ❤
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!
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.
Great comment. Interfaces also make unit testing much easier, some frameworks will automatically resolve mocked implementations of things like filesystem drivers to ensure safety.
@@rcnhsuailsnyfiue2 Agreed. Interfaces are very useful when you start implementing Service or Repository patterns in your project. Overall, it makes complex projects that require a lot of maintainabilities much easier to handle in the long term.
One thing I forgot to talk about is that most applications nowadays are very complex and aren't meant to be deployed once and for all. One of the most important parts of current application development is maintainability, and how can you grow the application over time without wasting time on redundant complex logic that could've been avoided at the start with better project architecture. Also, another crucial and significant parts of any project's development is the extendability of prior features without breaking them.
Yes abstraction or interface, will help you to maintain the code in future. Yes it will be coupling but it is what we want right?
If you get the handover source code from someone and you want to fix a bug that happened at Saving, you will look at FileSaver base class and see which are the child class, analysis the impact of changes.
If your design s individual class as shown in the video, you need to change each of the individual classes' method one by one, which is what we saying DONT REPEAT YOURSELVES.
You could use a wrapper that transparently chooses between the different savers, you know. No need to create a skeleton abstract class without any meaningful shared logic
@@wydx120 You missed the point. I explained why there will be a need to create a skeleton abstract class.
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.).
This. Inheritance is one small part of OOP programming that somehow became the dominant aspect instead of polymorphism, composition, and encapsulation which were the focus of its original advocates.
I’m not a big Uncle Bob guy but he’s dead on about this.
There is nothing wrong with inheritance. Even a composition maybe a type of base class with a standard interface. The key is to decompose and organize functionality at a data/protocol level... that is work with properly describing the data and then focus on the protocol (i.e., abstractions etc). Just think OSI model... perfect example of proper design principles. All design principles come from the desire to elegantly design protocols.
@@JamesPhipps You can have "polymorphism" in Python, JS,Ruby,Lisps(Clojure), Smalltalk by just passing in functions and passing arguments through, ne need to use inheritance, classes, or even even interfaces. def save(saveFunc=Save.XML, *args)
@@cryp0g00n4 there is everything wrong with inheritance if you're writing a shader, and in a lot of cases with systems programming, hell even game programming inheritance adds extra bloat. Like anything else in life and programming, it all depends on the situation and talking about some general idea of "inheritance" and "abstraction" is really quite useless and only intermediate devs do it
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.
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".
The big trope is actually that people abstract far too much. Plenty new programmers hear a couple buzzwords and go ball-deep into making interfaces for every little thing, even if there's no good reason to believe it needs it.
@@gloweye I remember in college they enforce that every class needs like 5+ interfaces. I agree there is no need for this but saying abstraction should be avoided is kind of ignorant. I've worked on codebases that have lived for 20+ years with both scenarios and I can tell you too many are far easier than too few.
And this by itself takes testability to whole another level. Using example from the video: to test your high level functions you do not need to actually save the file. All you need to make sure (i.e. using mocks) that correct "saver" call was executed.
Fuck this interface and class stuff.
@@humanrye8696 exactly, fully agree. having proper interfaces almost eliminates the need for some fancy mocking frameworks that do some voodoo like shit (reflections and stuff). if you have an interface you can simply program your own mocks by impementing the interface.
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
Java supports higher-order functions now, maybe try using them although you will have need pass through for arguments.
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.
Also thought about composition. (?)Something like
StateSaver(stateSerializer).save(filePath, gameState)
Exactly. Of course, choosing the wrong abstraction will lead to code that is worse.
This is 100% right. The terrible thing that inheritance does to your mind is that it makes you want to cram everything into the same hierarchy of "things". (i.e. "File Savers") In this example, all the little pieces like the "periodic" file saver and the different file formats, and even the "streams" that the bytes are sent to from the conversion should all be interfaces that are composed together.
Searched for this comment
Then what is inheritance good for? Inheritance is talked about but then every other instance of inheritance seems like it’s poorly-thought-out interfaces or something akin to that.
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.
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.
All have their merits and knowing more ways can make the code better. Fully functional is only really worth if with a language/compiler that can use the fact that the code is functional to optimize the compilation. If your using a more traditional language, functional constructs can still help improve code but some of it can also be much less performant since real functional languages use many hidden optimizations that a non functional language cannot apply.
I was thinking doing it like this in Python/Ruby/JS (simple dependency injection?).
def save(saveFunc=Save.XML, *args, **kwargs):
....# do some stuff with args and kwargs
....result = saveFunc(*args, **kwargs) # pass all other arguments straight to this func
....return somefunc(result)
, the save method would do a bunch of stuff then call your saveFunc using keyword arguments (which are objects) that are automatically passed to your saveFunc. This reduces coupling, but you need intergration tests to make sure code is exercised but your program will probably have errors anyway if you don't do testing, I believe this is "message oriented" OOP programming.
It's so easy to get this wrong and can be difficult to teach to someone else. You've done a great job at explaining it clearly and simply. Thank you.
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.
I just wanted to say that I really like your presentation style - working with examples from real use cases and showing how the code changes. That is very helpful and easy to follow, good job!
You know this channel is good when it's 11 days old and already have 17k subs with only 2 videos.
this video is only useful as an example of the kind of logical circles you can run around in if you don't understand SOLID principles. Please don't follow the advice in this video.
@@yrds96 That's not good, that's popular.
This is the situation at my work right now. Crazy balance between coupling and abstraction to deal with custom requirements growing. Every vendor has a different workflow and customers all want very unique things. It was once very coupled on the vendor side but we've broken it up a bit now.
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.
I think you are speaking about a language where an interface is not inherited.
In Java for example it's the case. In c++ you have to inherit from an abstract class to make the contract of an "interface".
@@fellowkrieger457 That's the same thing - you're not inheriting an implementation, you're just enforcing method signatures.
The reason it decreases coupling is because consumers are free to swap implementations as long as the class implements that interface.
This video decribes me toooo well. After learning OOP and design pattern stuff I am always thinking about "how" I can abstract something, not "when". And that indeed screw things up a lot later.
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))`.
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.
Great video. I think we as developers need to often stop and ask ourselves, "am I over-engineering this by implementing it this way?" Instead of just assuming they're doing the best work because it's a common practice. Otherwise it often causes immense headache when another developer (or even the person who designed something) need to refactor or change something.
Thank you for bringing attention to this overlooked topic!
Abstraction doesn't increase coupling when used properly. Abstraction hides complex ideas behind simpler ones. There is nothing simpler about inheriting from a FileSaver class if the only thing you share is the filename. This poorly chosen example feels like too much of a straw-man and I think you're doing people a disservice by confusing an example of bad object-oriented design with the idea of abstraction, which has no inherent relation to OOP.
This guy gets it, real communism has never been tried!
@@channel_channelsonThis would only make sense if real abstraction has never been done before correctly, which it has.
Exactly. In the video it says that inheriting from FileSaver sucks, because now you can't modify it that easily, because it must have a filename.
Well in that case just delete the inheritance and you're good to go??
Most of my time in companies I worked for goes into convincing engineers not always repeating is bad! If you have only 2 cases for your found pattern, it doesn’t mean you need to create shared code! If you don’t have a complete insight over your problem or future requirements do not abstract!!
Thank you for the video, The idea is to grow together and then we can do great things.
I REALLY love this way of showing code. Explaining what are you doing while showing a speed up video of the changes.
I have never liked reading heavily abstracted code. It makes reasoning about things more difficult. Ian Malcom said it best, "you were so preoccupied that you could, you never stoped to ask if you should".
Indeed. People mistake "complexity" with "tediousness." Sometimes the more tedious approach is the simpler approach. To a beginner, nothing's as complex as encapsulation & polymorphism. Back then it took me days to actually fully understand those concepts the first time I encountered them. I finished making an entire app not knowing what they truly are exactly. 😂
I just watched you videos on naming variables and nesting code. I ran over to see what other videos you had and realized how new this channel is. Keep it up! This will be super useful to anyone new to programming. You bring out subtleties and nuances in coding that I myself have struggled with, but never given voice to.
It's reassuring to hear that repeating code is ok, because in some cases it feels much more natural to do so, but I tend to feel some bizzare responsiblity to generalize when possible. Even though I'm writing code solo, for myself.
Abstraction has loads of other benefits apart from reducing code repetition. It makes the code much more readable to other developers, easier and faster to integrate new components into the code, fault isolation, which eventually leads to faster integration, again, and easier to test, since you can just test components individually, which again helps in faster development. More abstraction does lead to coupling but after a bit of trial and error you get a sense of just how much abstraction is required.
During development what I do is start out with no abstraction at all, all functions are their own source code with minimal component reuse. Then start abstraction from there, starting with the biggest, most reused components. Does take quite a bit of time to refactor the code but I'd say starting with abstracted code takes about equal amount of time in brainstorming the proper abstraction.
One consideration might be the skill level of other developers on your team. Delaying abstraction only until the benefits outweigh the coupling might mean you will encounter a huge mess the next time you need to work in this area of the code due to all the cumulative changes from the team. Setting up the abstraction early will encourage better design for future changes even if it may seem over engineered in the interim.
For instance, in a game, I might have to implement a global EXP multiplier for certain days of the week. Rather than having a variable be directly accessed in 60 different functions, I should just abstract the EXP variable to be handled by a class, allowing a global EXP multiplier to be more easily maintained within that single class.
I think?
For some projects a senior dev who was very used to working with architectures used to lay the basics, he would then relay it to me for the implementation. I'd always be lost at first but I always ended up understanding the reasoning later, and the motivation too as some other projects were deeply ingrained in that whole coupling mess. So I'd totally agree with that, it's better to conceptualize and plan early so that you don't end up building on a mess
yes abstraction can be great tool for design especially if paired with TDD.
It's funny because most beginner code is decent code. It's when you start getting "mid-to-high" skilled developers that things go bad. It takes time to "unlearn" all of those "best practices". People tend to interpret complexity as sophistication.
@@matias-eduardo Well, in a programming class or when learning most of the time you work alone. Once you have a bunch of full stack programmers, each of which with different favorite/main languages and paradigms or simply different ways of thinking, that stuff goes south more than 0% of the time. Or when you have too much "democracy" and 20 different decisions that don't all fit together get made. So you have no coherent design of the whole programm, but a mess of concepts.
Great approach for the craft of programming! I really like the incorporation of the word "aesthetic" - I've found it myself that beautifully working code _is_ better.
So, on the topic of abstraction - a broader generalization of what you've shown us, is that - if an abstraction is created to capture common ground from inside - implementation, tools, libraries, etc - it is usually unnecessary coupling. On the other hand - when abstraction tries to capture common view from outside - i.e. from those who _use_ the code, then it is usually OK, because it actually introduces a generalization, a true abstraction, and not just a code-duplication-removal.
If you start using langauges with optional classes (python, C++) or without classes (go, rust) all this becoming even more interesting! Basicaly, in most cases you shouldn't make abstractions at all, it's needed in rare cases. Procedure decomposition much better in many common cases, and OOP (especial with classes) can help in some corner cases only.
"identifying repetition and extracting it out" is honestly a junior/mid level understanding of abstraction. What you've described are the dangers of inheritance. The real power of abstraction is realized when you starting thinking about interfaces, layers, and domains. E.g. separation of concerns is achieved through abstraction ,and that decreases coupling.
First and foremost, I recently saw your videos, I love how you make them, I like the aesthetics of the edits and I like how you educate about these concepts, so kudos to you, I'm subscribed.
Now, to me abstraction means more than just create classes and hide implementation details. Abstraction to me also means the usage of interfaces and the elimination of 'new' which ties your classes together (Tight coupling).
Your FileSaver.Create method is still coupled to concretions like AWSSAver, SaveXML, etc. You could make the FileSaver where T is an IFile or a representation of something needed to save, that type T knows how to save itself because the "can do" contract is about implementing the Save method, after that you have different approaches, you can either use a factory pattern to make the concretion or better yet use a container that contains all the types something like services.Add() etc, and you can even change the abstraction on runtime, after that FileSaver can be much more, you can have an IEnumerable of files to save them, you can create a queue that saves them following a CRON or saves them using hangfire, etc, so you can use composition and have this IEnumerable as a property to do whatever you need.
To me, the idea of abstraction should be there to talk about them as it is things in the real world (concepts or physical objects), if you can say them speaking using language then you can do it properly.
Now, one rule of thumb I would use is... don't start with abstractions, start with the code all over a single piece of method and from there start the refactoring process, no duplicate code, separate things into methods, group behaviors and reasons to change into classes, etc., I don't think starting abstracting is a good idea, the main reason is because an implementation can change in the future because that Jira or whatever feature you are creating can change tomorrow.
I think this presentation leans into "abstraction" == "inheritance" == "coupling" a bit too much. Many abstractions exist with the goal of reducing coupling, but inheritance in particular is a highly coupled abstraction by its nature. For example, your "save" method probably has a lot of duplication in fetching the data it needs as it takes the game state and pulls from it. My instinct would be to invert this by separating how one gets the data for saving from how one actually stores that data. Maybe something like:
GameState::save(SaveWriter &writer) {
writer.savePlayer(this->player);
writer.saveEnemies(getEnemyData());
writer.saveMoreStuff(...);
}
This way choosing what to save is left up to the GameState and how to save left up to the XmlSaveWriter and JsonSaveWriter, etc. In this way, the types of things you can save are highly coupled with the saver itself, but since you want all formats to be compatible anyway, so that is a feature. Meanwhile you reduce coupling of the game state on the destination of the save and leave that up to the caller. Adding new save formats is just extending the interface.
The major point is that you are coupling the things that need to be the same (the data being saved) and decoupling the things that can vary (the save format). Exactly where that line is will depend on the project and desired goals. Maybe the SaveWriter should be more general with methods like saveInt(), saveList(), etc so that it is less tied specifically to saving game entities.
As I mainly write firmware for microcontrollers in C, my main criterion for abstracting or not a piece of code is whether this will save on memory. Maintainability is then a close second (may be prioritized over memory if the amount of memory saved otherwise is small). Elegance that's not essential to maintainability comes third. Embedded programming is quite a fun field!
that's awesome
Instead of coding in C, why not code in Rust or LISP and use Lisp macros as a super-powerful compiler generator... Also there is an option to inline functions or not, and often not inlining functions can let the code fit in cache.
@@aoeu256 because why would you do that? lol, C is the king
I usually find that good abstraction reduces coupling. I always aim to reduce coupling in my refactors and in my design. Having that as a goal works way better than anything else.
The value in not repeating code is for the sake of maintainability. If you fix a bit of code that is shared across multiple functions you can be prone to miss all the spots that need updating. I typically dont let functions do more than "1 thing". I also am not doing OOP anymore.
When a function does "1 thing" then its easy to generalize functions that can be generalized and functions that need to be hyper specific can be hyper specific
If I have multiple functions I place them in one separate file.
As a math student and a programmer, I'd say doing abstractions is like doing factorization
When you see a few terms, like x²+2x, you can factor it into x(x+2), which makes some further actions simpler because you have separated components to work with.
However, this isn't always the case to do. For example, x²+1 isn't that good to be factored to (x+i)(x-i). This only made your original polynomial messier.
This is so similar to code abstraction in the way that, you have to identify what's worth abstracting, and how it can help in further applications. Doing abstractions in already very simplified code only gives coupling, just like the x²+1 case I've mentioned. You can just call it not factorable (not abstractable) and peace out.
Small correction: it's (x^2)-1 that gets factored into (x+1)(x-1)
@@mrsharpie7899 what are you even correcting?
@@not_vinkami I believe they’re unaware that x^2 + 1 = (x-i)(x+i) and they’d assumed you’d meant x^2 -1 = (x-1)(x+1)
To be fair, when you really want to emphasize the imaginary answers of x^2 + 1, that factorization is useful.
As a physicist, sometimes (x+i)(x-i) is a useful factorization for (x^2)+1, but I agree that in most cases you're better off not factoring into imaginary components. And I think that's very apt for abstraction, even edge case abstraction can be useful, but outside of the edge case you designed it for the usefulness does not justify it.
I've definitely seen both scenarios, as well as the one where over-abstracted code losses all meaning. That said I've also had the experience where accession reduces coupling, because dependencies drop off. This is especially true when using DI.
I'm so glad I discovered this channel. I think I'm early, watched the two videos.
I'd definitely love to see code architecture & structuring related videos.
What you're referring to here as abstraction seems to mostly just be inheritance.
Abstraction deals more with making sense of raw data. EX: instead of remembering 3 numbers and an image, we group those together and call them a "GameObject," since it's easier to think about that way. Instead of calling 5 seemingly unrelated network functions, we wrap those up in an "UpdateServerState()," function because it makes more sense to read.
In oop terms, abstraction _is_ inheritance (and traits/interfaces, if they're separate in your language). Sure, we can call any struct or collection an "abstraction layer", but it doesn't help us reason about code architecture
@@Outfrost Reasoning about code architecture is literally what abstraction is. Abstraction is in every language. It's the act of naming and structuring things to give context to the code architecture in a way that "abstracts," the mental model away from the implementation, thus implementing a layer of reason into the system.
In the context of OOP abstraction is sometimes defined as the act of exposing/hiding properties and methods within a class, which still has nothing to do with inheritance.
@@ZachHixsonTutorials ok purist
While inheritance is one form of abstraction, others forms exist. A DateTime class is an abstraction, regardless if it doesn’t inherit from anything. Private methods, with good names of course, can (but not always) be abstraction.
Really the title of the video should be something like Bad Class Hierarchy Design Makes Your Code Worse. Which of course is true. And given there are lots more ways to design a bad hierarchy than good one, most of the ones developers deal with are closer to the bad end than good one.
The problem in the example is really that the different savers have too much responsibility, namely both serializing the game state to a certain format, and also writing it to some kind of storage. This would make much more sense if you had an XMLSerializer and JSONSerializer that only did just that: take some input data and convert it to the correct format, then return it either as a string or a stream. Then you would have a GameManager that uses one type of serializer (DI) to facilitate the whole logic of "saveGame", and one or more storage connection classes (also DI), for example.
In the end, you would end up with two interfaces: Serializer, Storage
And several single purpose classes: GameManager, XMLSerializer, JSONSerializer, FileStorage, SqliteStorage, etc.
Completely agree. And maybe use the strategy pattern if the serializers need separation between. ‘Walk the data structure’ and “emit the serialized blobs” code
What do you think about abstraction in the terms of readability? What I mean is when the naming is on point (on top of your logic in abstracting things), you can read the "headlines" of the code and it divides into very understandable blocks and chunks and functionalities for human brain, and you quickly get a grasp of the big picture. With a lot of repetitive coding you're a bit in the jungle trying to see the forest from the undergrowth, you're whacking with a machete left and right trying to piece together the general picture of repetitive sections, you're creating the library of structure in your head.
good video. one question you can always ask yourself is: what is the blast radius of this change. having code duplication can be a good thing if you want to reduce the blast radius. "do i want to break y when i fix this for x". sometimes, instead of abstracting something away to a library, just leave it where it is until you really need to abstract it. "YAGNI" can be perfectly applied to pre mature abstractions ("you aint gonna need it")
To be honest, good abstractions aren't made because you want to limit duplicate code. Sometimes two processes, even though they are completely unrelated, do things the same way. That doesn't mean that they should share an abstraction. Sometimes things just look alike. The point of abstraction has never been, and will never be, limiting code duplication. It is all about sharing contextual abstractions. Hey I need to read an XML file to this format. That is translated to wanting to read a file that is in a format, and translating it to specific type X. That is an abstraction. Abstracting an account and a vault because they both have passswords isn't. Sometimes two or more things just look alike, but they aren't the same, even though they do things in the same way. Never abstract to limit duplication, abstract because you want to introduce flexibility to your system.
I recommend Chris Keathly's talk on how applying S.O.L.I.D. increases interaction points. Both concepts are in direct tension. So like everything in programming, it depends...
I've never run into any system that suffers from overcoupling. Instead, I've often had to deal with systems that have copypasta code galore, dozens upon dozens of lines, and then when a feature needs changing, and it needs to happen everywhere, you gotta go and find all instances of this code and modify it X times, and test each one, because sometimes they have subtle differences and...
I feel you. Too much repetition hides the subtle differences and buries the underlying structure of the code - its business logic. Even if you only need to change one place, understanding the code and finding the right place to change is hard. Without a good structured/abstracted code, e.g. with everything inside one big KLOC function, you don't find what blocks are relevant for you or which ones to ignore, because it's too much to parse by your brain at once.
I've been writing the comment the second time since I don't know how to best put it.
No. Coupling is an inherent trait of Inheritance. Not of abstraction.
There is more than one way to abstract code then to subclass it.
If your entire definition of abstraction reduces to subclasses, you miss a few other options. Like extract to functions. Or traits/interfaces.
There is a rule I have: If the class only has one public method, it's a curried static function. Convert that to a static function first. That would have nipped the "FileSaver" class in the bud.
Then have an ObjectSaver interface that calls the static function. Martin Fowler calls that a repository.
Interefaces are also important to consider. in this videos examples, you could have as many diofferent types of savers as you want, with one bit of logic to determine which to use, that all share a common interface, and then set up a way to check if its using interval at runtime and set the hook for that, for example. But, interface design is tricky, and you can end up with extra coupling where a certain method doesn't make sense for a particular implementation...
I really appreciate the thought that abstraction and coupling are opposing forces. I've never considered it from that perspective and I think this will help me when designing future systems.
Thinking about the data you have and what you need to do to it is more important than figuring out abstractions. Figure out what data you actually need to perform the work and write code to operate on that struct. This is the main idea behind data oriented design
You just coupled in my mind all the abstractions I did in the past by this abstract framework of abstraction-coupling.
I will abstract no more in the same way, being now coupled to your ideas.
For me it's all about defining abstractions that couple the code in a useful way. In other words if there's code that when it changes, by definition (not by convenience) must change in all places then it should be coupled and therefore abstracted.
I agree. One way to think about it is that you're moving the coupling/complexity (or, more concretely, the things that need to happen and the order in which they need to happen to transform the data) to an isolated place (for example, a single enum) so that the rest of your code doesn't need to "think" about those things. It just needs to "do the thing". This is also why programming languages with proper reflection are so powerful, they can enforce things to happen in your code at compile-time so that the implementation itself does not need to "think" about it. To me this is what good de-coupling means. You move complexity to few places where you actually need to "think" about it, and the rest is just writing implementation code.
The coupling and associated limitations are a consequence of using inheritance to model relationships that are not is-a.
Think of it another way: The SaveJson class has-a filesaver, now the filesaver can become a private member variable initialized at construction.
This is the composition/dependency injection duo, and alleviates the problems you just mentioned.
Code is an art and a state of mind. I think that abstraction can't be learned, some feel it and some never, even with years of development. This is what differentiates an excellent programmer from others, an author of widely used libraries, frameworks, languages from others.
Very good visual quality of the video, can't wait to see where this channel will take us.
Everything can be learned, nobody is born with knowledge or natural aptitude towards programming abstraction.
@@katto1937 agree w first, disagree w second
some take longer to learn certain concepts than others
I disagree. There has to be an objective way to write better code, and avoid common pitfalls.
@@02orochi Maybe at first, but over an entire career this will completely even out as you will be very slow at learning other things. And the huge majority you learn faster because you've already done something similar before.
Search up the violin study on natural proficiency vs hard work.
no, it's very much a science, with rules, that you can learn. This is just what people say when they haven't learned them yet.
I clicked the thumbnail thinking "what bs is this?" and finished the video and now I know where all my frustration comes from. Thanks for showing us something so easy to miss
NOPE NOPE NOPE NOPE. ABSOLUTELY not. Coupling is not the opposite of abstraction. OOP was literally invented to use abstraction to decrease coupling. There are tons and tons of ways to do abstraction while reducing coupling.
Coupling as it was originally defined was defined as “when I change thing X, I also have to change thing Y”. Properly architected OOP eliminates coupling and increases cohesion.
The issue here is that you’re using the abstractions *wrong*, not that the abstractions are bad. Inheritance is great, *when you use it right*. And none of these examples use it right. You should use classical inheritance when the objects are mostly the same but differ on one, and only one, main axis. And you should use hooks + abstract methods to allow the base class (the parent class) to control initialization, not the child.
Interfaces are good! Use them! Use them more! Interfaces *decrease coupling*! If you have a method that accepts an interface instead of a class, you’ve made the method much more flexible! That’s good and one of the main goals of OOP. Before, the method refused to work with anything other than objects of one specific class. After adding an interface, it will work with *any object that implements the interface*. This allows you to achieve that one maxim: objects should be open to extension and closed to modification. You can extend the method’s behavior by implementing the interface on a different class. Boom, more flexible, easily-changeable software.
Other commenters mentioned domain objects. DO NOT USE THESE! They’re VERY rarely the kinds of objects you want to build. This puts you in a trap where you try to stuff messages into classes where they’re not supposed to go. Instead, think about the *messages that need to be sent*, and then come up with the objects where those need to be put. Use DRY and the single responsibility principle to help you do the design. Use TDD to bring out the design incrementally. And remember: you never know less than you know now.
For more stuff like this, I HIGHLY recommend reading Practical Object-Oriented Development in Ruby by Sandi Metz. It explains all of this and more a lot better and has a GREAT bit on abstraction in the prologue.
Abstraction *through inheritance* creates coupling. Once you mostly let go of class hierarchies and choose to program using data types and functions instead, most of these issues go away by themselves.
bro wakes up in a day thinking how can I make the most random things bad
Best abstraction for this case in my opinion:
interface GameStateStorage {
public void Save(GameState);
public GameState Load();
}
class JSONGameStateStorage imlements GameStateStorage {
JSONGameStateStorage(string filePath) {}
// implement methods
}
class XMLGameStateStorage imlements GameStateStorage {
XMLGameStateStorage(string filePath) {}
// implement methods
}
class PostgresGameStateStorage imlements GameStateStorage {
PostgresGameStateStorage(string dbHost, string dbUser, string dbPassword) {}
// implement methods
}
Just use an if statement. This is a solution to a made up problem that has nothing to do with programming.
The "Rule of Three" is a good counterweight to "Don't Repeat Yourself"
The idea is that you need at least three examples before you can decide what a good abstraction would be. If you try to write an abstraction too soon, you might abstract around concepts which turn out to be not very useful.
I think the most important lesson is "does this actually improve the code right now". Premature abstraction is mostly as bad as premature optimization, if you do it to early on your development you might get the wrong abstractions.
For that reason, all abstractions should provide some real benefit right now.
Sure, many times you see the potential for abstraction that might be useful BUT if you never get to need that abstraction, adding it is not only a waste of time but unnecessary added complexity.
And in the video, sure adding a Save interface to hide the implementations would be good if its used in many places and your going to keep both or all of them, but if the goal is to phase out old code, you probably want to keep the switch simple so that once you remove the old one you do not have left any abstractions your no longer using.
Its often harder to remove abstractions than to add them, especially if its many layers involved, it could be hard to even see that its no longer used.
i agree
i just love your animation style very simple and stylish
Good abstraction can actually reduce coupling because your main code is no longer tied to the implementation details of some external class. It can help to prevent hours of suffering when you suddenly need to change third party dependencies. As Kleppmann writes, abstraction is one of the best tools we have to mitigate accidental complexity in our systems. I certainly prefer the abstraction of a higher level programming language over having to code in binary. Unfortunately, bad abstraction tends to be much more prevalent and can cause many more problems than they "fix." Great vid!
Cant wait until he discovers Dependency Injection and Interfaces, then learn to use it properly... until then, he will rediscover whole OOP piece by piece
If you are adding an abstraction that will add coupling, you might choose the wrong abstraction.
What you need is a serializer that can transfer your game state into bytes (json, xml ...),
and a saver that takes bytes to make it preserve to some storage (S3, GCS, file, database, memory ... ).
IMO, abstraction is a great tool to eliminate coupling.
I learned to code on the job over the years, I don't have an academic CS background, and I've often noticed benefits of the academic training some of my colleagues received that make me kind of wish I had done a CS degree. That said, I have also notice a tendency in them to follow certain rules and conventions blindly, having been taught that that is the "right" way to do it. Everything you've said in this video just seems like common sense to me, it wouldn't occur to me that I "must" always avoid every scrap of repetition as a rule - repeting code is bad not just because it's "wrong", it's bad because I have to maintain more code, so the more I'm repeating the worse it is. But obviously, having the code defined separately in each class means I can change one without affecting the others. So I do whatever I feel is the best cost-benefit solution for my case. If you understand why these things are good or bad, you don't need formalised rules for when you should or shouldn't do something because it's common sense.
“Independent Evolution”. Just because 2 classes share some logic *now* does not mean they should *later*. Abstract *only* if you are certain those will always be in common (as best as you can predict/foresee). More often than not, answer for me is “no,” or “I guess we’ll see later.” So glad to see someone else has found words for this, thanks for the video
Cool stuff. I think one important thing to consider here is that abstraction doesn’t increase overall coupling per say. In your example, with no abstraction the coupling will now happen with the rest of your code interacting with your saver, and I don’t know if that’s a better scenario.
Architecture flows so the whole breadth of how the system works matters. The goal is to lock your features for modification, so removing the XML saver for example won’t break logic in multiple other classes needlessly that were already tested.
This is why the dependency rule is probably one of the most important rules to stick with. Your application should be split into layers, where there’s a saving top layer, for example. That layer should manage the bottom layers where you might have any variety of savers. That way, the rest of your code doesn’t care what happens when you save. It only knows to call a standard method on the top layer.
That way, it truly doesn’t matter if you abstract your savers or not, because their lifecycle will be managed in a single place and any number of changes to the savers won’t result in changing code in unrelated features, which is often the way developers introduce bugs to existing functional features.
Not a programmer, but I think that json and xml have nothing in common, thus no reason to make em connected by common class. It's much better to divide generation of json or xml and saving itself. Thus methods
template saveToJson(T where) and saveToXml(T where) are advantageous. They will generate text and call saver class.
Classes are the perfect places for abstraction, and can serve file, pipe, socket, db and whatsoever.
Wow, as a non-programmer, you demonstrated quite a deep knowledge.
Been debating about abstraction in my both my React and API code. On one half, I enjoy knowing how each function's flow. On the other, I like writing less. I haven't really touched OOP recently, but I'm trying to avoid it as functional programming is something I prefer working on in the future.
Well with higher order functions you are welcome back to the world of abstraction :)
@@DanDanDandelion Yeah but it's less code regardless. I worked with functional programming on Scala and rarely passed 50+ lines of code for basic college assignments.
The idea that abstraction increases coupling is insane; the whole point of abstraction is to reduce coupling.
If it was me and I wasn't limited by OOP abstractions, I would create an interface (trait) "Saver" that contains an associated type "Resource", a "constructor(Resource)" and a "save(GameState)" method. And then just pass a specific implementation of "Saver" to the Game object.
I think the missing piece is that inheritance is the only method of abstraction presented, and no mention of composition. GameObject is not great because it is typically implemented with inheritance but a better solution is ECS which is a compositional / data-oriented pattern, the same concepts are abstracted but in different ways
@@jonohiggs absolutely, OOP-style inheritance is an outdated form of abstraction...
Subscribed immediately! Btw what theme are you using? it looks very clean.
I think an often overlooked benefit of abstraction is that, if done properly, it makes understanding code simpler.
Yup. If you’re in a large org writing shared libraries or frameworks and you don’t understand abstraction you’re not going to have a job for very long. If you’re on a small team and insist on “pre-factoring” so much I need to have six files open just to figure out how I’m going to handle the response from an API endpoint let alone update models/UI then you’re not gonna have any friends.
However, his examples use inheritance, which makes understanding code needlessly harder. Never ever EVER use inheritance if you can avoid it, use composition instead!
Avoiding repetition is rule of thumb that should help us to make the code easier to maintain. If using a bit of duplicate code is in the end increasing the maintainablility, because we don't have to use abstraction, that is a good thing.
Great video! I tend to agree with you. Love the presentation. Looking forward to what you're planning on creating next.
"Don't repeat yourself" was never supposed to be about properties/fields, nor even code more generally. It's about logic. If you encode a single bit of logic twice, then you don't have a single source of truth and you're stuck having to make sure that any change to that logic is repeated in both places.
Lots of classes will have a Name property, because names are useful. That doesn't mean you should create a NamedObject abstraction.
And interfaces should not be created just because two or more things are able to implement the same interface. The interface is for the consumer(s) of that interface, not the implementers.
In summary, the possibility of bad abstractions does not mean that abstractions are bad.
I would love a video about file system structure! I know its always going to be different between projects but I know there are common patters with /src, /docs, /bin, etc. Are there principles we should be following as to not get too nested or too shallow in our file structure?
Just do what makes sense for your project, any convention for this would be dumb since every project has different needs
Ui rules" apply here. If you have navigation menus, it will take a long time to find what you're looking for if you have a lot of levels with a lot of generic split paths that end up hard to navigate. On the other hand, good luck finding a file in a completely flat directory for a sizable project.
This is a version of "the hardest problem in programming is naming things". Only nest when you have a logical group with an accurate name and doing so would make it less difficult to navigate. Don't nest things just to "organize". Start with very broad understandable groups. Keep things wide unless there's a good reason not to.
@@TruthAndLoyalty Awesome, that makes sense. I hate digging through pages and pages of game UI or inventory dividers but I like that all my weapons and armor are grouped. Thank you!
It works well when you program in a way where you only need to know about the interface.
Saving by interface makes it way easier to transition to another save format later down the line. If done right, you can change one line of code and now your entire code base is using a different file format.
Just found your channel, you've a new subscriber, really like the dedication to some topics related to clean code. Could you do a video about your take in OOP, second level inheritance, MVC and how what's been considered good practices has been changing through the years ?
This formulates a thing that I knew for 10 years, but didn't have the words to 100% describe to anyone, now I can point them to the video.
I disagree with the save example, because you want to provide a unified way of saving and loading your game to an arbitrary file format. The other components are not responsible for knowing how the game is saved, just that it's 'savable' (so interface)
Agree, the ugly abstraction was the problem, not abstractik itself, thats why we need composition ans dependency injection
This channel is great. I'm a hobby programmer and I find each of your videos very instructive and encouraging. Thank you. I'm looking forward to your next video!
Don't abstract away code, abstract away the idea
thats... right?
How strange - you didn't talk about DI here. This is the most valuable reason for abstraction. You don't have to change the code in any shape or form if you work with interfaces and inject them directly.
Overall great vid. Keep it up!
The 3b1b of code :)
Not really. There are others I can think of that have even higher production animations.
@@encapsulatio Even if the production isn’t the same quality as a channel like Reducible. I found his explanations to be straight forward and logical.
Agree! I think that's why some of the modern languages such as Rust have a principle of "Composition over Inheritance"
You got it wrong: you tried to implement 2 abstractions in 1. The first is what we call serialization. And the second just the transportation (file, network). The way to do the first is by using the Visitor Pattern. So you then have a json serializer and a xml serializer. The object to serialize should then implement the serializable interface (which can call save methods of the given serializer, that construct is called "double dispatch"). Use the Visitor Pattern in cases where 1: you have hierarchical data(structure) and 2: want to (depency) inject behaviour into the object.
One rule of thumb I recently found is that abstractions should not abstract away permutations. Different code can copy paste every once in a while if the permutations can be explicitly shown. Essentially, just abstract away the boilerplater, and only boilerplate. This sounds obvious, but it’s easy to get carried away with seeing some common code and you’d think “let’s abstract this away” without realizing that commonly repeating code isn’t always repeating code. I often ask myself (“is this code just handling some boilerplate on its own, or is it writing some permutations in some way?”
This falls inline with the xml/json example you gave (different perspectives, but still agreeing on similar things). From my perspective, you shouldn’t abstract away the internal filenames or file saves because they are the differences between xml and json, hence the permutations, so its implementation should be explicitly different. Abstraction would then become preferrable when some code expects a certain behavior that fits neatly into a function name.
I honestly can't tell if this is a joke video or it was simply meant to bait people (consider me baited), with the video starting with "For every bit of abstraction you add, you've added more coupling".
No, abstraction means literally the exact opposite. If your abstractions are making your code more coupled then you need to learn some better design patterns and stop using so much inheritance.
Agree, thank you
This got me curating coupling mechanisms more, rather than impulsive abstraction for concise presentation, despite presentation not benefitting from BEING concise.
For some reason, I'd had a bias that abstraction provided sufficient plasticity and organization and was almost always preferred when a mechanism wasn't exclusive to coupling.
No offense, just curious, but do you have any experience with writing code for actual companies?
This is the video that needs making
What the hell kind of question is that? I have even worked in actual companies and most companies especially at enterprise level have far worse code standards ranging especially from legacy code level and the way how teams are structured to deal with client feature requests. This video deals with just application of coding concepts while coding for company introduces complexities such as trade offs for code and analysis in juxtaposition with the companies resources and their project delivery structure which is a whole different thing.
@@georgeokello8620 it's a question that's not related to the video nor directed at you, I feel like that was made pretty clear in my original comment
This is a perfect example of misunderstanding abstractions...
The real solution would be to separate out "convert between representations" and "write stuff to a persistent store"
If you want to save to XML: first convert to a XML string and then save that string to a file.
Similarly with JSON.
Then you have properly separated your concerns and the code becomes much simpler to manage.
Using inheritance almost always is the wrong tool to solve abstraction related problems.
Good looking Channel, short format, single subject, calm ton & relaxing background music, visual/ easy to understand/synthetic examples & explanations, this is probably gonna be a killer channel, glad to be here at the begining of the story, whish you the best :)
This came at a good time for me. I'm rewriting my game engine's code and I have felt the effects of coupling without really understanding what I was doing wrong.