aashayamballi / django_clean_architecture

0 stars 0 forks source link

Clean architecture discussion #1

Closed BasicWolf closed 1 year ago

BasicWolf commented 1 year ago

Hi @aashayamballi,

Great to see people adopting clean architecture! I suppose your code is based on Uncle Bob's vision of the topic, as described in this article: https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html

Now, let's consider your code at the point of the second commit (57991a9c3f545b87017672e09112c42c83b3bdb1). We see a Django HTTP GET view which returns JSON-serialized data from the database. There is absolutely no need for Clean architecture in this case. And you can clearly see that from your code: all that is happening between querying the database and return Response(...) - are transformations from one kind of object to another. Do you really need all that?

The best code - is the code that you don't have to write nor maintain. If someone pushes you to do "Clean architecture way" - think of separation of concerns, CQRS (Comman Query Responsibility Segregation way):

  1. A simple Django-tailored code that reads data i.e. handles HTTP GET and reads from the database. That can even be a separate application/microservice.
  2. A more architecturally sophisticated code that accepts Commands (POST/PUT/PATCH) and writes to the database. This is where Clean architecture may take place. I say may, because if all that is needed is to take JSON data push it to a database table - you don't need Clean architecture either - you need a HTTP API front-end for the database.

With all that said, when do you need Clean architecture? Think of the business domain. Are there commands and business rules? Are there downstream services (database, message queues, sockets, emails, etc.) that you application is calling? If yes, go the Clean architecture way, BUT - very important - start with the Domain/Business logic/Use cases. All you need here are plain Python classes, since you probably go OOP way, no need in Pydantic. This is the framework-agnostic heart of the application.

With a use case ready, you can attach a Django View to send commands to the use case. You don't need a database at this point yet - make a fake repository that returns some fixtures. That works? Good, now try attaching FastAPI or Flask, pure WSGI or anything else - this is the ultimate test whether the Domain layer is truly framework-agnostic. The last step - make the real Django-based repository. DRF Serializers nicely handle nesting and relationships, there is no need in converting and parsing JSON in the middle, please avoid that!

Hopefully I didn't make things harder :)

Regards -- Zaur

aashayamballi commented 1 year ago

Thank you Zaur for your inputs.

At my work place I have been told to write code framework agnostic so that if they want to maybe switch from django - FastAPI/Flask/any python frameworks then it should be easily doable.

So my lead architect is from .NET background his main concern is that in Django we tightly couple our code and Django actually kind of encourages you to do so. So he's completely against this and he's been telling me to kind of create a seperate service layer, repository layer, entity layer etc. Which i think is an overkill for a django projects since Django has its pragmatic design. Previously i have been following HackSoft's style guide https://github.com/HackSoftware/Django-Styleguide while writing my Django code again my lead doesn't want me to write this code as per this style guide.

While researching on how can I achieve all these, then i came across uncle Bob's clean architecture pattern. After going through few articles, videos and blogs I assumed that this is what my lead architect was telling me to do.

So the basic sample app that i have created i just want to know is this the correct pattern? So in the code if you see I've actually created these seperate layers usecase, repository, entity, which are not dependent on any framework (flask/django/FastAPI) and maybe as my lead suggested if they want to move to a new framework then it's easily doable. I am using Pydantic just for entity layer which actually converters Django querysets to an entity. Also my lead architect says that with django we cannot write pure unit test cases and it's kind of an integration test.

So after researching about clean architecture i got know that we can write pure unit test cases as well with Django if we follow clean architect pattern.

Also the kind of projects that are done at my organizations are like short term projects, we develop for 3-4 months and maybe 3-4 Dev's work on it so i felt applying clean architecture here is also an overkill.

But again as I have no control I'm unable to convince this to our management that clean architecture is good but kind of projects that we're developing or delivering doesn't need clean architecture.

And as per your last statement, i have used DRF serializers in the DB/Django repository layer. So wanted to know in the DB/Django repository if i use the DRF nested serializers then how can I convert them to an entity? My approach was use DRF serializers for converting the querysets to orderdict -> json -> Pydantic class (entity). So this entity will be used in the usecase or service layer.

So overall i just want to know whatever i have done by creating this basic sample app is it the correct way or not? I know i can ask this to my lead architect only but there have been few conflicts so before going to him i just want to get help from someone who knows really about this framework agnostic code.

BasicWolf commented 1 year ago

Oh, the situation is very very unfortunate. I would ask the architect, does he really believes in switching from Django to FastAPI in a middle of 3-4 months long project? Although I believe that Clean Architecture can be applied in small and huge products alike. It's not the size, but a need that makes it reasonable. I hope you would come to a sensible agreement!

But hey, back to the tech stuff. I still think that queryset -> json -> Pydantic is a little bit an overkill. Pydantic is ok, but I think for this matter it's an overkill. Unless you're going to do any validation with it :) I consider a manual mapping between queryset --> entity a more verbose, but much more explicit solution.

In one PR, with comments: https://github.com/aashayamballi/django_clean_architecture/pull/2/files

aashayamballi commented 1 year ago

@BasicWolf thank you for the response and apologies for my delayed response.

his main intention is basically that he's not in favor of the mentioned style guide (https://github.com/HackSoftware/Django-Styleguide) and also he says that with a clean architecture pattern, we can write pure unit test cases. because he doesn't want me to write a kind of integration test where we connect to the DB to insert the data and do the assertions on querying the test DB.

Well, I told my architect that the kind of project we are building for that clean architecture is just overkill, but unfortunately, I was unable to convince our lead and strictly he told me to follow whatever he's been telling me to do.

anyways coming back to the technical stuff, I agree that queryset -> json -> entity(pydantic) is overkill, but in my real project, I would be getting nested data with Django's reverse relationship/M-M mapping. which would look like below

[
    {
        "name": "Stories",
        "posts": [
            {
                "title": "My first story",
                "content": "She opened the door and..."
            },
           {
                "title": "Story about a dog",
                "content": "I met Chase when I was..."
            }
        ]
   },
   {
        "name": "Stories-1",
        "posts": [
            {
                "title": "My first story-1",
                "content": "She opened the door and..."
            },
           {
                "title": "Story about a dog-1",
                "content": "I met Chase when I was..."
            }
        ]
   }
]

either I have to write nested loops to get this kind of response or use DRF's serializers, convert to JSON, and then to an entity. I felt writing a nested loop is not the best way to do it since time complexity wise it's going to be O(n²) (if the nesting is deep then more nested loops O(n^n).Also, I am not very sure how the DRF serializer does this nested conversion.

So if it was a flat queryset then with just list comprehension itself we would be able to convert it into an entity as how you have done it here - https://github.com/aashayamballi/django_clean_architecture/pull/2/files#diff-da2bd89f7b338884c816c7d8d8abc61c7401b095d03ce6fb7bc3c98357c9be0a

since we'll be having nested data for that reason I used DRF's serializer to convert that to JSON and then to an entity (Pydantic).

Could you please advise me on this?

BasicWolf commented 1 year ago

Hi @aashayamballi, I guess the architect implies domain / business logic tests, when talking of "pure unit tests". That's one of the main points of Clean architecture, but if the architect is mostly worried about "not writing to test database", why not use Testcontainers? Here is a good place to start: https://github.com/wonkybream/django-rdtwt

About fetching data from DB in Clean architecture - you can do this the same way as if there was no Clean architecture. DRF Serializers are not a silver bullet, they don't make any magic fetching data with o2m or m2m relationships. Unless you optimize the queryset, e.g. via using select_related() or prefetch_related(), DRF will be ineffective. See the second note in https://www.django-rest-framework.org/api-guide/relations/ Once you have a fine-tuned queryset, there are two ways: either use a DRF serializer to map query data to a dictionary (i.e. my_model_serializer.data) and then construct an entity by destructing that dict: MyEntity(**my_model_serializer.data). However, since you already evaluate a complex queryset, why bother with serializers in the first place? Unless they provide some specific benefit, just use the queryset results and map them to the corresponding entities. If you are prefetching the related data, there would be no penalty on converting it to domain entities. Bottom line, it should be something like:

queryset = MyModel.objects.query(...).prefetch_related(...)
my_entities = [map_to_entity(model) for model in queryset]
aashayamballi commented 1 year ago

hello @BasicWolf, thank you for the detailed explaination.

My main query is converting the queryset to corresponding entities using list comperhension for the nested data.

for example lets's say I have below models

class Question(models.Model):
    question_text = models.CharField(max_length=255)

class Choice(models.Model):
    question = models.ForeignKey(Question, on_delete=models.CASCADE, related_name="question_choice")
    choice = models.CharField(max_length=255)

with this model if I want to generate the output like below

[
    {
        "question": "Question 1",
        "choices": [
            {
                 "id": 1,
                "choice": "Choice 1 of Question 1",

            },
           {
                 "id": 2,
                "choice": "Choice 2 of Question 1",
            }
        ]
   },

        "question": "Question 2",
        "choices": [
            {
                 "id": 3,
                "choice": "Choice 1 of Question 2",

            },
           {
                 "id": 24
                "choice": "Choice 2 of Question 2",
            }
        ]
   },
]

I have to write the queryset and the serializers like below (Here I'm using basic serializer and not model serializer)

class QuestionChoiceSerializer(serializers.Serializer):
    id = serializers.IntegerField()
    choice= serializers.CharField()

class QuestionSerializer(serializers.Serializer):
    choices= QuestionChoiceSerializer(many=True, source="question_choice")
    question =  serializers.CharField()
question_choice_qs = Question.objects.all().prefetch_related("question_choice")
serialized_data = QuestionSerializer(question_choice_qs, many=True).data

so the above queryset and serializer would give me the desired resultset with ease.

If I wanted the same results in the clean architecture pattern without using serializers then I should have done something like below.

from dataclasses import dataclass

@dataclass
class ChoiceEntity:
    id: int
    choice: str

@dataclass
class QuestionChoicesEntity:
    question: str
    choices: list[ChoiceEntity]
class QuestionChoiceDBRepository:
    def get_question_choice(self) -> List[QuestionChoicesEntity]:
        question_choices_qs = Question.objects.all().prefetch_related("question_choice")
        return [
            QuestionChoicesEntity(
                question=instance.question, choices=self._get_all_question_choices(instance)
            )
            for instance in question_choices_qs 
        ]

    def _get_all_question_choices(self, question_instance) -> List[ChoiceEntity]::

        choices = question_instance.question_choice.all()

        return [
            ChoiceEntity(id=choice .id, choice=choice.choice)
            for choice in choices
        ]

so here if you see in the get_question_choice method I'm using the list comperhension to convert the queryset to an entity, but also if you look at the list comperhension for the choices I'm actually calling one more helper method to get the choices of a particular question. so in the _get_all_question_choices I'm again using list comperhension to transform the choice queryset to ChoiceEntity.

so if we now talk about the Big O Notation time complexity of this, it is O(n^2). Because I have written the nested loops. - https://www.bigocheatsheet.com/

in the case of serializers I did not have to write/worry about writing the nested loops in order to generate the data in the desired JSON format ( again I'm not sure how DRF's serializer do this behind the scenes). but If I wanted do it by hand then I kind of had to to write nested loops. so this is what I want to avoid because in worst case it is O(n^2). That is the reason why I was doing querysets -> orderdict (serializers) -> json -> Pydantic/ dataclass (entity)

Have you encountered this kind of situation? because this is a simple case where I have 1 level of nested data. If in case I had maybe 3-4 level nested data then I had to write 3-4 nested loops. I just want to make the time Big O Notation time complexity of this to O(n).

Can you please advice me on this?

Thank you.

BasicWolf commented 1 year ago

Hi,

I think you are overthinking this a little bit. As Donald Khuth said,

Premature optimization is the root of all evil (or at least most of it) in programming.

In other words - first make it work, then make it effective. Unfortunately many programmers stop after the first part - and never improve the initial implementation. That being said, queryset -> dict -> json -> dataclass is still a very ineffective way of converting data, since we can throw away the two intermediate steps, and still make it work. You have already written down everything yourself!

Loops and O^2 might feel uncomfortable, but this is not some heavy computational algorithm - it's in-memory data mapping from one format to another. As a matter of fact, this is exactly O(n) mapping, no matter how many nested loops are there. It's mapping of one data graph into another and you have to traverse over all its nodes. Considering that there are n nodes altogether (for this case of questions and choices n = q*c), that's O(n) mapping you'll be doing. That complexity is equally the same for a dict -> json or a json -> object (dataclass) converters.

Last, but not least: have a look into how DRF Serializers. I bet that you'll be surprised. Hint: check ListSerializer.to_representation(...).


P.S. If you have Entities which only reflect your database models AND have no related business logic, skip the entities completely and return a dict out of the queryset. This might be the case when fetching data (think of HTTP GET). If the queryset results are going to end up as JSON in HTTP response without any modifications on the way, use Django queryset's .values(), like list(question_choices_qs.values()) - no entities needed.

aashayamballi commented 1 year ago

Hello @BasicWolf,

Got your point on my concern on time complaxity and I believe it's okay to write nested loops since we're not doing some heavy computation. You gave the best anology i.e. n = q*c i.e. O(n)

Also I looked at the DRF's ListSerializer.to_representation(...) code which looks like this.

 def to_representation(self, data):
        """
        List of object instances -> List of dicts of primitive datatypes.
        """
        # Dealing with nested relationships, data can be a Manager,
        # so, first get a queryset from the Manager if needed
        iterable = data.all() if isinstance(data, models.manager.BaseManager) else data

        return [
            self.child.to_representation(item) for item in iterable
        ]

and I belive they are doing the same nested loop approch based on the child items to_representation method.

Also if we're following clean architecture pattern is it like we write framework agnostic code because in the book Clean Architecture: A Craftsman’s Guide to Software Structure and Design by Robert Matin mentioned the below statement.

Independent of frameworks. The architecture does not depend on the existence of
some library of feature-laden software. This allows you to use such frameworks as
tools, rather than forcing you to cram your system into their limited constraints.

Why I am asking this is in my earlier projects I used to follow best practices of Django and DRF. so for example for any API if I wanted to perfrom authentication or check the permission I used to writed the custom authentication or permission classess provided by DRF - DRF's Custom Authentication DRF's Custom Permission

so now my lead architect saying not to use these features provided by DRF which is actually battle tested. So he's telling us to write decorators in the service layers for the methods which we want to add permissions or do authorization.

I belive as I said my lead architect just want to write the code framework agnostic approch that such that if we migrate to new framework or if any code requried in other frameworks then it should be easily doable.

While writing code as per clean architecture pattern should we strictly adhere to the pattern or we can make use of these tooling/functionalites provided by the framework?

Can you please provide your thoughts on this?

BasicWolf commented 1 year ago

Hei @aashayamballi,

In short - I'd ask, or rather demand the architect to write one of those decorators himself as an example. Explain his vision of "Clean Architecture" for the project - not in words, but with code examples. Because the demand to "do Clean Architecture" without people understanding what it is and how to do it in a Django-base application - achieves chaos.

Clean Architecture is about separating your domain logic from the technicalities. It's about applying the dependency inversion principle between those layers. "Depend upon abstractions, not concrete implementation". I prefer the vision of Clean Architecture in form of "Hexagonal Architecture" or "Ports and Adapters". In my opinion the latter ones make it more intuitive and comprehensible. I believe you read my articles on the topic, which were mainly about utilizing the very opinionated Django framework in a Hexagonal Architecture application. Here is a great article which primarily focuses on understanding the hexagonal architecture. Please read it, before continuing further down


So, imagine a Python object which represents your application. That object has some methods (API), for example:

application.add_question(...)
application.get_questions_with_choices()
...

As a user of the application object, I don't care how those methods are implemented. I just call them. I can write a command-line wrapper, which calls those, or an HTTP request handler wrapper, or some RabbitMQ listener - in the end, all those would be calling application.do_something(...). And as a provider of application methods - I don't care who calls me. Be it command line, HTTP, Message queue handler etc.

The same goes with the application dependencies. An application doesn't directly depend on Django's database models and managers. An application calls a method of a dependency and all it knows is the interface:

class Application:
    def add_question(self, question):
        ... # do some business logic here
        this.repository.save_question(question)
        ...

The application class doesn't really care, what's behind the repository.save_question(). That's the job of a concrete repository implementation to handle the call properly.

Now to your question: can you use the tooling/functionalities provided by the framework? Of course you can! But you cannot do in the application layer. The application can use yet another dependency and do something like:

class Application:
    def add_question(self, user, question):
        if not permissions_provider.has_permission(user, 'add_question'):
            return PermissionDenied(user.name, 'add_question')
        ...

Internally the permissions_provider can call DRF, Django or whatever - the application doesn't care. In that way, the application stays agnostic of the frameworks.

I should also mention, that the earlier you can invalidate the command, the better. For example, if authentication is based on a JWT token, a cookie, an HTTP GET argument etc. - there is no need to go down to the application to invalidate those. Make the invalidation in the HTTP adapter (view).

If there are some fields that need to be passed to the command, again, check the JSON structure in the HTTP adapter, don't let the semantically invalid data to pass to the application. E.g. if your API expects

{
    question: string
    choices: list[string]
}

And the adapter gets something like

{
    "question": "What's the point?"
}

This should not go further that the HTTP adapter. The choices are simply missing, the incoming data is invalid, there is no need to pass it down to the application.

You have some challenges ahead. The architect should be there, showing and schooling you, not telling you. The architect should lead, not boss image

aashayamballi commented 1 year ago

First of all, this is a great detailed explaination anybody has given to me on "Hexagonal Architecture" or "Ports and Adapters and I really wanna thank you for that!

This indeed is a great article. I had a basic understanding on "Hexagonal" or "Ports and Adapters" architecture, but the article that you shared definelty enhanced my understanding on this topic. Also I was following your hexagonal-architecture-django, now I'm more clear with the approch you have taken to structure the project. (for example earlier I wasn't sure that why you have created an adapter module and inside that we have http and persistence, But after going through the article I'm very clear why you have structured your project like that). So again a big THANK YOU for sharing that article and educating me on this topic.

and I defiently agree on this point that you mentioned.

You have some challenges ahead. The architect should be there, showing and schooling you, not telling you. 
The architect should lead, not boss

As I mentioned in my previous comments, I had a heated debated with my lead architect and I had bad conflict with him. So after that I felt little hesitent reaching out to him for anything.

I have 5 years of experince and through out my experince I've been a solo developer. Whenever I had any doubts or anything almost everytime I reached out to the community to get the help. Thorughout of my experince it's the community people that have helped me and helped me to grow as a professional developer. So if it's not lead that that I will learn from, It will definelty be people like YOU, that I will learn from! I'm really greatful for this and again a big, big THANK YOU from my bottom of my heart. 🤝🙏

coming back to the tech stuff,

  1. If we make use of permission/authentication class provided by DRF, can we conisder these permission/authentication class as an adapter(just like django view) and instead of writing the business logic in the permission/authentication class directly can we create a seperate usecase layer for this? just like how we do it in the django view.
  2. Can you please tell me what is the use of writing a single class per single file, and how exactly it is useful in the clean architecture pattern or in general?
BasicWolf commented 1 year ago

Thank you for the compliments! I just try to make the world a better place :)

  1. I had exactly the same question after switching professional tools - from Python + Django to Java/Kotlin + Spring Boot. It was so frustrating - the fact that Java forces a single class per file! But our application was following Hexagonal architecture and there it made much sense. I remember briefly discussing this in https://znasibov.info/posts/2021/10/30/hexarch_di_python_part_1.html (see section "Architecture is about intent"). \ The idea is not really about a single class per module (related .py files means that they are modules!), but rather about clear boundaries, lowering coupling and ensuring high cohesion. \ As an example - you've seen serializers.py in DRF. This module is responsible for so many things! Which means that it has equally many reasons to change. Imagine you have to review a Pull request for such file. You can't really tell what was touched there, without looking thoroughly into the PR. On the other hand, imagine that it would have been a Python sub-package, with every serialiser in its own module and __init__.py just organizing this all for comfortable importing. You would immediately grasp, which files have changed and more importantly - which have not. If the commit message says "Add XYZ functionality to ListSerializer", but you see list_serializer.py and hyperlink_serializer.py modified - it means that either the developer accidentally committed changes of hyperlink_serializer.py, or there might be something wrong on architectural level - since those two non-related modules seem to be coupled. (Unless the developer was just lazy and didn't make granular commits!) \ The bottom line is that there is nothing wrong in putting multiple classes or functions into a single module. But one has to clearly understand the reasons and consider the alternatives.

--

  1. It really depends on needs, on how you want to structure the application and on the nature of authentication/authorisation. Ask yourself: are authentication and authorisation the parts of your application's business logic? Or perhaps it's enough and possible to handle auth in API/driver/primary adapters? \ Personally I would leave auth stuff in driver adapters, i.e. in views. Putting it all into the application layer would take a lot of effort. Like you said - battle tested - why on Earth should you push a square peg in a round hole? You are mature enough to anticipate bugs in a custom "framework-agnostic" auth implementation. So, not only it would consume time, which can be spent on something more valuable, it can end up with security issues. \ I would probably try to make the authentication and authorisation as lean as possible. My personal preference are methods which don't involve database at all - like JWT tokens. A signed token would carry all information required for authentication and authorisation. It can come from any origin - be it an HTTP request or a message from a queue. \ If that is not feasible, you can still safely rely on Django and DRF auth mechanisms and apply them in views. Even if they interact with the database - they do it independently from the business logic, and any other SPI/driven/secondary adapters.
aashayamballi commented 1 year ago

Now my question is why in python world people usually don't follow this single class per single file? as you said in Java world it makes more sense because in Java as you mentioned it enforces a single class per file.

Now my lead architect is forcing me to create this single class per single file.

let's say I have an api realated to questions in views.py for example class AllQuestions(APIView):, class QuestionsBySlug(APIView):, and class UserQuestions(APIView):,

so it makes sense to create questions_api.py and move all these questions related api views to to this module. So it makes no sense in the create seperate files for seperate API adapters for example all_questions.py, question_by_slug.py and user_questions.py

so this is the pattern my lead architect is telling me to follow which I don't think is the right apporch as well. Since he's from .NET/C# background there also it is done in that way only.

Also, I have seen some big opensource projects or for example the Django/DRF framework or any python library we consider, I have not seen them following this pattern anywhere.

Also I was following the approch of constructing the dependency in dependency_container.py file and loading them via AppConfig's ready method. so now my lead architect is saying "I’m not recommending Django specific appconfig ready as its not framework agnostic, it limits the implementation’s usability to Django based solutions." and he's telling me to use the Injector package to consturct the dependency.

My lead architect is like Django is not developed using SOLID principles and he's putting a lot of blame on the Django framework as well. which I don't think is right and it has become a very hectic situation for me.

I have talked to other django community people on this and most of them said is just to switch the company. can you please share your thoughts and suggestion on how can/should I deal with this?

aashayamballi commented 1 year ago

Also let's say even if we follow clean/hexagonal architecture pattern, and main benifit of clean architecture pattern is that we can migration to new framework would be easier since the core application layer will remain same. But the codebase of API and database adapter will be huge too, right?

So let's say we have django app that is created using clean/hexagonal architecture pattern now let's say we want to migrate to FastAPI. The API adapter (FastAPI) and database adapter (SQLlchemy/Tortoise ORM) are going to have quite a bit of code too, which we still have to rewrite from scratch. For example in django we might have custom middlewares, custom authentication/permission classes, views, consumers (django channels), django orm, and signals etc still needs be changed.

My lead architect is telling everybody that with this architecture pattern we can reuse the code in other applications as well. But my point is if your application requirement is 100% similar to the application what is already built then we can reuse the code. but if the requirement maybe 50% similar then also i don't think we can complete reuse the code, and we still might have to right the code as per the requirement

As per many articles and the article that you had shared clearly mentions that clean/hexagonal architecture is not for simple crud apps. But in our org mostly the kind of projects we build are CRUD only and sometimes it is not even crud it's just reading the data from the database.

So we never know that when the complexity of the application is going to grow, but how complex application should be in order to get benifits from clean/hexagonal architecture?

One of the lead architect here at my org was also telling that if we follow this pattern, we can migrate to newer language such as C# by extracting the dlls of these django/python files. In my opinion it could be technically possible (I'm not sure though) but it is an ugly hack and should be never used for production apps.

Can you please share your thoughts on this?

BasicWolf commented 1 year ago

Hi @aashayamballi,

I believe that the answer to "why in python world people usually don't follow this single class per single file? (and why e.g. Java does that)" comes from the historical context. Back in 1995 it made sense to make JVM's life easier. When there is a single top-level public class in compiled ".class" file, JVM can easily and unambiguously located it. Python was quite different. After all it was a scripting language in the first place. We expect that a script can fit into a single file, no matter how complicated the script is. With that thinking, putting related classes into a single makes sense. And there is nothing, absolutely nothing wrong with that!

But then comes another level of consideration - when you think about people who would work with your code, today, tomorrow, in a month and perhaps in few years. Imagine that I want to understand how and why UserQuestions evolved over time. Wouldn't it be easier, if all the changes were confined to a single file user_question.py? Imagine yourself tracking the changes of some Django class, that has lived in a single file with dozen other classes.

All that being said - import this -- "Practicality beats purity" and "Flat is better than nested.". If your classes are small, tightly related and would not change much - sure, putting them in a single file makes more sense. We have powerful tools to refactor code - so if at some point you'll want to put them in separate files, that should not be a problem.


About the IoC containers - in my "hexagonal architecture" demo project I tried to keep the things to the minimum. IoC and dependency injection doesn't need any frameworks, it can all be done by manual wiring. However those kind of frameworks (like Injector) give more flexibility in setting up the dependencies in different situations (e.g. for unit tests it could automatically wire some test doubles, instead of concrete implementation). I never used Injector and have no idea, how it fits with Django, that would be an interesting topic to discover :)

I think it's easy for people to tell "switch a job", perhaps switching teams, where that architect doesn't have a say would be sufficient? Let's take it this way: choosing a framework like Django or FastAPI + SQLAlchemy at the start of the product development is a heavy and an expensive choice. Once we make it, switching to something else would require a lot of effort. Exactly as you said - Clean Architecture, or not, you would still have to rewrite all the adapters to the new framework. Doing that kind of a change in a project that has a development lifetime of 3-4 months is ridiculous. So, the whole idea of doing Clean Architecture to be framework-independent is just stupid. Clean Architecture is about allowing the product to grow and evolve with less effort, compared to the codebase where every dependency is direct. As simple as that.

From what I can tell, your architects have some knowledge of good practices, but a little understanding of when to apply them. "SOLID", "framework independent", "code reuse" - sounds great, but as you said - a thin or a missing business logic layer does not advocate for Clean architecture at all. In fact, even with Hexagonal Architecture, I would completely skip any business entities if the only thing that my HTTP View does - reads data from the database and returns JSON. Something like:

def view(request, get_questions_port: GetQuestionsPort):
    questions: list= get_questions_port.get_questions()    # all the mapping to an independent list/dict structure happens inside the adapter that implements GetQuestionsPort
    return Response(data=questions)

The only way to guess the complexity of the application - is by collaborating with the business/end-user representatives, understanding their goals and defining the scope. Only then you can guesstimate the complexity of the domain and decide about the architecture. And if you clearly see plain CRUD - Hexagonal architecture is a bloated and unnecessary choice.

we can migrate to newer language such as C# by extracting the dlls of these django/python files

Oh, this made my day. Yeah, technically everything is possible, but at what cost? And if they want that in practice, em... have they ever heard of microservices? I've seen codebases made by people obsessed with design patterns - that didn't make the code any better.

If you consider looking for a new job, I would recommend you to try looking for something different that what you are doing right now. What I means is - you already know Django and Python - what if there is a company that doesn't care what you technologies you know at the moment, but rather your ability to solve problems and learn quickly. Don't lock yourself in a silo - you can push the boundaries further and higher.

I would highly recommend you to read this book (surprise!) before moving forward image Articles in the web are written by different authors and usually lack consistency. This book will fill the gaps and can help you to grasp the bigger picture.

aashayamballi commented 1 year ago

@BasicWolf,

Thank you so much for all the feedback and suggestions.

I have a better understanding on clean/hexagonal architecture patter from your article and as well from the article you have shared. but yes, as you said on the web, articles written by authors lacks consistency and definitely I will read Robert Martin's clean architecture pattern book.

I have a tough time ahead since the lead architect is making things a lot more complicated by going framework agnostic way. As you said clean architecture is about allowing the product to grow and evolve with less effort instead of thinking it as writing code framework agnostic.

And regarding switching the job and learning something new. Um I would say i have different opinion. I'm a fullstack developer, I'm working with React and Django and I have some experience in DevOps as well. To be honest everyday I'm learning something new about the tech stack that i already know (python, javascript, react, django, FastAPI, etc) and parallely I'm learning new tools/framework related to python and javascript. I believe that there a lot of things that I still have to learn and excel at the stack that I am familiar with and side by side learn the best practices and apply them.

BasicWolf commented 1 year ago

I am glad our conversation was useful! Good luck to you, and if you have any further questions - you know where to find me :)

aashayamballi commented 1 year ago

@BasicWolf ,

Without a doubt this is the best tech conversation I've had so far and got to learn new things form you!

I will be in touch and will reach out to you because I still have to learn so many things from you 🤓

really appreciate for insturcting and guiding me and one last big THANK YOU.