{"id":16692651,"url":"https://github.com/platisd/break-the-coupling-cpp","last_synced_at":"2025-08-24T11:16:05.743Z","repository":{"id":56610473,"uuid":"304126594","full_name":"platisd/break-the-coupling-cpp","owner":"platisd","description":"5 ways to decouple dependencies in C++","archived":false,"fork":false,"pushed_at":"2023-12-31T19:05:33.000Z","size":2831,"stargazers_count":6,"open_issues_count":0,"forks_count":1,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-07-18T02:19:46.328Z","etag":null,"topics":["cpp","decoupling","dependency-injection","inversion-of-control","templates","tutorial"],"latest_commit_sha":null,"homepage":"","language":"C++","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/platisd.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2020-10-14T20:21:27.000Z","updated_at":"2025-07-10T08:40:44.000Z","dependencies_parsed_at":"2025-02-16T06:32:52.523Z","dependency_job_id":"5b1401d6-c06d-44c9-b3b9-123e853bf5c3","html_url":"https://github.com/platisd/break-the-coupling-cpp","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/platisd/break-the-coupling-cpp","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/platisd%2Fbreak-the-coupling-cpp","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/platisd%2Fbreak-the-coupling-cpp/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/platisd%2Fbreak-the-coupling-cpp/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/platisd%2Fbreak-the-coupling-cpp/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/platisd","download_url":"https://codeload.github.com/platisd/break-the-coupling-cpp/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/platisd%2Fbreak-the-coupling-cpp/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":271851955,"owners_count":24834069,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-08-24T02:00:11.135Z","response_time":111,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cpp","decoupling","dependency-injection","inversion-of-control","templates","tutorial"],"created_at":"2024-10-12T16:28:00.886Z","updated_at":"2025-08-24T11:16:04.863Z","avatar_url":"https://github.com/platisd.png","language":"C++","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Break the coupling (in C++) ![Examples CI](https://github.com/platisd/break-the-coupling-cpp/workflows/Examples%20CI/badge.svg?branch=master)\n\nImplementations tightly coupled to their dependencies are difficult to maintain,\nreuse and test.\n\nIn this repository, I will show you 5 ways of refactoring with the intent to \n\"break\" tight coupling between business logic and dependencies.\nA good indicator over the effectiveness of a decoupling is testability: If you are\nable to test your class and your class only without being influenced by the inner\nworks of the various dependencies, then you know you have done a good decoupling.\n\nBelow you will find some tightly coupled code and several ways to break it away from\nits dependencies. The focus will be mostly on the code, however, the build configuration\nis often also very important. Being decoupled on the code level but not in the\nCMake/Bazel/Blueprint files will not get you very far. Last but not least, for every\nrefactored example you will find the unit tests that accompany it.\n\n- [Tightly coupled code example](#tightly-coupled-code-example)\n  - [CameraPowerController.h](#camerapowercontrollerh)\n  - [CameraPowerController.cpp](#camerapowercontrollercpp)\n- [Dependency Injection (Polymorphic)](#dependency-injection-polymorphic)\n  - [SerialPortAdapter.h](#serialportadapterh)\n  - [AsioSerialPortAdapter](#asioserialportadapter)\n  - [CameraPowerController.h](#camerapowercontrollerh-1)\n  - [di_polymorphism_main.cpp](#di_polymorphism_maincpp)\n- [Dependency Injection (Templatized)](#dependency-injection-templatized)\n  - [CameraPowerController.h](#camerapowercontrollerh-2)\n- [Dependency Injection (Abstract factory)](#dependency-injection-abstract-factory)\n  - [AsioSerialPortManager.h](#asioserialportmanagerh)\n  - [AsioSerialPortManagerFactory](#asioserialportmanagerfactory)\n  - [CameraPowerController.cpp](#camerapowercontrollercpp-1)\n- [Link time switching](#link-time-switching)\n  - [Coupled configuration](#coupled-configuration)\n  - [Decoupled configuration](#decoupled-configuration)\n- [Link time switching (Templatized)](#link-time-switching-templatized)\n  - [CameraPowerController.h](#camerapowercontrollerh-3)\n- [YouTube](#youtube)\n\n## Tightly coupled code example\n\nHere is some typical code that is coupled with its dependencies. It will be refactored\nto demonstrate the different ways you can decouple troublesome code.\n\n### [CameraPowerController.h](src/camera_power_controller/include/CameraPowerController.h)\n\n```cpp\nclass CameraPowerController\n{\npublic:\n    CameraPowerController(ProductVariant productVariant);\n\n    void turnOnCamera();\n    void turnOffCamera();\n\nprivate:\n    std::unique_ptr\u003cAsioSerialPortManager\u003e mAsioSerialPortManager;\n};\n```\n\n`AsioSerialPortManager`, as its name implies, is a concrete class that utilizes a\n[3rd-party library](https://github.com/chriskohlhoff/asio).\n\n### [CameraPowerController.cpp](src/camera_power_controller/src/CameraPowerController.cpp)\n\n```cpp\nCameraPowerController::CameraPowerController(ProductVariant productVariant)\n{\n    switch (productVariant)\n    {\n    case ProductVariant::A:\n        mAsioSerialPortManager = std::make_unique\u003cAsioSerialPortManager\u003e(\n            kSerialDevicePathForVariantA, kBaudRateForVariantA);\n        return;\n    case ProductVariant::B:\n        mAsioSerialPortManager = std::make_unique\u003cAsioSerialPortManager\u003e(\n            kSerialDevicePathForVariantB, kBaudRateForVariantB);\n        return;\n    default:\n        throw std::logic_error(\"Unknown variant\");\n    }\n}\n\nvoid CameraPowerController::turnOnCamera()\n{\n    mAsioSerialPortManager-\u003easioWrite(\"ON\");\n}\n```\n\nIn the `CameraPowerController` implementation, we invoke the constructor of `AsioSerialPortManager`\nwith some arguments. These arguments are determined based on logic residing within the class. Then,\nwe call the instantiated class in the other member functions.\n\nWhenever we invoke constructors of other classes, we become get coupled to them. In fact, this is\nvery often a red flag in regards to the testability of the class. Testability is decreased\nbecause we become obliged to indirectly test them as well along with the unit we actual want to test.\nMoreover, compiling and running a binary that involves them may be cumbersome or even impossible in\nour unit testing environment.\n\nFor example, the particular classes may not be compilable for the host platform that runs the\nunit tests. This is common when dependencies include resources that are precompiled for a different\noperating system or architecture. Additionally, even if the dependant classes are compilable, it\nmay be impossible to execute them on our host platform, due to them requiring some specific resources\nthat are only found on a *target* system. In our case, the `AsioSerialPortManager` class needs to\nopen a connection to a serial device that will only exist on the target we are developing for and not\non the developer's computer.\n\nIn other words, if we would like to unit test our `CameraPowerController` class and have the unit tests\nrun on our development machines there is no other option than decoupling it from `AsioSerialPortManager`.\n\n## Dependency Injection (Polymorphic)\n\nThe cleanest (IMHO) way to decouple dependencies is to inject their abstractions and use those instead.\nIf such abstractions do not already exist, this means that you have to define an interface\n(i.e. a pure abstract class) that represents the business value the dependencies offer in a rather generic\nway. Then, you can have the dependency you need inherit/implement it. Finally, make the constructor of\nthe class under test receive the interface as an argument.\n\n\n### [SerialPortAdapter.h](di_polymorphism/serial_port_adapters/public/SerialPortAdapter.h)\n\nThe most straight forward way perhaps is to create an abstract interface directly out of the dependency.\nHowever, this is a bad practice since you will be indirectly coupling your code with the particular implementation.\nInstead, I suggest you try to figure out what is the business value in a generic manner. With this in mind,\nI created the `SerialPortAdapter` interface that generalizes any class that works with the serial port and\nexposes a way to transmit data in a generic manner.\n\n```cpp\nstruct SerialPortAdapter\n{\n    virtual ~SerialPortAdapter() = default;\n\n    virtual void send(std::string_view message) = 0;\n};\n```\n\n### [AsioSerialPortAdapter](di_polymorphism/serial_port_adapters/asio_serial_port_adapter)\n\nThen we have the `AsioSerialPortAdapter` concrete class that inherits from `SerialPortAdapter` and exposes\nthe `AsioSerialPortManager` in a generic manner.\n\n```cpp\nclass AsioSerialPortAdapter : public SerialPortAdapter\n{\npublic:\n    AsioSerialPortAdapter(AsioSerialPortManager* asioSerialPortManager);\n\n    void send(std::string_view message) override;\n\nprivate:\n    AsioSerialPortManager* mAsioSerialPortManager;\n};\n```\n\n```cpp\nAsioSerialPortAdapter::AsioSerialPortAdapter(\n    AsioSerialPortManager* asioSerialPortManager)\n    : mAsioSerialPortManager{asioSerialPortManager}\n{\n}\n\nvoid AsioSerialPortAdapter::send(std::string_view message)\n{\n    mAsioSerialPortManager-\u003easioWrite(message);\n}\n```\n\n### [CameraPowerController.h](di_polymorphism/camera_power_controller/include/CameraPowerController.h)\n\nIn our `CameraPowerController` we inject the `SerialPortAdapter`. Now, there is nothing specific to the `asio` library.\nWe are effectively decoupled from it.\n\n```cpp\nclass CameraPowerController\n{\npublic:\n    CameraPowerController(SerialPortAdapter* serialPortAdapter);\n\n    void turnOnCamera();\n    void turnOffCamera();\n\nprivate:\n    SerialPortAdapter* mSerialPortAdapter;\n};\n```\n\n### [di_polymorphism_main.cpp](di_polymorphism/di_polymorphism_main.cpp)\n\nNow that we are injecting things, why not follow the *Inversion of Control* (IoC) principle all the way?\nThe serial port configuration is probably not a concern of the `CameraPowerController` class, however it takes\nplace in its constructor. Now that we are injecting resources and letting the users of our classes have control,\nwe are indirectly encouraged to move these seemingly unrelated functionality outside the class.\n\n```cpp\nstd::pair\u003cstd::filesystem::path, int\u003e\ngetAsioSerialPortManagerConfiguration(ProductVariant productVariant)\n{\n    switch (productVariant)\n    {\n    case ProductVariant::A:\n        return std::make_pair(kSerialDevicePathForVariantA,\n                              kBaudRateForVariantA);\n    case ProductVariant::B:\n        return std::make_pair(kSerialDevicePathForVariantB,\n                              kBaudRateForVariantB);\n    default:\n        throw std::logic_error(\"Unknown variant\");\n    }\n}\n\nint main()\n{\n    const auto [serialDevice, baudRate]\n        = getAsioSerialPortManagerConfiguration(getProductVariant());\n\n    AsioSerialPortManager asioSerialPortManager{serialDevice, baudRate};\n    AsioSerialPortAdapter asioSerialPortAdapter{\u0026asioSerialPortManager};\n    CameraPowerController cameraPowerController{\u0026asioSerialPortAdapter};\n    cameraPowerController.turnOnCamera();\n    cameraPowerController.turnOffCamera();\n\n    return 0;\n}\n```\n\n## Dependency Injection (Templatized)\n\nIf many levels of indirection and `virtual` functions are not desirable (e.g. due to strict requirements on performance)\nthen injecting your dependencies via the constructor is still the way to go. However, this time you will not use a class hierarchy.\nInstead, you can use a *class template* to determine the type of the constructor argument.\n\nThis allows for a looser coupling that is determined by the one that instantiates the class and not the class itself.\n\n### [CameraPowerController.h](di_template/camera_power_controller/include/CameraPowerController.h)\n\n```cpp\ntemplate\u003ctypename SerialPortManager\u003e\nclass CameraPowerController\n{\npublic:\n    CameraPowerController(SerialPortManager* serialPortManager)\n        : mSerialPortManager{serialPortManager}\n    {\n    }\n\n    void turnOnCamera()\n    {\n        mSerialPortManager-\u003easioWrite(\"ON\");\n    }\n\n    void turnOffCamera()\n    {\n        mSerialPortManager-\u003easioWrite(\"OFF\");\n    }\n\nprivate:\n    SerialPortManager* mSerialPortManager;\n};\n```\nAs long as the class template *type* implements an API equivalent to the `AsioSerialPortManager` then our coupling\nis only in the design/conceptual level. This is not ideal, however can be good enough for many of the cases. Furthermore,\nit should be noted that yet again we have moved the configuration of the serial port outside the class.\n\n## Dependency Injection (Abstract factory)\n\nIf there are really good reasons to give a class control over (some of) its resources then it may not be possible to\ninstantiate its dependencies in the integration scope (e.g. a `main()` function) because they require some information\nthat resides within the class that uses them.\n\nA good example is the *original* `CameraPowerController` constructor:\n\n```cpp\nCameraPowerController::CameraPowerController(ProductVariant productVariant)\n{\n    switch (productVariant)\n    {\n    case ProductVariant::A:\n        mSerialPortManager = std::make_unique\u003cAsioSerialPortManager\u003e(\n            kSerialDevicePathForVariantA, kBaudRateForVariantA);\n        return;\n    case ProductVariant::B:\n        mSerialPortManager = std::make_unique\u003cAsioSerialPortManager\u003e(\n            kSerialDevicePathForVariantB, kBaudRateForVariantB);\n        return;\n    default:\n        throw std::logic_error(\"Unknown variant\");\n    }\n}\n```\n`AsioSerialPortManager` requires two arguments to be passed to its constructor that are owned by the class that\nuses it. To be honest with you, I consider this a design smell. However, if you are really convinced something like this\nmakes sense, then you can inject an abstract factory class to keep your implementation decoupled from its dependencies.\n\nThe abstract factory class takes the arguments that would have otherwise been supplied to the concrete class' constructor\nand returns an instance of the concrete class. The \"trick\" is that our class depends on an abstraction returned by the\nfactory function call, not a particular implementation.\n\n### [AsioSerialPortManager.h](di_factory/serial_port_managers/asio_serial_port_manager/include/AsioSerialPortManager.h)\n\n```cpp\nclass AsioSerialPortManager : public SerialPortManager\n{\npublic:\n    AsioSerialPortManager(std::filesystem::path serialDevice, int baudRate);\n\n    void asioWrite(std::string_view message) override;\n\nprivate:\n    asio::io_service mIoService;\n    asio::serial_port mSerialPort{mIoService};\n};\n```\n\n`SerialPortManager` is a pure abstract class that generalizes `AsioSerialPortManager`.\n\n### [AsioSerialPortManagerFactory](di_factory/serial_port_manager_factories/asio_serial_port_manager_factory)\n\nThe `SerialPortManagerFactory` pure abstract class \"promises\" its children to return a child/specialization of the (also abstract) `SerialPortManager`.\n\n```cpp\nclass AsioSerialPortManagerFactory : public SerialPortManagerFactory\n{\npublic:\n    std::unique_ptr\u003cSerialPortManager\u003e get(std::filesystem::path serialDevice,\n                                           int baudRate) const override;\n};\n```\n\nUnsurprisingly, `AsioSerialPortManagerFactory` returns an `AsioSerialPortManager` instance.\n\n```cpp\nstd::unique_ptr\u003cSerialPortManager\u003e\nAsioSerialPortManagerFactory::get(std::filesystem::path serialDevice,\n                                  int baudRate) const\n{\n    return std::make_unique\u003cAsioSerialPortManager\u003e(serialDevice, baudRate);\n}\n```\n\n### [CameraPowerController.cpp](di_factory/camera_power_controller/src/CameraPowerController.cpp)\n\nOur `CameraPowerController` class maintains ownership of the resources needed for the instantiation of its dependency but\nno longer *depends* on it. Instead, it depends on the abstraction (i.e. `SerialPortManager`) returned by the `get` call.\n\n```cpp\nCameraPowerController::CameraPowerController(\n    SerialPortManagerFactory* serialPortManagerFactory,\n    ProductVariant productVariant)\n{\n    switch (productVariant)\n    {\n    case ProductVariant::A:\n        mSerialPortManager = serialPortManagerFactory-\u003eget(\n            kSerialDevicePathForVariantA, kBaudRateForVariantA);\n        return;\n    case ProductVariant::B:\n        mSerialPortManager = serialPortManagerFactory-\u003eget(\n            kSerialDevicePathForVariantB, kBaudRateForVariantB);\n        return;\n    default:\n        throw std::logic_error(\"Unknown variant\");\n    }\n}\n```\n\n## Link time switching\n\nOccasionally it may not be feasible or practical to refactor existing code but you still wish to break the coupling between\nthe implementation and its dependencies. What you can do, is continue depending on the same *declarations* during compile time\nbut \"replace\" the *definitions* of your dependencies when linking.\n\nSince your code still depends on the same exposed functions and types, you do not need to change it. Instead, when linking you\ncan provide an alternative implementation (e.g. a mock). This time, the \"magic\" happens on the configuration level.\n\n### Coupled configuration\n\n```cmake\n# CameraPowerController\nadd_library(camera_power_controller src/CameraPowerController.cpp)\n\ntarget_include_directories(camera_power_controller PUBLIC include)\n\ntarget_link_libraries(camera_power_controller\n        PUBLIC\n        asio_serial_port_manager\n        product_variant\n        )\n```\n\n```cmake\n# src_main\nadd_executable(src_main main.cpp)\ntarget_link_libraries(src_main\n        PRIVATE\n        camera_power_controller\n        )\n```\n\nOn a *configuration* level our `camera_power_controller` target is tightly coupled to the `asio_serial_port_manager` one.\nNo matter what we do on the code level, we will be pulling in the dependency.\n\n### Decoupled configuration\n\n```cpp\ntarget_include_directories(link_switch_camera_power_controller PUBLIC include)\n\ntarget_link_libraries(link_switch_camera_power_controller\n        PUBLIC\n        asio_serial_port_manager_interface\n        product_variant\n        )\n```\n\nTo break the dependency to the `asio_serial_port_manager` target, we instead depend on the\n`asio_serial_port_manager_interface` which only includes the header files necessary. This means that compilation will work as before.\nHowever, we still need the *actual* definitions during linking to build a binary. The definitions will be supplied in the integration\nscope, i.e. the target that builds the executable.\n\n```cpp\n# src_main\nadd_executable(link_switch_main link_switch_main.cpp)\ntarget_link_libraries(link_switch_main\n        PRIVATE\n        link_switch_camera_power_controller\n        asio_serial_port_manager\n        )\n```\n\nThe `link_switch_main` target, as the one that creates the executable, is responsible for bringing everything together and making\nsure that the necessary resources are available for linking. *This* is eventually the target that depends on `asio_serial_port_manager`.\n\nAfter decoupling, you can provide alternative implementations for the dependencies that will run on different platforms,\ne.g. during unit tests. Check out how we would now test `CameraPowerController`:\n\n* [Test configuration](link_switch/test/CMakeLists.txt)\n* [Mock](link_switch/test/mocks/MockAsioSerialPortManager.h)\n* [Alternative `AsioSerialPortManager` implementation](link_switch/test/mocks/AsioSerialPortManager.cpp) that statically\ninvokes the mocks.\n\n## Compile time switching\n\nAnother way to replace dependencies is with a class template. Particularly, turn `CameraPowerController` into one.\n\n### [CameraPowerController.h](link_switch_template/camera_power_controller/include/CameraPowerController.h)\n\n```cpp\ntemplate\u003ctypename SerialPortManager\u003e\nclass CameraPowerController\n{\npublic:\n    CameraPowerController(ProductVariant productVariant)\n    {\n        switch (productVariant)\n        {\n        case ProductVariant::A:\n        {\n            const std::filesystem::path kSerialDevicePathForVariantA{\n                \"/dev/CoolCompanyDevice\"};\n            const auto kBaudRateForVariantA = 9600;\n            mSerialPortManager = std::make_unique\u003cSerialPortManager\u003e(\n                kSerialDevicePathForVariantA, kBaudRateForVariantA);\n        }\n            return;\n        case ProductVariant::B:\n        {\n            const std::filesystem::path kSerialDevicePathForVariantB{\"COM3\"};\n            const auto kBaudRateForVariantB = 115200;\n            mSerialPortManager = std::make_unique\u003cSerialPortManager\u003e(\n                kSerialDevicePathForVariantB, kBaudRateForVariantB);\n        }\n            return;\n        default:\n            throw std::logic_error(\"Unknown variant\");\n        }\n    }\n\n    void turnOffCamera()\n    {\n        mSerialPortManager-\u003easioWrite(\"OFF\");\n    }\n\nprivate:\n    std::unique_ptr\u003cSerialPortManager\u003e mSerialPortManager;\n};\n```\n\n`CameraPowerController` is not coupled to the implementation of `AsioSerialPortManager` but something that behaves exactly\nlike one. This means we can create variants by using a different type during the `CameraPowerController` instantiation.\n\nFor the unit tests, we follow a similar approach to the other \"link switch\" method.\n\n## YouTube\n\nThis tutorial also exists as a [video on YouTube](https://www.youtube.com/watch?v=SRLVf6Ssx1s). Check it out and if you\nenjoyed it don't forget to comment, like and subscribe! 😊\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fplatisd%2Fbreak-the-coupling-cpp","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fplatisd%2Fbreak-the-coupling-cpp","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fplatisd%2Fbreak-the-coupling-cpp/lists"}