• Korne127@lemmy.world
    link
    fedilink
    English
    arrow-up
    3
    arrow-down
    6
    ·
    1 year ago

    This is the one thing where my opinion might be highly unpopular, but I don’t see circular dependencies as something bad. If you write really structured modularised code, and just use the APIs of the different modules, it is completely fine to have different modules all depend on each other. At least in object oriented programming, using this approach that’s almost guaranteed to happen.
    E.g. imagine a configuration module: Other modules need access to the configuration module to get the different entry values, but the configuration module needs access to other modules to change specific stuff if you change a configuration entry. Of course you could separate that module into one data part and one action part changing the data and doing stuff in other modules, but if you use many modules like that, this will just become even more complicated and lead to duplicated structures (e.g. a specific data tree plus the exact same tree for actions) which would be cleaner if it was directly in the same module.
    Or a different example: An (e.g. discord) bot which is connected in one module, and both messages to the bot can lead to the module getting informations from other modules and actions in other modules can trigger the bot, leading to them depending on each other.

    • eth0p@iusearchlinux.fyi
      link
      fedilink
      English
      arrow-up
      9
      ·
      1 year ago

      Circular dependencies can be removed in almost every case by splitting out a large module into smaller ones and adding an interface or two.

      In your bot example, you have a circular dependency where (for example) the bot needs to read messages, then run a command from a module, which then needs to send messages back.

          v-----------\
        bot    command_foo
          \-----------^
      

      This can be solved by making a command conform to an interface, and shifting the responsibility of registering commands to the code that creates the bot instance.

          main <---
          ^        \
          |          \
          bot ---> command_foo
      

      The bot module would expose the Bot class and a Command instance. The command_foo module would import Bot and export a class implementing Command.

      The main function would import Bot and CommandFoo, and create an instance of the bot with CommandFoo registered:

      // bot module
      export interface Command {
          onRegister(bot: Bot, command: string);
          onCommand(user: User, message: string);
      }
      
      // command_foo module
      import {Bot, Command} from "bot";
      export class CommandFoo implements Command {
          private bot: Bot;
      
          onRegister(bot: Bot, command: string) {
              this.bot = bot;
          }
      
          onCommand(user: User, message: string) {
              this.bot.replyTo(user, "Bar.");
          }
      }
      
      // main
      import {Bot} from "bot";
      import {CommandFoo} from "command_foo";
      
      let bot = new Bot();
      bot.registerCommand("/foo", new CommandFoo());
      bot.start();
      

      It’s a few more lines of code, but it has no circular dependencies, reduced coupling, and more flexibility. It’s easier to write unit tests for, and users are free to extend it with whatever commands they want, without needing to modify the bot module to add them.