Do you mean that my packages should use a nested copy of each package they depend on which is what they already do as a secondary option? This way, the package contains its dependencies while still having a clear definition of what it’s dependent on. Or do you mean that I should stop reusing functions and instead implement similar code separately in each package? I’m definitely not going to do the latter.
Why I don’t like the former approach much either
The former approach may not be problematic if the packages that other code depend on don’t have state and don’t automatically modify the state of the game (make changes to Instances for example). However, if a package has state stored as Luau tables or if it changes the state of the game, there may be problems when there are multiple copies of the package.
For example, let’s say the server side code of a package creates a RemoteEvent with a specific name in ReplicatedStorage and the client code uses FindFirstChild or WaitForChild to get a reference to this RemoteEvent. If there are multiple copies of this package and each of them creates their own RemoteEvent, the client may connect its handler function to the RemoteEvent of the other copy of the same package which will break the behavior of one or both package copies. I’m pretty sure the only way to ensure that the client has a reference to the correct instance is by sending the instance via a RemoteEvent. That doesn’t help when the RemoteEvent is exactly the thing we are trying to get a client reference to. This problem could be solved by not creating a new RemoteEvent in the server code if there already is a RemoteEvent with the correct name. However, each of those package copies still has its own Luau state which may cause problems.
Also, what if a package that is used for handling RemoteEvents has an event that fires every time any RemoteEvent handled by the package is fired? The game developer might want to track the whole game’s RemoteEvent traffic and if there are multiple copies of the package, the developer needs to make a connection to the event of each of them.
There could also be a package that has features for utilising two packages together. Other code in the game in which that package is used should probably be able to access data of the the same copies of the two packages that the package using both of them accesses.
Some reasons why I want to keep using package dependencies
I feel like it’s more organized to not have everything in one package. If a specialized package, like RagdollSystem, needs to do such a general thing that the same kind of thing with different data might need to be done in other unrelated code, it feels more logical to me to write a general purpose function for it in a separate package even if I don’t have other code needing that function at the moment.
The reason I am making packages is that I wanted to finally find a practical way to reuse code across projects. My earlier projects didn’t have a proper way of reusing code. Instead, I rewrote or copy pasted code. The packages I mentioned are some of my first Roblox packages and I think I started writing them at around the same time last year. I have been writing the packages simultaneously. Whenever my other packages have needed code for something that feels too general purpose to be implemented in a specialized package, I have added it to GeneralUtility. So the functions and data in GeneralUtility are mostly things that were written because they were needed in a package that uses GeneralUtility.
The purpose of these packages is to become a modular feature rich framework that contains both general purpose packages usable in many kinds of projects and more specialized packages usable in certain kinds of projects. In a project that doesn’t need ragdolls, GeneralUtility can still be useful. Dividing code into packages with clearly defined one-way dependencies makes it easy to reuse some packages without having to also add the packages that depend on them. If all the code used for something was in one package or not in a package at all, it wouldn’t be as clear which code needs which other code.
Admittedly, when a package uses another package that has more features than the dependent package needs, that results in there being unused code. However, I think unused code is a less significant problem than duplicated logic. I don’t think the memory used by that unused Luau code is usually significant unless there’s a huge amount of such code. The engine has a much larger codebase and a specific game only uses specific parts of the API so there’s already a lot of unused engine code taking memory and Luau code is quite insignificant compared to that.
Code should be maintainable and extendable. Centralizing and reusing logic makes updating easier. Also, rewriting the same kind of code multiple times means that you may sometimes forget some details, for example handling some special cases, and that may cause problems that could be avoided by using code that you’ve already used and found to work correctly.
Of course, one problem with making reusable code is that you may not be able to include some optimizations that you could include in code that is written for a specific purpose. However, it’s important to consider how significant that optimization is. If the code is only ran occasionally, and it’s not a big optimization, sacrificing reusability for it is probably not worth it.
At least when the dependencies are my own packages, package versions shouldn’t be a problem as long as I update the dependent packages when I make such changes in a dependency that they affect the dependent package.