Don't try to do the entire picture at once in full details, you just go mad in overload of details. Instead, just start with something simple, and gradually extend.
For pong:
1. Display black window (SDL init, opening a window, setting title bar, setting background, detect 'quit', close down SDL)
2. Display black window with white ball (ball object, draw game objects in window)
3. Display black window with moving white ball (ball speed and direction, position updates, frame rate limiting)
4. Display black window with moving bouncing ball (detect edges, reverse direction)
5. Add center white line
6. Add score board
7. Detect ball leaving the window at the left or the right
8. Change score and update the displayed score.
This is just one option, there are many other ways to do this.
Obviously, gradually extending functionality has the problem that at some point you'll find it doesn't fit. Worse, you made the wrong turn several steps back. You'll have to fix this before you can extend further.
As for the interaction between objects, the main thing is to decide who is going to make the final call. Usually that follows from what object is responsible for it. The other objects then have to provide information to the decision maker. Also, the decision maker must have means to tell others about the result.
For example, ball and window, in context of a bounce at the top (bounce at the bottom is probably similar enough to also handle here, but I don't want to discuss that here):
1. Ball is responsible for staying inside the window. (Assumption: ball knows its own x/y position and x/y speed).
The ball thus needs to know what window it must stay inside, and it must know where the upper border of that window is.
class Ball {
public:
int x, y; // position
int vx, vy; // speed
};
class Window {
public:
int top; // y position of top border
};
Ball gets the window, and can access its top. It doesn't need to notify anyone of its decision.
void Ball::StayBelowTop(Window &w) {
if (y < w.top && vy < 0) { // Oops, I must correct my course!
vy = -vy;
}
}
2. Window must ensure ball stays inside.
void Window::KeepBallBelowTop(Ball &b) {
if (b.y < top && b.vy < 0) b.vy = -b.vy; // Changes ball vy directly without notifying the ball
}
void Window::KeepBallBelowTop(Ball &b) {
if (b.y < top && b.vy < 0) b.RequestReverseVy(); // Ask ball to reverse it vertical speed.
}
void Window::KeepBallBelowTop(Ball &b) {
if (b.y < top && b.vy < 0) b.ReverseVy(); // Tell ball to reverse it vertical speed.
}
Three different solutions. The first one directly interferes with the b.vy variable, which implies you have decided that vy of the ball may be changed by others.
In the second solution, the window gently asks "please change your vy" (and hopefully the ball complies). In the third solution, the window just tells "reverse your vy!!".
The point I am trying to make is that the code follows the decisions that you make at a higher level wrt object responsibilities, and ways of telling other objects about decisions. This is why names of objects and methods is so important. Both Ball::RequestReverseVy, and Ball::ReverseVy may just do "vy = -vy;", the code line doesn't say if there was room for negotiation. (Unlikely to be useful for pong, though.)
So decide about the object responsible, decide how the information it needs is made available to it, code the decision making process, and decide how to let other objects know of its decision.
I don't really agree with any of the above solutions though, it all feels like over-engineering to me. I'd do
const int TOP = 0;
void Ball::StayBelowTop() {
if (y < TOP && vy < 0) vy = -vy;
}
Your ball is never going to be used in more than 1 window, and the top is never ever going to move (ie even the TOP constant is already overkill in a sense!). There is no need for a Window class, just directly code the boundary check.
While it looks like I am violating the OO ideas of object responsibilities I just explained here (Who is responsible for defining "inside"??? I have no Window to stay inside!), I actually still follow the rules above, I just made a different high level decision.
I decided at the highest level that TOP is the top y coordinate, and it is going to be 0 always. This is global constant information freely available to all objects. Furthermore I decided that the ball must make sure by itself to stay below the top. It doesn't need to notify anyone of its course change. This then leads to the implementation I just showed.
It's easy to get carried away by OO and object responsibilities, and how to notify others, leading to complicated patterns of object interactions. Sometimes you need it, but that doesn't imply you always have to do it in that way. Making a different decision at a higher level can cause dramatic simplifications further down the line.