0

As a 3rd-party, I'm writing a program to interface with a webservice. The problem I've run into is that some classes end up being as commonly used as ints/strings (e.g. User), so I need to be careful with which responsibilities I give each class to prevent mega objects which depend on every other mega object.

The best example is the Group class (think Facebook group). A Group has an AuditLog (component) which contains LogItems. These LogItems specify target of an action, among other things such as time/etc. Sometimes the target of these logs is another Group, so the AuditLog needs to know how to create a new Group object from the data given by the webservice's response. This results in a circular dependency where Group needs to know how to create its component, AuditLog, and AuditLog needs to know how to create Groups.

So how would I resolve this? I could use dependency injection to pass an AuditLog object to the group upon creation, but this is detrimental to abstraction because the user has to know how the group object links together internally. This also becomes infeasible when there are many components that have to be added to the Group object this way. I could instead make a static class for AuditLog:

from group import Group
from auditlog import AuditLog

ourGroup = Group(ourGroupId)
adLogs = AuditLog.getLogs(ourGroup, logType=Advertisement)
groupsWeAdvertised = [log.target for log in adLogs if isinstance(log.target, Group)]

This is a lot better, but still doesn't seem ideal. AuditLog is a component of Group -- it is only used by Group, and it has no purpose without a Group, so breaking it off into a top-level class seems awkward. There's no way to easily know that this is a child object of group (it can't exist without Group) rather than some top-level service.

Is there a more ideal way to decouple this system? I looked over various Software Patterns and spent yesterday looking for similar problems on softwareengineering SE, but all solutions I found either involve breaking abstraction by requiring the user to manually initialize components of Group or decouple it entirely to where there's maybe not enough coupling to be healthy like in the example above.

Note: I am using Python, so even if circular dependency is somehow ideal, Python3 does not allow it.

1

You could convert the target attribute into target_type and target_id, and this will have the following advantages:

  • LogItem will have the same attribute types for all instances and can be reused for anything more easily
  • LogItem stops depending on Group
  • Group can host the logic of parsing through the logs and creating Group instances where appropriate, ending the circular dependency.

Still, it feels that this will be a suboptimal design because your classes, particularly Group, seem to know/do too much, and without dependency injection it will not be possible to test any of this code effectively.

A better solution would be to do the following:

  • have a GroupRepository capable of returning a group given its id
  • have a factory that can give you a GroupRepository, so that the details of building one exist in only one place and are abstracted
  • move all the logic of querying audit records to AuditLog (one method for each query you need)
  • move the motive to to make these things talk to each other to the layer above (e.g. Controller), where you'll have the GroupRepo and AuditLog instances injected, and use the GroupRepo to resolve Groups in the individual logItems as needed.

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.