14

To give some context, I'm using python. We have the following package structure:

/package_name
    /request.py  # defines class Request
    /response.py  # defines class Response

Let also assume that we have a bidirectional dependency at class and module levels:

  • The method Request.run returns a Response instance.
  • Response needs a Request instance to be __init__ialised.

My naive solution would consist defining the abstract class IRequest and IResponse to be stored in the respective modules. As a result

  • Request will implement IRequest and depend on IResponse
  • Response will implement IResponse depend on IRequest

so the circular dependency at class level is defeated.

However, we still have a bidirectional dependency at module level, which prevents the modules to be imported when using python. The bidirectional module dependency can be solved storing the abstract classes in a third module say interfaces.py, but it sounds a dodgy solution to me.

Question: Am I violating any architectural principle? What are possible solutions?

MLguy
  • 301
  • 1
  • 3
  • 7
  • Is there an actual circular dependency between modules? It seems like you don't need the response.py module to import request.py, as it gets the Response object injected in initialized state. The circular dependency between the classes is also not obvious. Of course, Response relies on some interface being implemented by Request, but that doesn't imply dependency on the class itself. – Frax Apr 10 '18 at 14:35
  • I need `from request import IRequest` to document the expected input types, so `response.py` dependes on `request.py` (circular dependency). Could you extend your last sentence here or in an answer? – MLguy Apr 10 '18 at 14:52
  • Document? You mean, add type hints? Or something else? Seeing python I assumed untyped code, and in this case the problem would be inexistent. – Frax Apr 10 '18 at 15:15
  • Yes, I mean type hinting. I've been living assuming that if object `O` is injected in class `C`, than in a dependency graph `C` depends on `O` or at least on its interface/ABC if available. I find weak to explain in words with a comment or docstring the expected behaviour of the injected object, so I'm using type hinting. – MLguy Apr 10 '18 at 15:22
  • Since this is Python, some circular imports can be solved by just moving the import statement from the top of the file down to a later point just before it is needed, inside a function for example. Whether this helps in this particular case I'm not sure. – Sean Burton Apr 10 '18 at 15:48
  • 1
    A completely different approach can handle your conundrum. Have a third entity like `Context` or something like that will return the `Response` when you send a `Request`. That way there is no direct dependency with `Request` to anything else. Your `Client` and `Response` objects themselves would have a clear dependency structure. – Berin Loritsch Apr 11 '18 at 12:57
  • @BerinLoritsch interesting idea, but I think that it's overkill to abstract logic in another layer of code (`Context`) just because I don't quite having modules like `irequest.py` or `interfaces.py`. – MLguy Apr 11 '18 at 13:17
  • 1
    Not knowing the specifics of your request/response infrastructure, your `Context` or `Client` can help manage default values for your request objects and introduce the concept of a session if you need to in the future. Still worth a thought. – Berin Loritsch Apr 11 '18 at 13:21

5 Answers5

13

There are two main ways to deal with circular dependencies:

  • Hide it with interfaces
  • Add an intermediary object

In this particular case I would recommend the second option. Your module would have something like this:

/package_name
    /client.py
    /request.py
    /response.py

The new Client object would actually run the request. This moves Request.run to Client.run(request). That makes your Request object completely independent (no dependencies).

The next question is whether or not your Response object actually needs the Request passed in. Would you be able to get the same effect if you simply passed some of the values in? You have this option now because your Client object is responsible for initializing the Response.

So now your dependency hierarchy is:

Client->Request, Response
Response->Request? -- depends on answer to above question
Request

This is similar to the way a number of HTTP APIs are designed. The Client can cache default values to inject into the request (i.e. default headers) and perform other services.

Berin Loritsch
  • 45,784
  • 7
  • 87
  • 160
  • 2
    Accepted this answer because it's agnostic with respect to the programming language. However, specifically for python, my answer is preferable if you consider creating a third class `Client` overkill for your application. – MLguy Aug 21 '19 at 09:05
8

Thinking of an untyped code, if the Response gets the Request object injected, but doesn't create one on it's own, it doesn't depend on the Request class. It depends just on it's interface (which may not have any specific representation in code, but nevertheless definitely exists). So in that case the class dependency is not really circular.

To capture that state in the typed code, write the interface classes IRequest and IResponse, and put them in their own modules irequest.py and iresponse.py. Alternative naming, likely better showing the logic underneath, is to call the interfaces Request and Response, and the implementing classes RequestImpl and ResponseImpl (and change module names accordingly, of course). Apparently, there is no established naming convention for interfaces in Python, so the choice is yours.

The reason for splitting the modules is simple: if there is no logical dependency on a class (Request class in this instance), there should be no code dependency on it's implementation. It also makes it clear that other implementations may exist and be valid, and make it natural to add them (without blowing the single module to enormous size).

This is a standard and well-known practice. Some statically typed languages, like Java, may actually enforce it. In other it's just an informal, but encouraged standard (header files in C++). Look at any Java code for examples, like the Collection interface in OpenJDK. In Python, where static typing is a very new concept, there is no established practice, but you can expect one to emerge, and to follow the existing examples.

Practical note: As the Request uses Response class directly, you should probably save some lines by putting IResponse in the response.py module or just skipping the interface altogether - you can always add it when necessary.

Frax
  • 1,844
  • 12
  • 16
  • As mentioned in the question, to me it sounds weird to place `IRequest` outside `request.py` (e.g. `interfaces.py` or even better `irequest.py` as you suggest) because the developer **must remember** that classes in `request.py` must implement an interface located somewhere else. If you can provide some evidence that this is not bad practice, I'm happy to accept the answer! – MLguy Apr 11 '18 at 12:08
  • Added an explanation and an example in the answer. As for remembering: it should be very natural thing, as this is what happens all the time. Also, I guess the type hints should do a pretty good job at reminding that. At least, real static typing would. – Frax Apr 11 '18 at 12:49
  • What would you prefer between: (1) Leave `IRequest` in `request.py` and avoid using type hinting for the injected `Request` instance in `Response` or (2) Move 'IRequest' in `irequest.py` and use type hinting for the injected `Request` instance in in `Response`? – MLguy Apr 11 '18 at 13:21
  • Between these two, definitely (2). If you create the interface, by all means use it for type hinting. What's the other point of having it? OTOH, don't treat these things too seriously. Try doing it one way and see how it works and feels like, then maybe try the other one. It's not that big deal, actually. And it's not a big deal to get it wrong one time or another, it just happens. – Frax Apr 11 '18 at 13:52
  • Btw, consider the aswer by Berin Loritsch. He suggest a superior design, with a cleaner separation of concerns, that actually _avoids_ a circular dependency. It is not going to work for every case, but when it works, it's almost surely better. – Frax Apr 11 '18 at 14:07
5

The solution below allows compiling the Python code while avoiding the creation of a third module which reduces readability IMO. Python type hinting request.IRequest enabled code completion on IDEs (source). This feature might be very specific to Python, but it has all the advantages mentioned in other answers but without creating a third module or class (e.g. Context or Client), so lower cognitive load for the developer.

# [request.py]
from package_name.response import IResponse

class IRequest: pass

class Request(IRequest):
    def run(self) -> 'response.IResponse'
        ...

# [response.py] - Compiles
from package_name import request

class IResponse:
    def __init__(self, request: 'request.IRequest'):
        pass


# [response.py] - Doesn't compile
from package_name.request import IRequest

class IResponse:
    def __init__(self, request: IRequest):
        pass
Anil
  • 103
  • 1
  • 6
MLguy
  • 301
  • 1
  • 3
  • 7
2

Assuming you are never going to instantiate and return a response.Response without first instantiating a request.Request... You should be passing the request.Request instance to the response.Response constructor/initializer.

As in: response.Response(request.Request("some_request_var"), "other_var", another_var="another_var")

The response module will no longer (should not have) a dependency on the request module. The request.Request class/instance variables can be used to init response.Response class/instance variables then thrown away, or saved/referenced in its entirety (response.Response.request)

Unless the response module or the response.Response class is creating/initializing instances of request.Request there is no reason to have the request.Request class in the response module's namespace (a.k.a. import X, a.k.a. circular dependency)

If you truly have a circular dependency... You can use late imports. First assign None to the module level variable Request and or Response. This variable would normally store your reference to your import. Then inside a function/method at the module/class level do global Request or global Response then your import statement. Two common approaches are to have the class.__init__() check for None and if None do the import or have the package.__init__.py import your modules then call a late import function.

Examples:

# In response.py
Request = None
class Response(object):
  def __init__():
    global Request
    if Request is None:
      from request import Request

# In __init__.py
from request import Request, late_import as request_late_import
from response import Response, late_import as response_late_import
request_late_import()
response_late_import()
amon
  • 132,749
  • 27
  • 279
  • 375
Kevin
  • 21
  • 3
1

Not strictly tied to Python, you can use, as a general rule, the Dependency Inversion Principle to remove the problem of the mutual "exclusion" whenever it's a problem to have a statically-typed dependency between the types. The trick is to add an abstract class or interface for at least one of them (you can create an interface for both, but that may be overkill and counterproductive for their maintenance).

For instance: A <-> B would become A -> IB ; IB <-refines- B ; A <- B

Now, about Python, there's no problem in this regard as it's not statically-typed.

However, it's important to note that no matter which solution you take, you have to make sure there's integrity in that circular relationship. That is, if you remove q1 as the question of response r1, q1 has to remove r1 as its response, etcetera. This is the really tricky part and why you want to avoid circular references whenever possible.