3

I understand if 2 classes have circular dependency, eg:

public class MyWindow{
    public MyWindow(){
        new MyDialog(this);
    }

    public onDialogResponse(int option){
    }
}

public class MyDialog{
    MyWindow myWindow;
    public MyDialog(MyWindow myWindow){
        this.myWindow=myWindow;
    }

    public void onClicked(int option){
        this.myWindow.onDialogResponse(option);
    }
}

we can break circular dependency by creating an interface, eg:

public interface IWindow{
    void onDialogResponse(int option);
}

public class MyWindow implements IWindow{
    public MyWindow(){
        new MyDialog(this);
    }

    public onDialogResponse(int option){
    }
}

public class MyDialog{
    IWindow myWindow;
    public MyDialog(IWindow myWindow){
        this.myWindow=myWindow;
    }

    public void onClicked(int option){
        this.myWindow.onDialogResponse(option);
    }
}

but my question is, if IWindow is used once just for breaking circular dependency, is it worth to do that? because I think although circular dependency looks bad, breaking circular dependency in this case results in more complex code as an extra interface is created. Is there any practical motivation or usage to create a interface just for breaking circular dependency like this case?

  • 2
    I'd say removing a circular reference it's worth almost anything. – Zalomon Jun 16 '17 at 9:38
  • The circular dependency is only an issue if you need to put the different classes in different libraries, or they belong to different layers (layering violation). – Frank Hileman Jun 16 '17 at 17:08
4

Is there any practical motivation or usage to create a interface just for breaking circular dependency like this case?

Yes there is. Introducing an interface decouples MyDialog from a dependency on MyWindow. This is useful for several reasons:

  • As you've observed, it prevents a circular dependency
  • It allows you to unit test MyDialog in isolation by mocking IWindow
  • It uses a common "listener" pattern that is used throughout most UI's for components to notify clients when events occur. (this justification would be more sound if IWindow were named something more meaningful like IDialogListener). In this case it doesn't have to be used only once. You can continue to use this listener pattern for all your dialogs.
  • It doesn't limit use MyDialog to MyWindow. MyDialog can now be reused by any component that implements IWindow.
  • MyDialog can be provided anonymous implementation of IWindow instead of having to implement onDialogResponse() in MyWindow. This allows MyWindow or any other component to create multiple instances of MyDialog and handle their results independently. For example:

public MyWindow() {
    MyDialog dlg1 = new MyDialog(new IWindow() {...});
    MyDialog dlg2 = new MyDialog(new IWindow() {...});
}
  • In this example, there is no decoupling by adding the interface. There is a strong coupling chain; the only thing decoupled is the instantiation, which would be valuable only if the interface and class were in separate assemblies. – Frank Hileman Jun 16 '17 at 17:09
  • My example was to show that by adding an interface you could instantiate two separate dialogs and handle their results independently. This is not possible with OP's first snippet. There is in fact decoupling because compared to OP's first snippet, MyDialog is no longer coupled to MyWindow. The coupling used to be MyDialog <--> MyWindow, but after adding the interface the coupling is MyWindow --> MyDialog. I agree that more decoupling should be done, but that doesn't really relate to OP's question which was specifically about the introduction of IWindow – Samuel Jun 17 '17 at 0:02
4

I would agree that introducing an interface here is not too helpful. Samuel provides some legitimate benefits, but in my mind the interface is more sweeping the design problem under the rug. The real problem is that Window and Dialog have been tightly coupled. This coupling is done in two ways

  1. The window directly instantiates a dialogue.
  2. The dialogue calls back into a window method explicitly designed to be called by the dialogue.

It's both links together that lead to circularity, but I think both links are individually worth breaking. We could break the first link by using dependency injection, though without knowing more context it's not clear what the relationship between the dialog and the window should be (must the window have a dialog, does it need the ability to produce a dialog on command, can they instead be free floating, etc.) so one cannot be too concrete. In the other direction, an event based option is a natural and popular choice to decouple UI components. In this case the dialog just announces (e.g. publishes to a message queue) "I, the dialog, have been clicked with this option" and the window or some other subscriber can decide what to do with that information.

How elaborate and frameworky you want to go with this depends on the complexity of your application. If all you foresee having is one window and one dialog that fit in a couple of screens, then honestly you can keep the current design. In general, though, I think you should not add interfaces to paper over a bad design but to add flexibility to a strong design.

  • 1
    I agree both directions of the coupling need to be addressed. MyWindow creating its own dependency is a no-no – Samuel Jun 16 '17 at 5:54
2

Although there are many good reasons to use an interface, your example isn't one of them.

Circular dependencies often highlight over coupling in our design.

In your case Dialogue should expose an event hander Click which Window then assigns to rather than knowing about its parent.

This allows it to be used in more places than just a Window.

If you feel Dialogue should control what happens when its events fire then the interface you inject should be tailored to that task. ICanHandleClick or something, which then implies it can be used generally. Not just Window :IWindow

  • Events provide more decoupling than interfaces, and this is why events were created for .net. Good answer. – Frank Hileman Jun 16 '17 at 22:00

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.