You can call it ports and adapters or hexagonal architecture. Regardless, what's facinated me about this picture isn't the layers. It's the Clean Architecture UML diagram over in the corner of a plugin that looks like an upside down, folded over version of the Dependency Inversion Principle:
So while the original DIP only showed how to descend into layers this shows how to come back out without creating dependencies that point in the wrong direction.
If I were to color this to follow the clean architecture diagram it would look like this:
Some like to think of an interface like it's owned by the class that implements it.
Some like to think of an interface like it's owned by the client that uses it.
For the first time I'm starting to think either can be true. Both interfaces are really owned by the red inner layer. Rather than ownership of interfaces happening vertically or horizontally here it's happening diagonally. At least until you rotate around the "mechanism Layer" to get back to the clean architecture UML. Then ownership is vertical.
By ownership I mean what gets to dictate change. Neither Presenter or Controller designers get to dictate that the Input or Output port interfaces need to change. The Interactor designer gets to make that call. Which is why browser plugins are at risk of breaking every time a new version of the browser comes out.
I've written some example code that is just supposed to show how these layer plugins might look. Please check it for clarity. It's just a button push without much transformation or useful indirection so it might not be the best example yet. Ideas to improve that would be most welcome. One thing that's completely missing is an Entity.
Code listing presented in an order that follows the flow of control:
package candiedOrange.plugin.adapters;
import candiedOrange.plugin.usecases.ButtonUseCaseInputPort;
public class ButtonControler {
ButtonUseCaseInputPort button;
public ButtonControler(ButtonUseCaseInputPort button) {
this.button = button;
}
public void push() {
button.push();
}
}
package candiedOrange.plugin.usecases;
public interface ButtonUseCaseInputPort {
void push();
}
package candiedOrange.plugin.usecases;
public class ButtonPushUseCaseInteractor implements ButtonUseCaseInputPort {
ButtonUseCaseOutputPort outputPort;
public ButtonPushUseCaseInteractor(ButtonUseCaseOutputPort outputPort){
this.outputPort = outputPort;
}
@Override
public void push() {
outputPort.push();
}
}
package candiedOrange.plugin.usecases;
public interface ButtonUseCaseOutputPort {
void push();
}
package candiedOrange.plugin.adapters;
import candiedOrange.plugin.usecases.ButtonUseCaseOutputPort;
public class ButtonPresenter implements ButtonUseCaseOutputPort{
@Override
public void push() {
System.out.print("push");
}
}
Testing each layer seperately:
TestUseCases
package candiedOrange.plugin;
import candiedOrange.plugin.usecases.ButtonPushUseCaseInteractor;
import candiedOrange.plugin.usecases.ButtonUseCaseInputPort;
import candiedOrange.plugin.usecases.ButtonUseCaseOutputPort;
import org.junit.Test;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class TestUseCases {
public static class PushMock implements ButtonUseCaseInputPort, ButtonUseCaseOutputPort {
boolean pushed;
public boolean isPushed() { return pushed; }
public void push() { this.pushed = true; }
}
@Test
public void testInteractor() {
PushMock presentorMock = new PushMock();
assertFalse( presentorMock.isPushed() );
new ButtonPushUseCaseInteractor(presentorMock).push();
assertTrue( presentorMock.isPushed() );
}
}
TestAdapters
package candiedOrange.plugin;
import candiedOrange.plugin.adapters.ButtonControler;
import candiedOrange.plugin.adapters.ButtonPresenter;
import candiedOrange.plugin.usecases.ButtonPushUseCaseInteractor;
import candiedOrange.plugin.usecases.ButtonUseCaseInputPort;
import candiedOrange.plugin.usecases.ButtonUseCaseOutputPort;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class TestAdapters {
private final ByteArrayOutputStream outContent = new ByteArrayOutputStream();
private PrintStream oldStdOut;
@Before
public void setUpStreams() {
oldStdOut = System.out;
System.setOut( new PrintStream(outContent) );
}
@After
public void cleanUpStreams() {
System.setOut(oldStdOut);
}
@Test
public void testOut() {
System.out.print("hello");
assertEquals( "hello", outContent.toString() );
}
@Test
public void testPresenter() {
outContent.reset();
new ButtonPresenter().push();
assertEquals( "push", outContent.toString() );
}
@Test
public void testControler() {
TestUseCases.PushMock interactorMock = new TestUseCases.PushMock();
assertFalse( interactorMock.isPushed() );
new ButtonControler(interactorMock).push();
assertTrue( interactorMock.isPushed() );
}
@Test
public void testEndToEnd() {
outContent.reset();
new ButtonControler(
new ButtonPushUseCaseInteractor(
new ButtonPresenter()
)
).push();
assertEquals( "push", outContent.toString() );
}
}
Package structure:
The adapter package is the green layer with controllers, presenters and gateways. You might notice the only thing importing the adapters package was TestAdapters. This ensures that layer can be removed and replaced easily.
Looking to make the above into a readable, robust, and useful example. Critical input welcome.