11
\$\begingroup\$

enter image description here

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:

enter image description here

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:

enter image description here

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:

enter image description here

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.

\$\endgroup\$
4
\$\begingroup\$

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.

It is true. The difference lies in semantics.

Interface owned by its own class

The core is: EVERY class has an interface aside the language construct "interface". The problem is: Sometimes it is not beneficial.

If you have a class in JAVA sometimes it is hard to ensure encapsulation. Therefore you hide the class behind an "interface" language construct.

The separate interface of a class is only an extraction of the interface the class already implicitly has. You can compare it to the header file in the programming language "C" where it provides perfect encapsulation.

In JAVA it is a helper construct which is not enforced. So nothing hinders you to use the concrete class implementation instead of the interface. In "C" you only make the header files known to other compilation units (I hope so, as I am not a C-developer).

Interface owned by the framework

The other way around: A framework provides functionality. To let your object cooperate with the framework they have to meet some requirements that are formulated as "interface" language constructs.

Here comes the complicatet thing:

You may have a class with an extracted alternative interface AND you want to have a contract to work together with a framework. Then you have two interfaces with different semantics to handle.

The clean architecture

If you consequently follow the SOLID principle it will guide you to exact this architecture. You have no chance to miss it.

Your code

In general I do not think that pressing a button is a usecase but I think you have a correct reference implementation of clean architecture.

\$\endgroup\$
  • \$\begingroup\$ You are right about header files in c. You're also making me think I need to be clearer about what I mean by ownership (see edit). As for a button as a usecase, I'm trying to keep the example simple. Would it help if the system did more than just pass on knowledge that the button has been pushed? I have a variation that gives the button on and off states rather then being a momentary push button. \$\endgroup\$ – candied_orange Feb 20 '17 at 14:31
  • \$\begingroup\$ I know that you want to keep the usecase small. Extending the usescase would not change my answer :-). A class always owns an interface. You can extract it into a separate construct "interface" and it will semantically remain in ownership of the class. If you want to use a framework and you want the framework work with your objects then the framework decides about the rules it accepts your objects. You have to meet interface contracts defined by the framework. \$\endgroup\$ – oopexpert Feb 20 '17 at 16:48
  • \$\begingroup\$ I disagree. The owner of an interface isn't about semantics. Just because you name your class Dog doesn't give you authority over the Pet interface that my client uses. \$\endgroup\$ – candied_orange Feb 20 '17 at 16:57
  • \$\begingroup\$ You didn't match my case. Dog to Pet is a semantical (and technical) abstraction. Dog.cpp to Dog.h is separating the interface from the implementation of the exact same semantic. It's only a technical abstraction to hide internals. \$\endgroup\$ – oopexpert Feb 20 '17 at 17:05
  • \$\begingroup\$ Well that's just silly. That means interface ownership can only happen in languages that allow implementations and interfaces to have the same name. No. Ownership is not such a simple problem that the names are all you need to know. \$\endgroup\$ – candied_orange Feb 20 '17 at 17:18

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.