So we have a monorepo where 90% of all code is shared between clients. However, occasionally a specific client wants to override a specific feature so we end up with Standard::Feature and Client::Feature (naming is demonstrative and is completely open to change). When we set the environment variable stating that this Rails instance of the application if for the client, we currently have a unmaintainable mess of code that shims in Client::Feature for Standard::Feature when calling Feature.call . However, since 90% of the code is not changed, they don’t want to recreate all features in the Client pack as that would be hundreds of practically empty files.
e.g. If both use Standard::CommonFeature we would prefer not the just create a dummy link to Client::CommonFeature just so we could just keep a common namespace.
I’m not sure I’m totally following. You’re saying when the application loads with a certain environment variable, certain methods return one class vs a different one? I’m not sure what you mean by “they don’t want to recreate all features in the client pack.” If the code is shared, why is the client needing to recreate features?
Yes. One proposed solution would be to “recreate” all class (i.e. Client::CommonFeature = Standard::CommonFeature) so that we could always just call %(#{ENV["CLIENT"]::CommonFeature).constantize and get the right object.
It sounds like you want to have a common abstract interface for your client. That way you can say that a function always returns an interface, and different things can implement that interface differently. Sorbet makes this pretty easy (link), if you’re already using it
It is more that we are hoping to have a single public entry point Feature.call and have the private internal API handle the switching in a more maintainable way.
You could still do that with interfaces. I probably wouldn’t use an environment variable for this (although I’m not sure in what context folks are starting and using your application), but either way, you can have the env flag flip between the different implementations (as a hash or case statement or whatever).
Does your link talk about using interface to do that? We aren’t currently using Sorbet, but anything to fix this issue would be a easy sell. The application is always boot on client servers where that environment variable is never switch, though us as developer change it everytime we switch to a specific clients bug.
Switching between implementations is totally up to you since it depends on the semantics and logic of switching between them. Sorbet just provides the type support for interfaces.
I guess I’m not understanding what the concrete pain is still. Is the pain that you have to stop and restart your server when switching between implementations?
We do not need to switch implementations, so that is a relief. The concrete pain is twofold. The first is that it is a common problem that a developer forgets to update a hash . As such, in spite of implementing the client specific version of the code and the unit test for the code as the hashing doesn’t count as a branch, it is frequent place to not add the integration test and the coverage numbers do not reflect it.
The second is that since some services are used by multiple features, we unfortunately have a windy path where we call Feature.call which delegates to Client::Feature with uses Service which may delegate to Client::Service . This makes the code difficult to follow and adds many unneeded checks.
Sorry I think you lost me again at “a developer forgets to update a hash.” What hash? For what purpose? How is a hash related to client specific implementations of interfaces?
If code is reused, there will always be some abstraction. It sounds like removing some delegates and calling the shared code you want to invoke directly may reduce some indirection.
Overall this sounds like a design problem where the architecture of the system doesn’t lend itself well to the problem you’re trying to solve. Rather than making it easier to work within that painful design it might just be better to work on updating the design to be simpler. It’s hard for me to say though because I’m still trying to understand the problem