This might clear up a few things:
Explanation of the UML arrows
Note the images are for Visual Studio, but I would think most of the information would be equivalent with most UML documentation.
The dashed line represents the equivalent of "implements"
The naming of the entities in the UML is throwing me a bit too... it appears that Policy
is dependent on an interface that describes the contract to the lower level module of Mechanism
However on the second (the solid line) that is supposed to be representative of inheritance (at least I believe). Mechanism
implements the interface (abstraction) Policy
rather than prior to the application of DIP when Policy
referenced the concrete Mechanism
.
What it's trying to convey mostly is classes shouldn't depend on other classes, they can however depend on abstractions (interfaces) rather than concretes.
The easiest example of this: Original Foo/Logger with dependencies on lower level modules.
// "Low level Module" Mechanism equivilant
public class Logger {
public void logInformation(String logInfo) {
System.out.println(logInfo);
}
}
// "High level module" Policy equivalent.
public class Foo {
// direct dependency of a low level module.
private Logger logger = new Logger();
public void doStuff() {
logger.logInformation("Something important.");
}
}
In the above, Foo
is dependent on the concrete implementation of Logger
. This can be refactored as such (note there are several methods to do this, this is just one)
public interface ILogger {
void logInformation(String logInfo);
}
public class Logger implements ILogger {
@Override
public void logInformation(string logInfo) {
System.out.println(logInfo);
}
}
public class Foo {
private ILogger logger;
public void setLoggerImpl(ILogger loggerImpl) {
this.logger = loggerImpl;
}
public void doStuff() {
logger.logInformation("Something important.");
}
}
In this refactor, Foo
is no longer dependent on Logger
, but now utilizes the interface ILogger
- meaning that you can switch in and out implementations of ILogger at runtime, object instantiation, etc.
You could consume Foo
as such:
Foo foo = new Foo();
ILogger logger = new Logger();
foo.setLoggerImpl(logger);
foo.doStuff();
This would of course, print to console "Something important". Now what happens if you don't want to log to console, but to a database?
public class LoggerToDb implements ILogger {
@Override
public void logInformation(string logInfo) {
DbContext databaseContext = new DbContext();
databaseContext.insertLog(logInfo);
}
}
and could now be consumed as:
Foo foo = new Foo();
ILogger logger = new LoggerToDb();
foo.setLoggerImpl(logger);
foo.doStuff();
Note how nothing had to change in your Foo
implementation, because Foo
was not dependent on Logger
, but instead ILogger
- with this approach we can provide a new concretion to the abstraction, and swap it into Foo
without ever even touching Foo
! Pretty neato IMO.
Note in the above examples I'm building the object and providing an implementation, this could also be done with an IOC framework like Java's Spring.