Java – Prevent circular dependencies without introducing intermediary library

design-patternsjavaobject-oriented-design

TL;DR Below

I'm working on a game server (in Java, but that part is less important), and have decided to split up the server logic from the engine logic; in part because they're in two different logical domains. The server will interact directly with the client, and handle the networking aspect of things, while the engine will deal with in-game events. I would like these to be as separate as possible, so that the engine doesn't deal with packets and networking, and the server doesn't deal with player movement, or game events. However, these two will have to remain coupled because, although they are in different domains, they are part of the same process.

For example, when the user launches their client and connects to the server, the server must send a message to the engine to create a new game session. The engine then initializes the world, adds the player to it, and registers the proper in-game event listeners. At this point, the engine tells the server that the world is ready. The server then sends a message client to load resources (images, landscape data, text, etc), registers packet handlers, and everything is ready to be played.

We also have to take it the other way. If the player disconnects their client, or logs out, then the server is the first to know of this state change. The server session may stay active, in case it's just a slight network hiccup, but eventually, it must tell the game engine to do any cleanup.

There can be many implementations of the engine and server. For example, they can be running in the same JVM instance, they could be on different instances, using socket channels to communicate between eachother, or we could have the engine on another server, using something like a REST service to communicate messages between them.

TL;DR

What sort of design patterns or strategies can I use to send messages between two coupled components, that should remain as separate as possible, without introducing an intermediary library?

Best Answer

You could define interfaces, ans use their implementing classes as listeners. Look at the Observer Design Pattern.

Interfaces also force you to think in term of "services", so it would be relatively easy to implement several versions of your server and engine, and always use the interfaces for abstracting their implementation details.

For instance, imagine you want to implement a chess game. Create the following interfaces (given names are for clarity purpose):

  • ServerInterface: it represents a server for a client. Must contain methods such as addClient(ClientInterface), which allows you the connect a new client; move(Piece,Position), for displacing a piece on the board; addWatcher(WatcherInterface); etc. A LocalServer is probably the simplest implementation of this interface, because any call to the server will directly correspond to a call to a server method. A TCPServer will transparently forward your call through TCP, so that from the client point of view there will be no behavioral differences with a local server.
  • ClientInterface: it represents a client for a server. Must contain methods such as setServer(ServerInterface) for defining the server that must be called when the client plays; clientLeft(ClientInterface) for informing the client an other client left the game; etc. Again, a LocalClient implementing this interface will simply take into account received messages, while TCPClient will forward messages trough TCP and MailClient will send an e-mail to the client and will wait for an answer (if required) from the same medium.
  • EngineInterface: it represents all the means of communication with the engine. Typically, you will have methods for asking if a move is acceptable or not, etc. You can imagine a LocalEngine that implements your own rules, or a DistantEngine that uses a REST API on the web for subcontracting this part of your system.
  • WatcherInterface: the classes implementing this interface are only interested in observing the game. Your server will communicate any move to such classes. A Logger will react to the messages sent by writing the moves into a file. A ScreenWatcher will graphically represent the current state of the game. Etc.

With this solution, your components are highly decoupled, because each part only knows it can be connected to zero, one or many other parts, without specifying how these parts actually behave. Each part only rely on a small set of interfaces that are easy to maintain.

In your code, you will need a bit of glue for instanciating the concrete classes. For instance, in your client, the gamer will select a TCP game, then enter an IP address, and click on "join". The code associated to the GUI has therefore to create a TCPServer and to present it as a ServerInterface to the client associated to the GUI. The client should never know it is connected to to a TCPServer, so there will be nothing specific to this particular implementation used by the client. In the server side, your TCP application will be listen for incoming connexions. When a connexion starts, the application creates a TCPClient and presents it to the existing server as a ClientInterface, which will deal with it ignoring it is a TCP client.

Related Topic