Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FEEDBACK] Further clarify the role of interfaces.py #11

Open
SHxKM opened this issue Aug 13, 2019 · 10 comments
Open

[FEEDBACK] Further clarify the role of interfaces.py #11

SHxKM opened this issue Aug 13, 2019 · 10 comments

Comments

@SHxKM
Copy link

SHxKM commented Aug 13, 2019

Hi. Thanks for your hard work on this documentation.

Even as a sole-developer wanting to refactor a fairly small Django application (12-15k LOC), I find value at least in the concepts offered by the approach presented here. It has set me on a path to better understand DDD, so I've also watched this talk which advocates services.py for writing to the database and selectors.py for reading from the database.

There's one part that I don't quite understand yet: the interface part. A few things are puzzling me about it and I hope to get some clarifications (I'd be happy to contribute to the docs if conclude the answer may be helpful to others). I hope I can articulate clearly.

Interfaces and DRY

We are told in the docs that if one domain needs to talk to another, it must do so using an interface that it cultivates. In other words, DomainA that wants to take some info from DomainB must do so strictly through DomainBInterface. But what about DRY?

Let's assume three domains: Artist, Album, User. (just like in the example docs, it could be argued that Artist and Album should live inside the same domain)

Assume that both Artist and User will want to fetch the same info from our Album, say albums within the last seven days. Does that mean that Artist and User must both write the same Album interface?

Below is some pseudo-code:

# user/interfaces.py
from album.apis import AlbumAPI

def get_latest_albums():
    AlbumAPI.get_latest()
# album/interfaces.py
from album.apis import AlbumAPI

def get_latest_albums():
    AlbumAPI.get_latest()

Doesn't this produce non-DRY code?

Interfaces and refactoring

Tying with what is brought-up above, it's still unclear to me why the fact we have interfaces.py saves us headache when refactoring.

If a method in domain_b.apis has to change, then we potential will have to refactor all DomainBInterfaces that were constructed in other domains (domain_a.interfaces.DomainBinterface, domain_c.interfaces.DomainBinterface, etc...).

Whereas if we consume on the DomainBAPI directly, we only need to change the method in one place? I'm sure there's something I'm missing here.

@phalt
Copy link
Owner

phalt commented Aug 14, 2019

Hey @SHxKM this is a really well thought out comment and I appreciate the feedback.

First off, I am assuming the second code example has a mistake and it should be artists/interfaces.py and not albums/interfaces.py. Because a domain's interface to itself is redundant. I'll go on the basis that this is wrong for the rest of my reply 😄

So you're right - those two examples are identical, and in this instance the are violating the DRY principle. If this was as far as we were going to take this project, that redundancy could be refactored. One of the core principles of DADs (I need a better acronym) is to make it easier for future developers to come in and expand features in place. I think perhaps the example given is not a real-world case. Let me explain.

Let's evolve with the example you gave: Artists and Users both want to communicate with Albums. We create the initial state:

# user/interfaces.py
from album.apis import AlbumAPI

def get_latest_albums():
    AlbumAPI.get_latest()
# artists/interfaces.py
from album.apis import AlbumAPI

def get_latest_albums():
    AlbumAPI.get_latest()

Awesome. We're violating DRY here, so maybe a developer will choose to not do this and refactor. That's probably okay for that requirement and it'll pass code review (even mine). If we didn't go any further with this, interfaces is redundant. We don't really need DADs.

But thi is a real-world evolving project, so we've suddenly got a new requirement:

  • Artists want only the following fields from albums: album title, listens, and sales.
  • Users want only the following fields from albums: album titles, and publishing dates. What's more, we want to decorate the album title with some weird string formatting because it's a millennial-esque website and we like fancy things.
  • They both want slightly different attribute names for each domain.

Now it should become clear why two interfaces is not redundant. The needs for Artists and Users from Albums is different:

# user/interfaces.py
from album.apis import AlbumAPI

def transform_albums(*, albums):
    return [
        {'title': f'*~-_-~{a.title}~-_-~', 'published_date': a.publish_date} 
        for a in albums
    ]

def get_latest_albums():
    albums = AlbumAPI.get_latest()
    return transform_albums(albums=albums)
# album/interfaces.py
from album.apis import AlbumAPI

def transform_albums(*, albums):
    return [
        {'title': a.title, 'listens': a.listen_count, 'sales': a.sales} 
        for a in albums
    ]

def get_latest_albums():
    albums = AlbumAPI.get_latest()
    return transform_albums(albums=albums)

What is the alternative here? We could develop these requirements inside the Albums domain, but then that domain will start having business logic for other domains within it. From my experience too, this breeds a "your problem" culture, especially when you own Albums, I own Users, and I don't want to have to maintain your stuff.

DDD talks about responsibilities and separating concerns. With DADs, the interfaces layer is that transformation layer that allows domains to remain pure, and the interfaces between them handling any required transformations.

Does this help clear up the reason behind them?

@phalt phalt changed the title [FEEDBACK] (fruther) clarify the role of interfaces.py [FEEDBACK] (further) clarify the role of interfaces.py Aug 14, 2019
@phalt phalt pinned this issue Aug 14, 2019
@phalt phalt changed the title [FEEDBACK] (further) clarify the role of interfaces.py [FEEDBACK] Further clarify the role of interfaces.py Aug 14, 2019
@SHxKM
Copy link
Author

SHxKM commented Aug 18, 2019

@phalt Thanks for the detailed clarification. I have a follow-up question:

so maybe a developer will choose to not do this and refactor. That's probably okay for that requirement and it'll pass code review (even mine). If we didn't go any further with this, interfaces is redundant. We don't really need DADs.

But thi is a real-world evolving project, so we've suddenly got a new requirement:

But what if there really isn't a new requirement? What if this get_albums() func is really needed as is in say, 3 places? My question then is, where do you (or would you) place it?

Thanks again for this refreshing methodology.

@phalt
Copy link
Owner

phalt commented Aug 20, 2019

But what if there really isn't a new requirement?

If it's not proving useful then it might be overengineering. I'd clean it up. There isn't anything wrong with anticipating future changes and then, when you get to the future, discover there isn't any. Software is fluid and changes. We'll just adapt to what makes sense for the situation currently. If we discover we need to add it, we just put it back in. This is what the "pragmatism" bit means at the start of the guide.

Thanks for your comments, it's helped me challenge my assumptions!

@SHxKM
Copy link
Author

SHxKM commented Aug 20, 2019

Thanks @phalt. Your positive attitude is quite refreshing.

I wasn’t trying to challenge your assumptions per se, more like trying to pick your brain. So if you do have a general rule of thumb regarding this question, I’d be glad to hear your thoughts:

My question then is, where do you (or would you) place it?

@phalt
Copy link
Owner

phalt commented Aug 20, 2019

No probs. I'm just keen to have people interested in a thing I developed from all my hard work 😆

It sounds like the interfaces could be promoted into it's own domain, sort out like a gateway or orchestration layer. I mentioned that some domains can just have services, and their only job is to co-ordinate or abstract other domains beneath it. Perhaps that could fulfill the same requirements we have here?

@SHxKM
Copy link
Author

SHxKM commented Aug 21, 2019

Perhaps that could fulfill the same requirements we have here?

I'm a little hesitant to make a concrete suggestion as I have very little experience with DDD or DADs, but perhaps a reluctantly populated common module can be introduced in cases where a coordinating layer doesn't really fit in one of the domains in the project? common/interfaces.py?

@phalt
Copy link
Owner

phalt commented Aug 21, 2019

Sounds like a good suggestion :)

@SHxKM
Copy link
Author

SHxKM commented Aug 21, 2019

Great. Thanks for your responsiveness. Feel free to close this :)

I’m happy to contribute to the docs if you feel that should go somewhere in there but I do feel this is an edge-case, as you noted.

@phalt
Copy link
Owner

phalt commented Aug 21, 2019

I've pinned this discussion because I think it is useful for others who are keen on the guide but don't know what to do!

@phalt
Copy link
Owner

phalt commented Mar 2, 2021

Hey @RTS340 yes I have had issues like that in the past. This isn't a problem with Django API Domains per say - you can end up with them in many ways. Usually circular imports are usually due to design choices in the past create circular dependencies.

I have two suggestions, one easy, one more complex:

  1. Easy option - put your imports at the top of functions instead of a file - then the import will only be evaluated when ran, and this usually avoids circular dependency issues.
  2. Refactor and redesign your components to have a 3rd "parent" domain that co-ordinates both.

The first way is easy but inevitably feels hacky, and that's because it is a little bit.

The second way is a much better approach, but costs time and effort - which understandably we don't always have!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants