Multiple Inheritance / Diamond Problem

I have an in-game object, say a car, that needs to run code both on the server and client, so it’s split into three classes, one that contains all the functions needed by the client (CarClient), one that containers all the functions needed by the server (CarServer), and one which contains functions needed by both (CarBoth). CarBoth doesn’t inherit from anything, CarServer and CarClient inherit from CarBoth. So far so good.

Ok, next I need to create a truck, which inherits from the car. So I create TruckBoth, which inherits from CarBoth, and TruckServer which inherits from TruckBoth and CarServer - wait a minute how does that work? Which classes constructor should I use? Which methods take priority when being overridden? I’ve hit the diamond problem, and have no idea what to do.

What do other developers do? I can’t be the first person whose needed different client and server versions of classes which inherit from other classes with client and server versions.

Well mostly we keep our scripts centralized for a given object. Meaning their isn’t 3 classes for one vehicle. In all honesty OOP is nice and all but there’s a time and place to use it. And this use case IMO. Isn’t one of them. Just do what the normies do. Control the car locally, handle server stuff on the server. To do this you don’t really need classes. You just need a centralized script to listen out for the events the client will call.

1 Like

I am using just one module script called DefaultCar and putting everything in it, then putting it to ReplicatedStorage. Can you send your explorer as a screenshot?

In Lua, you write your own OOP. Lua isn’t an OOP language by default, and there is not standard set of OOP features – you have to make a lot of decisions. You’ll have the best experience writing code if your OOP framework is built for and takes full advantage of the Roblox platform, rather than a one-size-fits-all “standard” approach.

It sounds like your current OOP framework is a one-size-fits-all approach. You could certainly solve your issue by making arbitrary decisions about inheritance ordering, but in my opinion there’s a better approach:

  • Build your OOP framework around Roblox’s client/server relationship – allow defining a single class (Car) with different members depending on its environment, without using inheritance.
  • “Compile” the server or client version of each class before doing inheritance. This means “Car” is solidly “shared code + environment-specific code” before inheritance is handled. You can keep environment-specific code inside submodules to the main class module.
  • Take advantage of Roblox’s replication system and store all modules in ServerStorage. When the game starts, have your primary server script copy all shared and client modules to ReplicatedStorage. This makes navigating your code in the explorer tree easier:
    image
  • Don’t stick too strictly to OOP. If you stick to super strict OOP then things will get harder rather than easier, because there’s no built-in tools to navigate and work with OOP-style code.

This accomplishes the following goals:

  • Solves your inheritance problem
  • Makes writing OOP-style Roblox objects easier due to the server/client split being native to your OOP framework, rather than tacked on through inheritance.
  • Makes navigating server/client split code easier
  • Keeps server code only on the server

I’ve written systems that stick strictly to OOP, stick strictly to code split with code in entirely different locations, and stick strictly to a one-size-fits-all OOP style, and eventually it was a pain to use, so I highly suggest designing your OOP framework to avoid those issues!

13 Likes

A post was merged into an existing topic: Off-topic and bump posts

If you really hate standards, you can use runService:IsClient() or :IsServer() and dynamically include methods based on the environment.

But you probably shouldn’t do that. But you could. But don’t. I’ve done it. I try to avoid it. Do what @Corecii said instead.

2 Likes

Maybe it’s just your naming, but it seems like part of the confusion here is that Car and Truck are really siblings, and you’re trying to make them both siblings and parent-child. A truck isn’t logically a type of car, normally you’d have a common abstract ancestor like MotorVehicle, wherein all their common base functionality and properties would reside. Then you don’t have multiple inheritance. And when going from general to specific, you normally don’t have multiple inheritance. Multiple inheritance would make sense if you had a base motor vehicle class, and a base airplane class, and you wanted something like a flying car that really could sensibly use a lot of code from each.

You get to decide what overrides what, by what you replace in the class metatable (if you’re implementing it that way).

And of course, it’s always worth taking a few minutes to think about what the composition solution looks like.

3 Likes

You could try an interface approach. An interface defines the behaviors or operations of a type. Any types that have these behaviors automatically implement that interface.

For example, let’s define some basic interfaces. To keep it simple, we’ll say that each behavior is an indexable method on a value:

interface Car:
	Move()
	Steer()

interface Server:
	SendToClient()
	ReceiveFromClient()

interface Client:
	SendToServer()
	ReceiveFromServer()

interface Truck:
	Move()
	Steer()
	LoadCargo()
	UnloadCargo()

With this, a function that receives a Car interface as an argument expects a value that has Move and Steer methods. Likewise, all you need in order for a value to implement the Car interface are Move and Steer methods. This flexible approach allows a type to implement any number of interfaces at the same time:

type CarServer: (implements Car, Server)
	Move()
	Steer()
	SendToClient()
	ReceiveFromClient()

type CarClient: (implements Car, Client)
	Move()
	Steer()
	SendToServer()
	ReceiveFromServer()

type TruckServer: (implements Car, Truck, Server)
	Move()
	Steer()
	LoadCargo()
	UnloadCargo()
	SendToClient()
	ReceiveFromClient()

type TruckClient: (implements Car, Truck, Client)
	Move()
	Steer()
	LoadCargo()
	UnloadCargo()
	SendToServer()
	ReceiveFromServer()

Unfortunately, in a language like Lua, it can be difficult or expensive to fully enforce such a type system. You can cheat, though: when creating a new type, you explicitly specify the interfaces it is expected to implement. Asserting whether a type implements an interface then becomes as trivial as a lookup table. The idea here is to use interfaces as a framework or guideline for how your types are designed.

2 Likes

Yeah, in Lua there is no formal support for classes or interfaces, so implementing interfaces is just a special case of multiple inheritance where your convention is to only inherit additionally from classes that define only pure virtual methods.

This reminds me a lot of my question here:

Except that you’re trying to use OOP instead of ECS. My advice? I don’t like OOP, so drop it altogether and join the cool kids using ECS. :stuck_out_tongue:

When using ECS all of your modules will be broken down into much finer components and systems, many of which are client/server agnostic. By putting components in a single large pool and drawing from them as needs arise, server components will naturally only be added to server entities and the same goes for client components. A car on the server and on the client may look different because the server-side only system what was asked to make a car is aware of client/server needs and added different components (and some of the same) to the server and client entities.

1 Like

Thanks everyone! I’ve marked @Corecii’s answer as the solution as I think it fits my particular circumstances best, but all the answers were helpful, and I think in future projects I may attempt to use an ECS, and at least change up my approach to OOP.

Man, code organization is a bit of a bottomless rabbit hole!

1 Like