{"id":22665576,"url":"https://github.com/seadowg/dip-lesson","last_synced_at":"2026-04-15T20:03:43.102Z","repository":{"id":140476950,"uuid":"156858261","full_name":"seadowg/dip-lesson","owner":"seadowg","description":"Using Dependency Inversion to help you (re)write testable code","archived":false,"fork":false,"pushed_at":"2018-11-12T17:34:18.000Z","size":73,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2025-10-24T11:02:05.902Z","etag":null,"topics":["dependency-inversion-principle","java","test-driven-development","tutorial"],"latest_commit_sha":null,"homepage":"https://www.seadowg.com/dip-lesson/","language":"Java","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"unlicense","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/seadowg.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":"2018-11-09T12:19:22.000Z","updated_at":"2020-07-23T12:38:38.000Z","dependencies_parsed_at":null,"dependency_job_id":"b2b95423-b3a4-4ffa-893b-1b999f53102b","html_url":"https://github.com/seadowg/dip-lesson","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/seadowg/dip-lesson","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/seadowg%2Fdip-lesson","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/seadowg%2Fdip-lesson/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/seadowg%2Fdip-lesson/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/seadowg%2Fdip-lesson/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/seadowg","download_url":"https://codeload.github.com/seadowg/dip-lesson/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/seadowg%2Fdip-lesson/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31857625,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-15T15:24:51.572Z","status":"ssl_error","status_checked_at":"2026-04-15T15:24:39.138Z","response_time":63,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"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":["dependency-inversion-principle","java","test-driven-development","tutorial"],"created_at":"2024-12-09T13:35:43.906Z","updated_at":"2026-04-15T20:03:43.085Z","avatar_url":"https://github.com/seadowg.png","language":"Java","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Using Dependency Inversion to help you (re)write testable code\n\n## Scenario\n\nImagine you are working on a GUI application. The code for this application\nhas existed for a while and the team weren't using strict Test Driven Development.\nYou've recently started working in the codebase and you'd like to write some tests\nthat let you feel more confident about making changes.\n\n## The object under test\n\nYou want to start making changes to the code for a button in the application. This\nparticular button is one that changes from whatever color is starts out as to\nred whenever it's clicked. As we're in Java and our names are long and explicit it's named\n`ChangeToRedWhenClickedButton`.\n\nOne complexity is that our application runs in a very strict operating environment\nwhere we need to ask permission to respond to clicks events the first time they\nhappen (similar to using the camera or network in an Android or iOS app).\n\nHere's the code for `ChangeToRedWhenClickedButton`:\n\n```java\npublic class ChangeToRedWhenClickedButton extends UIButton {\n\n    @Override\n    public void onClick() {\n        StrictOS.requestClickPermission(new StrictOS.PermissionCallback() {\n            @Override\n            public void onGranted() {\n                setColor(Color.RED);\n            }\n\n            @Override\n            public void onDenied() {\n                // ignored\n            }\n        });\n    }\n}\n```\n\nYou can see that when we respond to the click we have to ask for permission using the static\n`requestClickPermission` method. We pass that method a callback that will respond using either\nthe `onnGranted` or `onDenied` method if the user grants or denies the permission respectively.\n\nLet's write a test for this class:\n\n```java\npublic class ChangeToRedWhenClickedButtonTest {\n\n    @Test\n    public void clickingOnButton_whenPermissionIsGranted_changesToRed() {\n        ChangeToRedWhenClickedButton button = new ChangeToRedWhenClickedButton();\n        button.setColor(Color.GREEN);\n\n        button.onClick();\n        assertEquals(button.getColor(), Color.RED);\n    }\n}\n```\n\nNow let's run it:\n\n```bash\njava.lang.IllegalStateException: UI not initialized!\n\n\tat internal.os.StrictOS.requestClickPermission(StrictOS.java:5)\n\tat initial.ChangeToRedWhenClickedButton.onClick(ChangeToRedWhenClickedButton.java:12)\n\tat initial.ChangeToRedWhenClickedButtonTest.clickingOnButton_whenPermissionIsGranted_changesToRed(ChangeToRedWhenClickedButtonTest.java:16)\n```\n\nAh. That's not good. It seems there is some complexity to calling the `requestClickPermission` method when\nthe application is not actually running. It would be great if under test we could control the outcome\nof `requestPermissionClicked`. This is awkward though as we'd have to mock a static method. We can of course do\nthat (using libraries such as PowerMock) but often when something is hard to test it can be a sign to stop\nand think about why. Is this code over complex? Are the dependencies too tangled? Is it not modular enough? Let's\nexplore the idea of refactoring our code so that it might be easier to test.\n\n## Dependency Inversion Principle\n\nDIP is a commonly used practice (often accidentally due to test pushing you towards it) within the TDD world as following\nit's advice can be useful for designing testable code. DIP has two main components:\n\n**a)** High-level modules should not depend on low-level modules. Both should depend on abstraction.\n\n**b)** Abstractions should not depend on details. Details should depend on abstractions.\n\nOK so what does this mean? So in our example we can think of our `ChangeToRedWhenClickedButton` as a \"high-level module\"\nand our `StrictOS` as a \"low-level module\". So **(a)** is stating that our button should not \"depend\" on our OS utility. What\n**(b)** is then suggesting is that our button instead depends on an abstraction. This is all pretty academic right now\nso let's try following the advice and create an abstraction for our `StrictOS`:\n\n```java\npublic interface ClickPermissionRequester {\n    void request(StrictOS.PermissionCallback permissionCallback);\n}\n```\n\nWe've used an `interface` so we can use a different implementation in our tests and our real code. To do that of\ncourse we will \"inject\" our dependency so that `ChangeToRedWhenClickedButton` depends on the interface rather\nthan a concrete implementation:\n\n```java\npublic class ChangeToRedWhenClickedButton extends UIButton {\n\n    private final ClickPermissionRequester clickPermissionRequester;\n\n    public ChangeToRedWhenClickedButton(ClickPermissionRequester clickPermissionRequester) {\n        this.clickPermissionRequester = clickPermissionRequester;\n    }\n\n    @Override\n    public void onClick() {\n        clickPermissionRequester.request(new StrictOS.PermissionCallback() {\n\n            @Override\n            public void onGranted() {\n                setColor(Color.RED);\n            }\n\n            @Override\n            public void onDenied() {\n                // ignored\n            }\n        });\n    }\n}\n```\n\nNow for our application we can simply wrap our `StrictOS` interaction in an implementation\nof this interface that could be passed to the button in its constructor:\n\n```java\npublic class StrictOSClickPermissionRequester implements ClickPermissionRequester {\n    @Override\n    public void request(StrictOS.PermissionCallback permissionCallback) {\n        StrictOS.requestClickPermission(permissionCallback);\n    }\n}\n```\n\nAnd now we can use a fake implementation on our tests:\n\n```java\npublic class ChangeToRedWhenClickedButtonTest {\n\n    @Test\n    public void clickingOnButton_whenPermissionIsGranted_changesToRed() {\n        ChangeToRedWhenClickedButton button = new ChangeToRedWhenClickedButton(new GrantedClickPermissionRequester());\n        button.setColor(Color.GREEN);\n\n        button.onClick();\n        assertEquals(button.getColor(), Color.RED);\n    }\n\n    private class GrantedClickPermissionRequester implements ClickPermissionRequester {\n\n        @Override\n        public void request(StrictOS.PermissionCallback permissionCallback) {\n            permissionCallback.onGranted();\n        }\n    }\n}\n```\n\nAnd it passes! By extracting an abstraction around requesting click permissions we've made our object\nmore testable.\n\nOf course this also provides other advantages: one for instance is that if the `StrictOS`\nobject changes it's API then our `ChangeToRedWhenClickedButton` does not have to be changed (unless the entire nature\nof the behaviour changes). We're keeping the parts of our code we control protected from the parts we don't.\n\n## Going further\n\nOK so we've learnt some fancy terminology and we've got a test. Have we really improved the code other than for our\ntestability? Not a lot.\n\nLet's think a little hard about **(b)**: \"Abstractions should not depend on details.\"\nIf we look at our `ClickPermissionRequester` it's pretty clear it's an abstraction that does \"depend on the details\". For instance,\nin our `ChangeToRedWhenClickedButton` we don't care about the case where permissions aren't granted. But because of the detail\nof the `requestClickPermission` method we've made it care about that case. Let's flip this on it's head and create an\nabstraction from the viewpoint of our \"high level\" module:\n\n```java\npublic interface Clicker {\n    void click(ClickCallback clickCallback);\n\n    interface ClickCallback {\n        void clicked();\n    }\n}\n```\n\nOK so now we have the word \"click\" in there a few many times but the interface is inherently simpler. Let's create\nan implementation for to wrap our `StrictOS` interaction:\n\n```java\npublic class StrictOSClicker implements Clicker {\n    @Override\n    public void click(ClickCallback clickCallback) {\n        StrictOS.requestClickPermission(new StrictOS.PermissionCallback() {\n            @Override\n            public void onGranted() {\n                clickCallback.clicked();\n            }\n\n            @Override\n            public void onDenied() {\n\n            }\n        });\n    }\n}\n```\n\nAnd we can update the test:\n\n```java\npublic class ChangeToRedWhenClickedButtonTest {\n\n    @Test\n    public void clickingOnButton_whenPermissionIsGranted_changesToRed() {\n        ChangeToRedWhenClickedButton button = new ChangeToRedWhenClickedButton(new FakeClicker());\n        button.setColor(Color.GREEN);\n\n        button.onClick();\n        assertEquals(button.getColor(), Color.RED);\n    }\n\n    private class FakeClicker implements Clicker {\n        @Override\n        public void click(ClickCallback clickCallback) {\n            clickCallback.clicked();\n        }\n    }\n}\n```\n\nNow our button doesn't care about the concept that the click can fail. Maybe at some point down the line we'll\nneed to update our `Clicker` abstraction for that but that can be driven out by the UI layer needing to present\nan error or something of that nature.\n\n## Reality check\n\nThis example is obviously a little contrived. Here's some points to keep in mind\nwhen applying any of this in the real world:\n\n* Often you end up dealing with objects where you don't have access to the constructor (looking at you Android). This\nmakes \"injecting\" the abstractions you pull out a lot harder. You can solve this with public fields with default values or\nDependency Injection/Service Locator frameworks.\n* We'd ideally want tests that check the behaviour at an application level before running through a change like\nthis. With a more complex example it would be easy to miss a detail when creating your abstractions and end up\nwith changes in behaviours or errors.\n* It's far more ideal to apply this kind of philosophy with a test first approach. In that case our abstractions can be\ndriven straight from the tests. This allows to not get caught up in the details and get straight to our `Clicker` abstraction.\n* If you're in a similar situation but you own the static method (i.e. it's code in your codebase) getting in your\nway a good first step is to fix that rather than wrap it (as you may already have an \"abstraction\" that is just static).\n* As with anything in programming this is just \"like, your opinion man\". If DIP helps you solve a problem then\nthat's fantastic. If it doesn't, don't sweat it.","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fseadowg%2Fdip-lesson","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fseadowg%2Fdip-lesson","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fseadowg%2Fdip-lesson/lists"}