Search Unity

Question SOLID - Dependency inversion, am i overengineering?

Discussion in 'Scripting' started by elpatrongames69, Feb 9, 2024.

  1. elpatrongames69

    elpatrongames69

    Joined:
    Mar 2, 2023
    Posts:
    99
    Dependency inversion is {
    The Dependency Inversion Principle (DIP) states that high-level modules should not depend on low-level modules; both should depend on abstractions. Abstractions should not depend on details. Details should depend upon abstractions.
    }



    I try to follow SOLID 100% for learning purposes.

    Let's say there is a gamemanager class and we need to access it from the player for some reason.

    im 100% sure that there wont be a second gamemanager in games lifetime. However, according to the principle, for example, I need to create an IGameManager interface and access it this way.
    In other words, we will create a new interface so that the player is dependent on the interface instead of being dependent on the gamemanager class.

    If we do not do it this way and the player remains dependent on the gamemanager class instead of the interface, this is a SOLID violation, am I right?

    Even in a simple level game, even if you use eventbus, scriptable object architecture etc., such class dependency will still become necessary at some point. In such cases, will we have to constantly create interfaces?

    In short, my question is, will I always have to create interfaces for each class or is there another way?
     
  2. CodeSmile

    CodeSmile

    Joined:
    Apr 10, 2014
    Posts:
    5,937
    Please understand that by doing so you are learning a PARADIGM only. This is essentially like following the Bible to the strictest sense. While there is some good advice overall, there's also potential for disaster. Especially when it comes to games. Thou shalt not lay down with they neighbours interface! Isn't that somewhere in there? ;)

    First fail. :D

    A GameManager is ultimately a blob of code or at least runs high risk of becoming the "thing that contains everything and does everything with it". Because if you omit the ubiquitous "manager" from the name, you have a class called "Game" which, if you're making a game, is equivalent to naming it "Everything" or "Project".

    Here's an exercise: come up with a name for that class that describes most precisely what the class does or is supposed to do. It must not include the word "manager" or a replacement for it such as "controller" or "handler".

    If you don't know what the class is supposed to do, figure that out first. Before you have that, you cannot possibly name it.

    Of course you don't need an interface for every class.

    Perhaps you should start your learning exercise in software architecture (and/or how it relates to games respectively game architecture). This then tells you what should depend on what, or whether it needs to at all.

    Small caveat: to understand the concepts of software architecture, you need some intermediate programming experience and you also need to be able to disregard all the "enterprise architecture" stuff that just isn't applicable in or overkill for most games. But that just means: "fail early, fail fast, and don't fail the same way again".

    Though in essence, software architecture is about ensuring one thing above all else: dependencies must only flow in one direction - never criss-cross.

    Architecture design would then tell you where to create Assembly Definitions (.asmdef) to enforce the designed dependency relationships. Because where you have two things depending on one another, you must use an interface on one side.

    Simple example: If one class A depends on (uses) another class B, then the assembly for A must include a dependency to B's assembly. And once that dependency is established, B can no longer add a dependency on A because then you get the "circular dependency" error. It's at that point where you are required to use an interface in B to access an interface whose implementation is in A. Or a common (abstract) base class in another assembly C that both depend on would be a viable alternative.

    When you have that established, it becomes obvious where you need an interface and where you don't.

    You should also dive into data-driven design/development/etc if you're more interested in game programming than OOP paradigms. Data-driven is what Entities (Entity Component Systems) is all about. Specifically for games OOP should be used judiciously as it has some severe performance penalties (virtual methods, data spread all around memory).

    Unfortunately, Unity by itself with its MonoBehaviour scripting forces OOP design onto use that can easily get quite hard to work with. Some devs use dependency injection (VContainer, Zenject) to decouple things, others apply discipline. No matter, it is hard, complex, and often there's ten ways to go about it and eight are just going to make things even harder and more complex. Strictly following SOLID is almost always one of them. ;)
     
    Last edited: Feb 9, 2024
  3. elpatrongames69

    elpatrongames69

    Joined:
    Mar 2, 2023
    Posts:
    99

    The Gamemanager example is ridiculous, yes, you are right, I just wrote it because it came to my mind.

    Thank you for your comment. Actually, I think I normally use interfaces properly, that is, only when necessary.

    I think normally I follow the solid properly. I'm just trying to find the right way around this dependency inversion principle.

    Normally I use it like this
    It can be referenced between its own systems (assemblies) as needed. For example, if necessary, there can be a direct connection between the projectilethrower and projectile classes via getcompenent. If I decide to create more than one projectile type, then I connect the projectilethrower to the IProjectile interface.

    Apart from my own assembly, I generally communicate with eventbus.

    I've heard of ECS, but I haven't had the chance to learn what it is yet.

    And why do these people say dependency injection makes things more difficult? I couldn't understand the reason. When I use a proper framework (other than Zenject), I can add new dependencies very simply and quickly.
     
  4. MelvMay

    MelvMay

    Unity Technologies

    Joined:
    May 24, 2013
    Posts:
    11,473
    Difficulty isn't limited to extending your project. It also includes the ability to easily debug it too for instance.

    Abstractions rarely make debugging easy and like most things, DI comes at other costs because engineering is often a compromise.

    In the end, you judge for you, your team, your project. No one rule to rule them all. ;)
     
  5. CodeSmile

    CodeSmile

    Joined:
    Apr 10, 2014
    Posts:
    5,937
    ... which in itself could become an issue. I can make a Singleton very simply and quickly. That is actually a problem because the simplicity creates too many singletons. ;)
     
    elpatrongames69 likes this.
  6. elpatrongames69

    elpatrongames69

    Joined:
    Mar 2, 2023
    Posts:
    99
    I didn't know about it because I started using it for the first time today, thank you for the information.

    So why exactly and how difficult does it make debugging and is that the only downside?

    Personally, the thing I have the most problems with are dependencies. I can accept that debugging becomes a little more difficult if it helps me take control of this.


    I apologize for asking so many questions, I could not find any source on the internet that provided detailed information on these issues. Your comments are very valuable as I want to learn how real professionals do it.
     
  7. CodeSmile

    CodeSmile

    Joined:
    Apr 10, 2014
    Posts:
    5,937
    In the sense of architectural bounds or in the sense of missing/nullreference exceptions?
     
    elpatrongames69 likes this.
  8. elpatrongames69

    elpatrongames69

    Joined:
    Mar 2, 2023
    Posts:
    99
    Until a few months ago, I was having problems with both, but when I started using event bus and scriptable object architecture, I greatly reduced the architectural limits.

    Let me tell you why I think it could benefit me.
    With Scriptable object architecture, this time I have to manually add the same data to different scripts. For example, I have a scriptable object called ScriptableIntVariable that keeps the score of the game. Since a few scripts reach their scores and change their spawn rates accordingly, I need to assign them from the inspector.
    With dependency injection, I can easily control this from one place and change multiple scripts in one move.

    Apart from this, it seems like a good idea to control the dependencies in general from a single place. It will provide easy modification opportunities such as keeping the game settings on a scriptable object and instantly changing them as easy, medium or hard with dependency injection.

    Apart from this, I was also thinking of using it to create classes for the interface that I mentioned in another topic, but I gave up after you suggested a factory pattern.
     
  9. CodeSmile

    CodeSmile

    Joined:
    Apr 10, 2014
    Posts:
    5,937
    I've used this low-level granularity of the SO architecture before where you only have primitive types with no meaning associated for them. Looking back at this, we had hundreds of config variables for our GUI but only one "instance" of the thing we were configuring - it always makes me wonder whether we would have made things even better by either having a few categories of SO with multiple values that belong together or just a giant blob of data - after all, one could practically change any value from anywhere anyway - it was a configurator after all.

    Just saying this because it may be easier to have an SO called PlayerStats rather than several individual but generic value-type SO variables. The event system still works since it is property-based.

    The original value-type system was invented to support a team of non-technical designers, so they could create any value anywhere for any purpose. But if you don't have that team of non-designers, a larger blob would be a better solution.

    You are most likely to already have a "GameStats" SO instance assigned to whoever needs the score. But sure, you lose the ability of changing an individual GUI elements behaviour by drag & drop, ie if you want it to display health rather than score. Then again, if you use UI Toolkit, that's what the bindings and binding paths are for. Runtime bindings I believe are available in 2022.3 and 2023 adds some more niceties on top.

    I've been pondering this myself. But from a design angle. Not sure if this helps but generally I think it's often overlooked how much awesomeness you can create with just a little editor scripting so that's one example (still a concept but from what I know this should work nicely). ;)

    My use case is that I have many pickup items in the map, they don't move at runtime, can be picked up and respawn and I want a flexible placement system in the editor where I deal with a single "pickup" item rather than Health, Weapon, etc. and on top, I may need to flag each item as "spawn only in difficulty X or game mode Y".

    My idea is to have a "pickup" container game object which references the actual SO (or prefab, or both) that gets placed in the map. The pickup can then do the initial spawning (based on difficulty, mode etc), runtime collision checks, deactivates the model child object on pickup and then starts the respawn timer, activates a spawn effect child (if provided) and then reactivates the actual item - whatever it may be, and this would be fast without using an object pool.

    I'm thinking that this will work fine BUT I will have to do some (not much) editor tooling to best support this exactly because I don't want to select pickup items manually to check them - I want a list of all pickup items in the map and then sort and filter them. For example: show me a list of all pickups of type health, or with missing references, and such. This may actually even work out of the box by using Unity's Search window.

    Likewise, if I were to drag a Health prefab into the scene, it would not create the Health instance but replace it with the pickup instance and assigns the Health prefab to the corresponding field, and then in-editor code would spawn an in-editor representation of the Health item with DontSave flag on the instance.

    I could then easily toggle the view mode of the map: no pickups, sprite pickups, mesh pickups. Bonus for visibility and performance.

    This also prevents instances from being saved in the scene, which can quickly bloat the scene size with identical instances of prefabs where you could simply spawn them at map load in 0.1 ms, or even only when the player is in range, and what not.
     
    elpatrongames69, CodeRonnie and Ryiah like this.
  10. AcidArrow

    AcidArrow

    Joined:
    May 20, 2010
    Posts:
    11,788
    Focus on keeping things simple (which is harder than it sounds) than following some principles that programmers with too much time on their hands thought of.
     
  11. Owen-Reynolds

    Owen-Reynolds

    Joined:
    Feb 15, 2012
    Posts:
    1,998
    The on-line criticisms say that it gets out-of-hand to where you can't even tell what something does. "If n<0 n=0; becomes "test for A and do B" and you've got to track down who plugs in A and B to figure it out. A Unity example, I think, is a script where you have to drag in
    public Rigidbody rb;
    which is always your rigidbody. Adding
    rb=transform.Find("body").GetComponent<RigidBody>(); if(rb==null) ...
    is technically introducing a dependancy (not a code dependancy, but still) but makes it better.
     
    elpatrongames69 likes this.
  12. CodeRonnie

    CodeRonnie

    Joined:
    Oct 2, 2015
    Posts:
    531
    I don't agree with this interpretation that the S.O.L.I.D. principles require you to use the C# keyword: interface. It can get a bit confusing though because the concept of an interface, not the keyword, may refer to a minimized contract between two objects. You can create an interface, functionally, using an abstract class. Or, two objects may interface when they observe each other's events. A third object could bind these two objects together via their events without either needing any kind of reference to another type, let alone an interface. Your quote says that you should depend on abstractions, it doesn't say that you should depend on the interface keyword in the C# programming language.

    So, will you need to create a corresponding interface for every class? No. Sometimes a code base, usually from outside the game industry, may lean in that direction. However, as I heard John Carmack say in a recent interview, I think that's based at least somewhat on a lack of familiarity with the debugger. In some development environments unit tests are how code quality is confirmed. You might be working in a language with dynamic typing, that has no strict type checking on whether object types even match. That code may be running on a remote server in a way that seems impractical for connecting a debugger. In those cases, rather than depending on the compiler to throw warnings and errors, or depending on a debugger to step through the code line-by-line, they will wrap each new class in a series of unit tests. These tests provide the object with various inputs, and assert that the expected outputs and behaviors are observed. To isolate each new class properly for testing, its important to be able to mock the external dependencies of that object. I think of this like pulling one component out of an engine, bringing it into a laboratory for testing and benchmarking, and connecting it to tools that can smoke test that component outside of the engine its ultimately intended for. That's where interfaces come in. If that's how your whole code base is "compiled" and tested, you can see why some may go in the direction of an interface for every class. In game dev, just from what I've seen, we're more likely to not even have any unit tests, and instead rely on the strict rules of the C++ or C# compiler, and the debugger, along with rigorous QA testing and bug reporting.

    The problem with creating a (keyword) interface for every class, in my opinion, is that not every class represents an abstract interface for generalized contracting to other random objects. Sometimes you just need to have more definition than that. If you do have more strictly defined behavior, and a corresponding interface, then the interface (keyword) isn't really an abstract interface (conceptually), in my opinion. It just adds clutter and creates more work. Abstraction should add some kind of value to your development experience, not just add more work for the sake of obsessive compulsion.

    There is an analogy that I have started to use to described interfaces, especially for non-programmers in my life. Imagine you're building a computer. It is composed of various, modular components. It has a motherboard. We can think of the motherboard like a composition root. It is the root component from which we will compose our computer with other components. It has the BIOS that boot straps the system kernel. Everything flows out from that root component, down to the "leaves" of the tree of components that branch out from it. We could also think of ourselves, the builder, and the list of parts we've decided to buy, as the root of composition and its configuration. We are the composer of this system. We have made the decision to buy a certain configuration of parts and bind them together by plugging them in.

    The only way that the modular components in the computer need to coordinate is through their adopted interfaces. Over time, the computer industry as a whole, culturally and collectively, have agreed upon what constitutes these standardized interfaces. Graphics cards and RAM need to support certain PCI standards. CPUs connect to the motherboard with certain agreed upon pin configurations. The power supply has cables that connect via standardized connectors to the other components. Those connectors that bind various modular components into a unique overall configuration are the interfaces.

    Sometimes these standards align, and sometimes they don't. When the contractual specifications of an expected interface don't align for both components, then they don't really share a compatible interface. Sometimes when component connections are mismatched (not necessarily in this computer building example) there are adapters or couplers that can bridge the differences between the expected interfaces on each side. We can also do this in code by just writing a new class that does support the expected interface which serves as a wrapper facade around the concrete component we care about. But, this only works if the concrete component is a match conceptually. It must be capable of performing the same general function.

    Tight coupling is like soldering two components together, or forming them together on the same semiconductor wafer or microchip. If you want to take one thing out, you really have to take everything out together, including anything else in the chain of tightly coupled components. That may not always be a bad thing, but it works in some cases, and is less ideal in others. Sometimes you want to be able to just disconnect a modular component at a relatively abstract boundary.

    Conversely, you wouldn't want your CPU to have countless wires sticking out with loose couplers in the middle of each wire, connecting internal parts of the CPU back to itself. That would just be a mess. That is what creating an interface for every class is like. Things that should be tightly coupled as one module have unhelpful abstractions interfering with their internal coherence.

    Some people often wonder why you should use any interfaces at all. Why not just solder everything in your computer together? Well, even if you never intend to take that computer apart, or swap its components, I think you might at least agree that it's not a terrible paradigm to have present generally, applied strategically, throughout the industry. It's better to have the capability, even if you don't always need it, than to find that you need it, but do not or can not have it.

    The problem with interfaces (keyword) versus interfaces (concept) is that an interface is only as good as it is logically interchangeable and widely adopted. If you create an interface that no other component would ever support, then it's too idiosyncratic and proprietary. You may as well solder it to the other components that are required for it to function. If you make three widgets in your own shop, and they are only ever meant to be used together, you may as well build them into one housing and treat the whole thing as one modular component.

    Deciding where you should or should not use an abstraction or an interface is one of the things that makes engineering an art form.
     
    Last edited: Feb 9, 2024
    elpatrongames69 and SisusCo like this.
  13. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,726
    With every single piece of code you write, ALWAYS ask yourself:

    "When this code breaks, how will I figure out what is going on?"

    And remember that when it breaks you may have 10 billion different uses and use cases splattered all over your program. If simply finding a suitable location to put a breakpoint or dump something to a log requires you to dig for days and days, I submit that whatever programming pattern / strategy you are using is a poor one.
     
    MelvMay and elpatrongames69 like this.
  14. elpatrongames69

    elpatrongames69

    Joined:
    Mar 2, 2023
    Posts:
    99
    Thank you very much everyone
     
  15. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,330
    To answer this question directly: yes, that is a violation. SOLID is a very strict set of principles.

    But in my opinion there's no reason why one should follow SOLID religiously (outside of learning purposes).
    Loose coupling isn't really needed in every single place in the codebase.

    This is very key. IGameManager would be a pretty terrible interface, because it's so clearly just GameManager wearing a fake mustache, and would most likely provide no additional flexibility - except perhaps better unit testability - in which case it's just additional maintenance overhead with no benefit.

    A more practical example would be to replace the dependency to GameManager with more reusable interfaces like ICommand, IObservable<T>, Action, Func<T> etc.

    Creating good, simple, reusable abstractions is an art form. Just slapping a C# interface between two classes, that exactly mirrors all the public members of one of the classes, is very far from this :D
     
    Last edited: Feb 10, 2024
    Lekret, Ryiah, mopthrow and 2 others like this.
  16. TheNullReference

    TheNullReference

    Joined:
    Nov 30, 2018
    Posts:
    268
    Using Inheritance via polymorphism would also qualify as an abstraction, but that comes with other problems that make Interfaces a better choice.

    It's almost impossible to use Unity effectively without violating SOLID. That's just the truth about the framework, it's not capable of adhering to SOLID principles.

    People who want to try need to rely on all sorts of 3rd party support to make it possible, going around the framework this way causes all sorts of pain.

    Unity employees have been pretty clear they're not interested in changing things either.

    It takes time to develop a sense of how much abstraction is required. You want to play nice with the Unity Editor and prioritize building your game instead of fighting the framework. At the same time it would be nice not to drown in bugs 100 hours into a project.

    Rather than worrying so much about abstracting your Player from your Game Manager, it's better to adopt Unity's Game Object Component, composition minded approach. Why do you need a Player Controller, why do you need a Game Manager? Those concept themselves violate the principles of Unity's framework (if they're god objects).

    The only part of SOLID that Unity really encourages is the S (Single Responsibility). That's how you should be writing your components. If a Player Controller is managing your health, death, movement, stats, buffs - it's clearly violating that.

    But we still have the issue of it referencing other concrete components? So the idea then is to make your component behaviours generic enough, that it doesn't really matter if they're not interfaces.

    Instead of having 'PlayerHealth' and 'EnemyHealth', just have 'Health'. Now you don't need an IHealth interface. It's not perfect, but it was what Unity had in mind when creating their engine.

    https://medium.com/@simon.nordon/unity-architecture-gameobject-component-pattern-34a76a9eacfb

    I think it's possible someone could come up with a framework that integrates with Unity while making SOLID possible and painless. I see people are trying.
     
    Last edited: Feb 10, 2024
    elpatrongames69 and SisusCo like this.
  17. meredoth

    meredoth

    Joined:
    Jan 29, 2014
    Posts:
    8
    The important thing about SOLID is not the principles but the problems they try to solve. Knowing the SOLID principles means to be able to recognize those problems in your code base and then deciding if those problems are going to have consequences in the future.

    When you recognize an area in your code that such a problem exists, you have to make a conscious decision if you will use the relevant SOLID principle that solves it. If you think that this problem will negatively affect you in the future (extending the code, refactoring, debugging, adding/removing features etc.) you use SOLID else you don't, the SOLID principles are principles, not rules.

    Ultimately at a high enough level, code architecture is about time investment. You have to decide if the time you spend now, will save time for you and your team in the future. It is not always an easy decision and as with all investments at first you will loose more than what you earn, but experience is the most important factor for these kind of decisions.

    Experience is not transferable, you have to earn it and for this reason my opinion is to try and apply the SOLID principles even if it is wrong to do it at 100%, since you are doing it for learning purposes. Knowing why something is wrong because you can see all the problems it created for your codebase in the future is more important than just knowing it is wrong because a book, video or someone else told you.

    Especially in code architecture, you gain as much experience by doing the wrong thing as by doing the right one.
     
  18. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,330
    In case somebody is interested in where Uncle Bob himself advices drawing the line, you can read some of his thoughts on the matter in his article Design Principles and Design Patterns.


    My interpretation is that DIP is literally about making every single dependency of every single class either an abstract class or interface type.

    However, Bob does concede, that not following this principle is "not so bad" in the rare occasions where a concrete dependency is so tried and true that it is not volatile at all, and inside abstract factories that are responsible for creating instances of other concrete classes.
     
    Last edited: Feb 10, 2024
  19. eisenpony

    eisenpony

    Joined:
    May 8, 2015
    Posts:
    974
    No. There is no SOLID principle that says you must only depend on interfaces.

    In fact, it's much more difficult to violate the Liskov substitution principle if you only have concrete references.

    The only SOLID principle that suffers from referencing concrete implementations is dependency inversion. Your code will feel more flexible if you depend on an interface, but interfaces are not required to implement simple dependency inversion.

    Also, if you are in control of the code, changing a dependency from a concrete type to an interface type is typically very straightforward, assuming the code has not already been twisted to support different behavior in another manner (like, for example, via flag or mode parameters, or by checking values in the ambient context, such as globals or singletons).
     
  20. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,330
    Care to elaborate on what makes you think so?

    (As you probably know) DIP is also called the Hollywood principle: "Don't call us, we'll call you." In the OP's example it seems to me like a low-level module (Player) is directly calling a concrete high level module (GameManager). So to me it seems like it violates DIP with every possible interpretation of the principle I can think of.

    Unless you consider Player and GameManager to exist in the same "level", and interpret DIP as not applying to classes within the same "level"? I'm glancing through the Dependency Inversion Principle chapter of the Agile Software Development book right now, and it looks like the book does leave the principle a bit more open for interpretation, unlike the article I linked earlier that seemed to more definitely state that the principle completely disallows any classes having dependencies to any other concrete classes.
     
    eisenpony likes this.
  21. eisenpony

    eisenpony

    Joined:
    May 8, 2015
    Posts:
    974
    I realize I'm out on a limb, taking an interpretation against Bob Martin.

    The reason I say this doesn't violate SOLID is because I consider the flow of control the critical aspect of the DIP rather than the flow of references. If both of these classes receive their references via dependency injection, inversion of control is satisfied.

    However, since we are talking about SOLID, I guess it's fair to use Bob Martin's definition.
    In that case, Player directly referencing GameManager, or visa versa are both violations. The strictest interpretation means we must always reference abstractions, both upwards and downwards.

    The question about whether the reference is "up" or "down" is a bit subjective. I didn't pay much attention to that detail, though, in retrospect, I see the class names suggest this relationship. Either way, Bob's definition doesn't seem to care.

    I find this interesting because of the marginal differences between approaches. In a scenario in which GameManager and Player require sending bidirectional messages, the difference between having that coupling via interfaces or via direct reference doesn't seem like the critical issue.

    The increased coupling of the bidrectional messages is the main problem and Bob's SOLID is mostly silent on that issue.
     
    SisusCo likes this.