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?

8
  • 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, 2018 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?
    – Elrond
    Apr 10, 2018 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, 2018 at 15:15
  • 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. Apr 11, 2018 at 12:57
  • 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. Apr 11, 2018 at 13:21

5 Answers 5

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.

1
  • 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.
    – Elrond
    Aug 21, 2019 at 9: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.

5
  • 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!
    – Elrond
    Apr 11, 2018 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, 2018 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?
    – Elrond
    Apr 11, 2018 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, 2018 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, 2018 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
1
  • Neat solution for when you can't introduce a third module, ie. for SQLAlchemy models.
    – kevlarr
    Aug 12, 2020 at 15:11
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()
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.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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