lamas
lamas

Reputation: 4598

C++ and modularity: Where am I supposed to draw the line?

According to widely spread advice, I should watch out to keep my larger software projects as modular as possible. There are of course various ways to achieve this, but I think that there is no way around to using more or less many interface classes.

Take, as an example the development of a 2D game engine in C++.

Now, one could of course achieve a very modular system by using interfaces for practically everything: From the renderer (Interface Renderer Class -> Dummy, OpenGL, DirectX, SDL, etc.), over audio to input management.

Then, there is the option of making extensive usage of messaging systems, for example. But logically these again come with a high price in performance.

How am I supposed to build a working engine like this?

I don't want to lower the limits for my engine in terms of performance (maximum viable amount of entities, particles, and so on) just to have a perfectly modular system working in the background. This matters because I'd also like to target mobile platforms where CPU power and memory are limited.

Having an interface for the renderer class, for example, would involve virtual function calls for time-critical drawing operations. This alone would slow the engine down by a fair amount.

And here come my main questions:

Upvotes: 3

Views: 1755

Answers (4)

Stack Overflow is garbage
Stack Overflow is garbage

Reputation: 247969

There are many ways to keep your code modular without using a single "interface" class.

  • You already mentioned message passing
  • then there's plain old callbacks. If an object needs to be able to trigger some event elsewhere in your system, give it a callback function it can invoke to trigger that event. It doesn't need to know anything about the rest of your architecture then
  • and using templates and static polymorphism, you can achieve most of the same goals as you would with interface classes -- but with zero performance overhead. (For example, template your game engine so that a Direct3D or OpenGL-based renderer can be picked at compile-time)

Moreover, modularity is tricky, and it's not something you get just by hiding everything behind an interface. For it to be modular, whatever that interface implements should be replaceable. You have to have a strategy for replacing one implementation with another. And it has to be possible to create multiple different implementations.

And if you just blindly hide everything behind interfaces, your code will not be modular at all. Replacing any implementation of anything will be such a huge pain, because of the countless layers of interfaces you have to dig through to do so. You'll have to go through hundreds of places in the code and make sure that the right implementation is picked and instantiated and passed through. And your interfaces will be so general that they can't express the functionality you need, or so specific that no other implementation can be made.

If you want a cheesy analogy, bricks are modular. A brick can be easily taken out and replaced with another. But you can also grind them up into tiny particles of baked clay. Is that more modular? You've certainly created more, and smaller "modules". But the only effect is to make it much much harder to replace any given component. I can no longer just pick up one tangible brick, throw it away, and replace it with something else that's brick-sized. Instead, I have to go through thousands of small particles, finding an appropriate replacement for each. And because the replaced component is no longer surrounded by a couple of bricks in the larger structure, but with tens or hundreds of thousands of particles, a ridiculous number of other "modules" are now affected because I swapped out their neighbors that they interfaced with.

Grinding everything up into finer and smaller bits doesn't make anything more modular. It just removes all the structure from your application. The way to write modular software is to actually think and determine which components are so logically isolated and distinct that they can be replaced without affecting the rest of the application. And then write the application, and the component, to maintain this isolation.

Upvotes: 3

SuperElectric
SuperElectric

Reputation: 18874

Prototype first, then let the interface boundaries emerge.

Preemptive interface design can make coding a drag

Trying to engineer abstraction barriers before you code can be tricky, as you run two risks. One is that you'll inevitably draw some abstraction barriers in the wrong places, and as you start writing working code (as opposed to interface code), you find that your interfaces serve your problem poorly, despite sounding good when described in natural language. The other problem is that it makes coding more of a drag, since you have to juggle two concerns in your head instead of one: writing working code for a problem that you don't completely understand yet, and adhering to an interface that may turn out to be bad.

Interface boundaries emerge from working code.

I am of course not saying that interfaces are bad, but that they're hard to design correctly without having written working code first. Once you have a working program, it becomes obvious which parts should be different instantiations of the same virtual function, which functions need to share resources (and therefore should be put in the same class), etc.

Prototype, then draw only the interface boundaries you need.

I therefore agree with @jdv-Jan de Vaan's suggestion that the first thing to do is to blast out the shortest readable program that works. (This is different from the shortest program that works. There is of course some minimal amount of interface design even at the very beginning.) My addition is to say interface design comes after that. That is, once you have the simple-as-possible code, you can refactor it into interfaces to make it even shorter and more readable. If you want interfaces for portability, I wouldn't start that until you actually have code for two or more platforms. Then the interface boundaries will emerge in a natural (and testable) manner, as it becomes clear which functions can be used as-is for both, and which need to have multiple implementations hidden behind interfaces.

Upvotes: 2

Jerry Coffin
Jerry Coffin

Reputation: 490108

Keep in mind that virtual function calls are intended primarily to deal with a collection of (pointers/references to) objects that aren't necessarily all of the same actual type.

You certainly should not even contemplate something like squares being drawn via OpenGL, but circles via DirectX, or anything on that order. It's perfectly reasonable to handle modularity at this level via templates or even file selection when you build your code, but virtual functions for this situation make no real sense.

That probably brings up the relevant piece of advice for getting performance out of C++: use templates for flexibility and modularity while still retaining maximum performance. One of the main reasons templates are so popular is that they give you modularity without sacrificing performance. The CRTP is particularly relevant to code that may initially seem like it needs virtual functions.

As far as where to draw the line between consistency and performance, there really is no one answer -- it depends heavily on how much performance you need. For your situation (3D game engine for mobile devices) performance is clearly a lot more critical than for many (most) other situations.

Upvotes: 1

user180326
user180326

Reputation:

I don't agree with this advice(or maybe your interpretation). "As modular as possible": Where should this end? Are you going to write a virtual interface for 3d vectors, so you can switch implementations? I don't thinks so, but it would be "as modular as possible".

If you are selling a game engine, modularization can help to keep your build times lower, to reduce the amount of header files needed by your prospective clients, and the ability to switch implementations for a particular problem domain (such as directx vs opengl). It can also help to make your code maintaible by partitioning it. But in that case you're not required to decouple the modules with interfaces.

My advice is to always write the shortest readable program that works. If you have can write 20 lines of code that solve some problem locally, or scatter the function over five different classes, the latter would be more modular, but usually the result is less reliable, less readable and less maintainable.

Upvotes: 1

Related Questions