Hey guys, so I’m working on a project that is getting pretty unwieldy. Clumsy bugs are starting to appear, and from past experience this has usually been a good indicator that I’m on a bad path here.
So here’s what’s going on…
I’m getting my teeth stuck into OOP And I have classes inside of classes inside of classes.
For example, lets imagine a game of chess. I have a Game class that manages the logic, while classes like the Board holds the data-structures. Board is actually a 2D array, each having a Space which may or may not contain a Piece.
Now I’m running into problems like If a human makes a move, which class should actually be responsible for issuing the move command. The Board class, or the Game class. Who does the error checking? The Space class? Right now, Game simply forwards the move request to Board, which asks Space a bunch of questions, and then decides whether or not the move is legal. But isn’t that logic, so therefore better to happen the Game class? And should the Game class speak directly to the spaces, or do I have to make a bunch of getters down the chain of command?
You see the kinds of questions that are popping up in my head right now?
Maybe this is a familiar struggle to any of you? Any advice, or pointers to good resources? Thank you!
Been there, done that… and I keep doing it, lol… I think it’s common to discover your architecture isn’t quite right as you move forward with implementation. While there is no ‘right’ answer, I think the names of your classes indicate expected responsibility; Board and Space are containers, where Game is the set of rules which run the game. So, yes, the Game should handle game play…
Unit Tests are a way to galvanize a UML design. I would be lost without testing since I use it as the reality of the current code functionality/stability.
Also testing allows you to see dependencies(not quite seen in the UML design) and verify app scope very easily.
OOP has done great things for complex applications and polymorphism is top.
So from my experience, OOP is like a training program, takes years to master and it’s not cut and dry by a long shot. As most books point out, it’s distilled knowledge from massive failures of time and resources.
This is just my experience and also, sometimes OOP can get in the way, that is where time and experience show you when to wield and when to just write the code.
In the end, a design ultimately gives you the power to contain any idea, it’s just creating and refining the design and not coding that gets some.
The most important principle that will help to simplify your designs and make them more reusable is to avoid cyclic dependencies. In the game example, this would mean that Board can know about Piece, or Piece can know about Board, but they can never both know about one another. It probably makes most sense to have Board own some Pieces, which immediately clarifies the design - any function that has to know about both Boards and Pieces needs to either be a member function of Board, or a free function which can depend on both Board and Piece.
An unrelated point: the OOP ‘everything must be a class or a member function’ approach often (usually?) doesn’t lead to well factored designs. Adding every potentially-useful function as a member function leads to bloated, overly-coupled classes. It’s generally a good idea to put the absolute bare minimum functionality into a class (just enough to maintain the class invariants) and to implement everything else as non-member functions.
After many years in the gaming industry, I’ve found OOP a great principle, and guide. Sometimes an idea will fall through into complex functionality. But the general OOP principle always sits on top, or underneath of course.
It’s a big subject that I’m clearly incapable of explaining in a forum post off the top of my head.
But, taking your chess idea, I would have a general base ‘piece’ class that can be any type (rook/bishop/king) and on the top have a ‘piece management’ class, that work their way towards each other during development. Any top game loop class should aim to be as short as possible, with just simple calls to gameplay logic, keyboard, timer checks and general Quit/Start game logic. In practice, especially with teams of people, there can be a natural tendency to bloat some classes, and with some common sense, the team leader should see that coming and tidy up before it gets out of hand.
But there’s a thing called time and money, and it’ll get in your way - a lot. I’ve found general enthusiasm got in my way of proper OOP sometimes, not so much now. But it can cause headaches and messy code later on if not periodically checked.
In the end, creative processes are very iterative and ideas develop and expand over time. Keeping tabs on the hierarchy is an important process and part of development. Not too mention your sanity!
I haven’t watched Brian Will’s video, and I am curious now. But I think throwing OOP overboard is not a wise choice. Like others already stated in this thread, OOP is a good way to limit the scope of bugs and side-effects. That’s why it always seems like overkill in the beginning, because you deal with a few fragments just to get an idea.
When OOP was introduced in languages like Smalltalk (and later java), they made a point that everything is an object. And that is something we might want to revise nowadays, like @reuk pointed out. This sole reign of objects led to empty objects, having meaningless names like MathHelper and such.
Repetetive tasks should not be pinned to an object. The STL has the separation in containers and std::algorithm, and I think that is a good way to go about things. Specific behaviour in an object, try to be as general as possible, which means have free functions for general tasks.
It’s great to know I’m not alone and this doesn’t (have to) mean I’m a terrible programmer, in fact, perhaps what you’re also inadvertently suggesting is that it should be common. That way you’re frequently doing the necessary house cleaning that allows the project to grow.
I have know idea what UML or unit tests really is. I thought that was a beta testing / bug finding thing.
Thank you for the really concrete advice. The way I have it organised is that Game has a Board and Pieces class. Game does the logic for the game (i.e. move), and checks with Board if a proposed move is legal. (i.e. if (!board.space.contains) board.move(piece, position);.
I think that seems right.
Sorry, could you give me a concrete example, I can’t actually grasp what you’re saying
Welp. I’ll add this to the list of videos to watch!
Sorry, what does the piece management do? And does that need to know about the positions of the other pieces. Isn’t knowledge piece placement itself a data-structure and therefore kind of what I’m going for with the Board class? The problem I’m having with my Game class, is that a lot of it’s work is just passing on functions calls to different classes (liike game.move basically does nothing more than call board.move).
@daniel, I am super happy to let the big boys battle this one out. There’s a lot of hyperbole on the internet surrounding programming paradigms, but the fact is that OOP has proven itself over time.
I apologise if I miss something out, but it can be a object called ‘player.’ A Rook can inherit a piece, but it doesn’t have to. It only knows so much, like how it moves and what it looks like. A player object should know what the board looks like and how to play, where the pieces are etc. Above that you can have a general game object that can own and control choices like human or AI players, names and the like.
I won’t go on, fundamentally it should be flexible, a design can change or be added to easily. The AI code could be part of each player or a static library that follows the whole match for example. I always find an OOP approach helps me understand what I’m trying to achieve, even if I don’t follow it like a religion. It’s a good start, and it really helps future projects with potential reusable code, like product registration functions for example.
I think this is the kind of question that is best answered with links to books, and do note I have read all the books I recommend.
I definitely disagree that you should dismiss OOP, and I am always surprised when that comes up. With that said, much of what is discussed in these books applies more generally to programming, beyond OOP.
I have those at the office, and am working from home due to corona, but I remember that I read the Clean XYZ series first, then Code Complete 2, and Code Complete covered all the ground of the Clean Code and Architecture books.
Most of the times I write a static method, it ends up as a free function later. Sometimes it makes sense to keep it in the class though, when it’s something too specific that wouldn’t make sense in any other context.