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?

Elrond
  • 301

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.

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,874
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
Elrond
  • 301
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
  • 135,795
Kevin
  • 21
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.