31

A group of friends and I have been working on a project for the past little while, and we wanted to invent a nice OOP way of representing a scenario specific to our product. Basically, we're working on a Touhou-style bullet hell game, and we wanted to make a system where we could easily represent any possible behaviour of bullet we could dream up.

So that's exactly what we did; we made a really elegant architecture that allowed us to section off the behaviour of a bullet into different components that could be attached to bullet instances at will, kind of like Unity's component system. It worked nicely, it was easily extensible, it was flexible and covered all our bases, but there was a slight problem.

Our application also involves a heavy amount of procedural generation, namely we procedurally generate the behaviours of the bullets. Why is this a problem? Well, our OOP solution to representing bullet behaviour, while elegant, is a little complicated to work with without a human. Humans are smart enough to think of solutions to problems that are both logical and clever. Procedural generation algorithms aren't that smart yet, and we've found it difficult to implement an AI that uses our OOP architecture to its fullest potential. Admittedly, that is a flaw of the architecture is that it's not intuitive in all situations.

So to remedy this problem, we basically shoved all the behaviours offered by different components into the bullet class, so that everything we could ever imagine is offered directly in each bullet instance as opposed to in other associated component instances. This makes our procedural generation algorithms a little easier to work with, but now our bullet class is a huge god object. It is easily the largest class in the program so far with more than five times as much code as anything else. It's a bit of a pain to maintain as well.

Is it okay that one of our classes turned into a god object, just to make it easier to work with another problem? In general, is it okay to have code smells in your code if it admits an easier solution to a different problem?

6
  • 1
    Well...this is kinda hard to answer. In my opinion NO. Especially if you work together with other people. If you work alone you can do whatever you want. But in general it depends on who you work with. Apr 13, 2015 at 5:31
  • 13
    "If it's stupid, and it works; it's not stupid." That being said, you do have to make the consideration that even if it works exactly as you want it to, whether or not you're not making future coding on that god class near impossible because of how you decided to fix it.
    – Flater
    Apr 13, 2015 at 8:57
  • 8
    It's better to smell and know you stink, then to smell only other people notice. :)
    – Reactgular
    Apr 13, 2015 at 13:03
  • 1
    Sounds like you need an adapter class that provides a non-OO interface to your procedural code, so you can keep the rest of your code clean(er).
    – nikie
    Apr 14, 2015 at 8:36
  • 1
    It sounds like you build a wonderful system and decided to blow it all to hell now it doesn't work as expected on one feature. Is that really what you want? Be proud of your work!
    – Mast
    Apr 14, 2015 at 9:31

6 Answers 6

74

When building real-world programs, there is often a trade-off between staying pragmatic on one hand, and staying 100% clean on the other. If staying clean prohibits you to ship your product in time, then you are better off with a little bit of duct-tape to get the d***d thing out of the door.

Said that, your description sounds different - it sounds you are not going to add a little bit of duct tape, it sounds like you are going to ruin your whole architecture because you did not look long and hard enough for a better solution. So instead of seeking for someone here on PSE who gives you a blessing on this, you might better ask a different question where you describe some details of the problems you have in-depth, and look if someone offers you an idea which avoids the god-class approach.

Maybe the bullet class can be designed to be facade to a bunch of other classes, so the bullet class becomes smaller. Maybe the strategy pattern can help so the bullet can delegate different behaviours to different strategy objects. Maybe you need just an adapter between your bullet component and your procedural generator. But honestly, without knowing more details of your system, one can only guess around.

2
  • 6
    Just to reaffirm this answer, I was going to write something with the very same two recommendations. Regarding the last paragraph, what OP describes seems to be a code smell in itself indicating that another level of indirection is needed. Whether that's a better factory class, facades, or even a full blown DSL that the AI can generate for is left up to the OP.
    – Daniel B
    Apr 13, 2015 at 7:08
  • 2
    Good suggestions on Facade / Strategy to split up code into smaller chunks. Though without knowing more details it's hard to know what to suggest.
    – user949300
    Apr 13, 2015 at 16:56
25

Interesting question. I am a bit biased though due to my previous experiences, which prompts me to answer with No.

Short answer: We never stop learning. When you hit a wall like that, it is a chance to improve your architectural/design skills, not an excuse to add code smells.

The longer version is that I have been asked similar questions a lot of times in my projects at work now, but inevitably, we ended up discussing the issue in much more depth, then the original questioner gave it credit for. You want to include mentalities like root cause analysis with that.

I found the 5 Whys particularly helpful. Your explanation here reads just like the first why:

  • "We have this god class now" Why?
  • "Because it simplifies the procedural generation algorithm"

What is it exactly that is simplified? And why is that so? Keep on going along that line and what typically happens is that you start to recognize way more fundamental problems, but at the same time, they allow you more fundamental solutions just as well.

Long story short, after thinking more deeply about the problem at hand and in particular its causes, in my experience the original question ended up being void and utterly uninteresting to all participants. If you have doubts, challenge them and either they will be gone, or you'll know what you have to do. Mind you, it is a good sign in my opinion to have these doubts about code/design. Others call it a gut feeling, but in order to challenge these problems, you must first recognize them. Since you made that first step, congratulations! Now go down the rabbit hole ...

9

Having a god class like this is never desirable, as it does not only mean that your bullets are now monolithic objects, but the same goes for your procedural generation algorithm as well.

The first step would have been to analyze, why exactly your AI did have so much trouble with dealing with the complexity of your pattern?

Did you, by chance, tried to turn your AI into a god class object as well, fully aware about the semantics of every single possible property? If you did, that's where the problem originated.

The solution would then not have been to integrate all strategies into the bullet class itself, but instead offloading the hinting for the AI from the AI core to the strategy implementations themselves.

That would have given you the flexibility you originally desired, including the option to extend the system by new strategies as you wish without the fear of running into side effects with legacy behaviors.

Instead you have now all the problems associated with god objects: No only your god class itself is a hard to understand, hard to debug monolith, but the same goes also for all other components accessing such a god class. Due to the lack of abstraction, your AI is now bound to turn into an abomination of similar complexity as it has to be aware of all the redundant, individual properties now.

Even right now, you are already experiencing problems with maintenance. These problems are getting worse, especially as soon as you loose the team members who still have a cognitive model of how that class is supposed to work.

So far every single project I encountered which used such god classes or god functions either was completely rewritten from scratch or ceased, no exceptions.

2
  • One thing that's often missing in discussions about how big or complex classes should be is a distinction between "how much should code which holds a reference of type X be able to do with it" versus "much much logic should be in the class file for X". If a client would likely have collections of objects that support a variety of capabilities (e.g. "fnorble"), and often find itself wanting to ask all members of a collection to e.g. "fnorble if possible, else do nothing", then having an interface which combines many such methods may be much more helpful than trying to split the interface.
    – supercat
    Apr 13, 2015 at 19:51
  • Given such an interface, it's possible to have code encapsulate objects with any combination of abilities, without having to handle different combinations separately. Segregating the interfaces would make it necessary to write many different proxy classes, since proxies for a type that supports method X couldn't be used to wrap objects that can't X, and proxies that can wrap objects that can't X wouldn't be able to expose method X even if it was wrapping an object that could support it.
    – supercat
    Apr 13, 2015 at 19:56
8

All bad code since the dawn of time has a story behind its evolution that makes it look reasonable step by step. Yours is no exception. Coders learn by coding. There are aspects of your problem you could not have foreseen that seem obvious now. There are decisions you made that were entirely reasonable incrementally, but led your architecture in the wrong direction overall. You need to identify and reexamine those decisions.

Ask around your office if people have any ideas for how to fix it. Most places I've worked, around 10-20% of the programmers have a real knack for that sort of thing, and have just been waiting for the chance. Figure out who those people are. Often, it's your new hires who aren't historically invested in the current architecture who can most easily see alternatives. Pair them with one of your veterans and you might be amazed what they come up with together.

2

In some cases this is definitely acceptable. However, I find it hard to believe there is no good solution using both procedural generation and your nice attached/component based behavior architecture. If all behaviors where just pulled into the bullet class there is no functional difference between the god object, and neat architectured version. What made it so hard for your procedural generation algorithms to use?

I think a new question (maybe here, or on gamedev.stackexchange.com?), where you outline what problems you had with your architecture in combination with proc. gen., would be really interesting. Let us know here if too if you do make a new question!

2
  • 3
    Can you provide any example for a situation where having a god class would be both acceptable and desirable while this class is not just an automated compilation of individual sources?
    – Ext3h
    Apr 13, 2015 at 12:45
  • With acceptable I was referring to "Is it okay to have code smells if it admits an easier solution to another problem?" God classes are generally easily solvable by using composition. But yes there are certain code smells that are definitely not worth always 100% getting rid of. In the end some pragmatism is required to get work done :). (This does not mean I think you should throw out best practices on a whim. I believe that most software houses would be better of following best practices more strictly).
    – Roy T.
    Apr 13, 2015 at 13:01
0

For inspiration, you might want to have a look at functional programming, or more precisely, tight-tailored types. The idea being that things don't work are impossible to represent in the type system you have available. For example, say you have a gun that has the components "shoot bullets" and "hold bullets". If they are two separate components, there's configurations that don't make sense - you can have a gun that shoots bullets but doesn't have any in its storage, or a gun that stores bullets but doesn't fire them.

At this level of complexity, you're thinking "right, a human will figure out this is useless, and avoid the impossible combinations". A better way might be to make it outright impossible to do this. For example, the component for "shoot bullets" might require a reference to the "hold bullets" component (or just hold it as part of itself outright, although that has it's own issues).

While your examples might be a lot more complicated, the key is still limiting the possible combinations, adding constraints. In a way, if it's too complicated for procedural generation to get right, it's probably too complicated anyway. Think about all the ways you're constraining yourself when designing the weapons - do you tell yourself "right, this gun already has a flamethrower, there's no point in adding a grenade launcher"? That's something you can incorporate in your heuristics. You might want to use a probability mechanism that's a bit more complicated than just giving a fixed chance of having each component - you might have components that tend to exclude each other, or components that tend to work well together. So you could modify the probabilities of selecting a new component based on the components you already have.

And last, consider whether it's actually a big enough problem. Is it ruining the fun of the game? Is the result guns that are useless or overpowered? How much? Is one out of 10 nonsensical? Games like Borderlands (with procedurally generated weapon effects) often ignore nonsensical effect combinations - what's the point of having a shotgun with a 16x scope? And yet that happens very often in Borderlands. It's just played for laughs, rather than being considered a failure of the generation mechanisms :)

Not the answer you're looking for? Browse other questions tagged or ask your own question.