Wow this book is a goldmine for architecture patterns. I love how easy it is to get into a topic and quickly grasp it.
Having said that, from a practical and experience standpoint, using some of these patterns can really spiral out into an increased complexity and performance issues in Python, specially when you use already opinionated frameworks like Django which already uses the ActiveRecord pattern.
I’ve been in companies big and small using Python, both using and ignoring architectural patterns. Turns out all the big ones with strict architectural (n=3) pattern usage, although “clean”, the code is waaaay to complex and unnecessarily slow in tasks that at first glance should had been simple.
Whereas the big companies that didn’t care for these although the code was REALLY ugly in some places (huge if-else files/functions, huge Django models with all business logic implemented in them), I was most productive because although the code was ugly I could read it, understand it, and modify the 1000 lines of if-else statements.
Maybe this says something about me more than the code but I hate to admit I was more productive in the non clean code companies. And don’t get me started on the huge amount of discussions they avoided on what’s clean or not.
I think one of the biggest problems I encounter whenever I hear that a project follows strict architectural patterns essentially boils down to too many obfuscated abstractions that hide what is going on, or force you to jump through too many layers to accomplish tasks.
Many files/functions/classes need to be updated to accomplish even simple tasks because somebody made a decision that you aren't allowed to do X or Y thing without creating N other things.
But in those companies that didn't care about architectural patterns its very likely that while there was more ugly code in certain places, it resulted in code with less indirection and more contained to a single area/unit or the task at hand making it easier for people to jump in and understand. I see so many people who create function after function in file after file to abstract away functionality when I'd honestly rather have a 100 line function or method that I can easily jump around and edit/debug vs many tiny functions all in separate areas.
Not to say having some abstractions are bad but the more I work in this field the more I realize the less abstractions there are, the easier it is to reason about singular units/features in code. I've basically landed on just abstract away the really hard stuff, but stop abstracting out things that simple.
I've come to the similar conclusion - just write the damn logic inline, and only decouple the parts which would make the whole thing difficult to test. Test decoupled parts thoroughly but in isolation.
Strict architectural pattern usage requires understanding the domain, and understanding the patterns. If you have both, navigating the codebase will be intuitive. If you don't, you'll find 1000 LOC functions easier to parse.
That's the problem, if you are working in a compagny which have mostly junior (1 or two year of programming), it is better for you to not implement to complicate pattern otherwise your day will be fill of explaining what a Factory is.
I found the book's use of modeling how to pilot an alien starship to be a little misleading, because a starship is a highly engineered product that functions in large part as a control mechanism for software. It comes with a clean design model already available for you to discover and copy.
Domain modeling should not be about copying the existing model -- it should be about improving on it using all the advantages software has over the physical and social technologies the new software product is meant to replace.
People are smart, and in most projects, there are key aspects of the existing domain model that are excellent abstractions that can and should be part of the new model. It's important to understand what stakeholders are trying to achieve with their current system before attempting to replace it.
But the models used in the business and cultural world are often messy, outdated and unoptimized for code. They rely on a human to interpret the edge cases and underspecified parts. We should treat that as inspiration, not the end goal.
> I found the book's use of modeling how to pilot an alien starship to be a little misleading, because a starship is a highly engineered product that functions in large part as a control mechanism for software. It comes with a clean design model already available for you to discover and copy.
Doctor Who fans will note that TARDIS craft seem to follow a different design: they regularly reconfigure themselves to fit their pilot, don't have controls laid out in any sensible fashion, and there's at least one reference to how they're "grown, not built". Then again they were also meant to be piloted by a crew and are most likely sentient, so it's also possible that due to the adaptations, the Doctor's TARDIS is just as eccentric as he is.
It's not like Doctor Who is "hard" sci-fi tho, it's basically Peter Pan in Space.
I love this book but yes, you really need to understand when it makes sense to apply these patterns and when not to. I think of these kinds of architectural patterns like I think of project management. They both add an overhead, and both get a bad rap because if they are used indiscriminately, you will have many cases where the overhead completely dominates any value you get from applying them. However, when used judiciously they are critical to the success of the project.
For example, if I am standing up a straight-forward calendar rest api, I am not going to have a complicated architecture. However, these kinds of patterns, especially an adherence to a ports and adapters architecture, has been critical for me in building trading systems that are easy to switch between simulation and production modes seamlessly. In those cases I am really sure I will need to easily unplug simulators with real trading engines, or historical event feeds with real-time feeds, and its necessary that the business logic have not dual implementations to keep in sync.
>I’ve been in companies big and small using Python, both using and ignoring architectural patterns. Turns out all the big ones with strict architectural (n=3) pattern usage, although “clean”, the code is waaaay to complex and unnecessarily slow in tasks that at first glance should had been simple.
The problem with "strict architectural pattern usage" is that people think that a specific implementation, as listed in the reference, is "the pattern".
"The pattern" is the thought process behind what you're doing, and the plan for working with it, and the highest-level design of the API you want to offer to the rest of the code.
A state machine in Python, thanks to functions being objects, can often just be a group of functions that return each other, and an iteration of "f = f(x)". Sometimes people suggest using a Borg pattern in Python rather than a Singleton, but often what you really want is to just use the module. `sys` is making it a singleton for you already. "Dependency injection" is often just a fancy term for passing an argument (possibly another function) to a function. A Flyweight isn't a thing; it's just the technique of interning. The Command pattern described in TFA was half the point of Jack Diederich's famous rant (https://www.youtube.com/watch?v=o9pEzgHorH0); `functools.partial` is your friend.
> Maybe this says something about me more than the code but I hate to admit I was more productive in the non clean code companies.
I think you've come to draw a false dichotomy because you just haven't seen anything better. Short functions don't require complex class hierarchies to exist. They don't require classes to exist at all.
Object-oriented programming is about objects, not classes. If it were about classes, it would be called class-oriented programming.
My experience matches this. It's so liberating as well. I find it easier to internalise such code in my head compared to abstraction-soup. As you can imagine, I like golang.
Me three. I'm even happy to refactor code into a form where there's less repetition and perhaps more parametrised functions, etc.
Finding my way around a soup of ultra abstracted Matryoshka ravioli is my least favourite part of programming. Instead of simplifying things, now I need to consult 12 different objects spread over as many files before I can create a FactoryFactory.
This has been my experience in working with any kind of dogmatic structure or pattern in any language. It seems that the architecture astronauts have missed the point: making the code easier to understand for future developers without context, and provide some certainty that modifications behave as expected.
Here's an example of how things can go off the rails very quickly:
Rule 1: Functions should be short (no longer than 50 lines).
Rule 2: Public functions should be implemented with an interface (so they can be mocked).
Now as a developer who wants to follow the logic of the program, you have to constantly "go to definition" on function calls on interfaces, then "go to implementation" to find the behavior. This breaks your train of thought / flow state very quickly.
Now let's amp it up to another level of suck: replace the interface with a microservice API (gRPC). Now you have to tab between multiple completely different repos to follow the logic of the program. And when opening a new repo, which has its own architectural layers, you have to browse around just to find the implementation of the function you're looking for.
These aren't strawmen either... I've seen these patterns in place at multiple companies, and at this point I yearn for a 1000 line function with all of the behavior in 1 place.
> Turns out all the big ones with strict architectural (n=3) pattern usage, although “clean”, the code is waaaay to complex and unnecessarily slow in tasks that at first glance should had been simple.
My last job had a Python codebase just like this. Lots of patterns, implemented by people who wanted to do things "right," and it was a big slow mess. You can't get away with nearly as much in Python (pre-JIT, anyway) as you can in a natively compiled language or a JVM language. Every layer of indirection gets executed in the interpreter every single time.
What bothers me about this book and other books that are prescriptive about application architecture is that it pushes people towards baking in all the complexity right at the start, regardless of requirements, instead of adding complexity in response to real demands. You end up implementing both the complexity you need now and the complexity you don't need. You implement the complexity you'll need in two years if the product grows, and you place that complexity on the backs of the small team you have now, at the cost of functionality you need to make the product successful.
To me, that's architectural malpractice. Even worse, it affects how the programmers on your team think. They start thinking that it's always a good idea to make code more abstract. Your code gets bloated with ghosts of dreamed-of future functionality, layers that could hypothetically support future needs if those needs emerged. A culture of "more is better" can really take off with junior programmers who are eager to do good work, and they start implementing general frameworks on top of everything they do, making the codebase progressively more complex and harder to work in. And when a need they anticipated emerges in reality, the code they wrote to prepare for it usually turns out to be a liability.
Looking back on the large codebases I've worked with, they all have had areas where demands were simple and very little complexity was needed. The ones where the developers accepted their good luck and left those parts of the codebase simple were the ones that were relatively trouble-free and could evolve to meet new demands. The ones where the developers did things "right" and made every part of the codebase equally complex were overengineered messes that struggled under their own weight.
My preferred definition of architecture is the subset of design decisions that will be costly to change in the future. It follows that a goal of good design is minimizing architecture, avoiding choices that are costly to walk back. In software, the decision to ignore a problem you don't have is very rarely an expensive decision to undo. When a problem arises, it is almost always cheaper and easier to start from scratch than to adapt a solution that was created when the problem existed only in your head. The rare exceptions to this are extremely important, and from the point of view of optics, it always looks smarter and more responsible to have solved a problem incorrectly than not to have solved it at all, but we shouldn't make the mistake of identifying our worth and responsibility solely with those exceptions.
> What bothers me about this book and other books that are prescriptive about application architecture is that it pushes people towards baking in all the complexity right at the start, regardless of requirements, instead of adding complexity in response to real demands.
The trouble is if you strictly wait until it's time then basically everything requires some level of refactoring before you can implement it.
The dream is that new features is just new code, rather than refactoring and modifying existing code. Many people are already used to this idea. If you add a new "view" in a web app, you don't have to touch any other view, nor do you have to touch the URL routing logic. I just think more people are comfortable depending on frameworks for this kind of stuff rather than implementing it themselves.
The trouble is a framework can't know about your business. If you need pluggable validation layers or something you might have to implement it yourself.
The downside, of course, is we're not always great at seeing ahead of time where the application will need to be flexible and grow. So you could build this into everything, leading to unnecessarily complicated code, or nothing, leading to constant refactors which will get worse and worse as the codebase grows.
Your approach can work if developers actually spot what's happening early and actually do what's necessary when it actually is. Unfortunately in my experience people follow by example and the frog can boil for a long time before people start to realise that their time is spent mostly doing large refactors because the code just doesn't support the kind of flexibility and extensibility they need.
> The dream is that new features is just new code, rather than refactoring and modifying existing code
I don't just mean new features. I mean new cross-cutting capabilities. I mean emitting metrics from an application that has never emitted metrics. I also mean adding new dimensions to existing capabilities, like adding support for a second storage backend to an application that has only ever supported one database.
These are changes that I was always taught were important to anticipate. If you don't plan ahead, it'll be near impossible to add later, right? After a couple of decades of working on real-life codebases, seeing the work that people pour into anticipating future needs, making things pluggable, all that stuff, seeing exactly how helpful that kind of up-front speculative work turns out to be in practice when a real need arises, and comparing it to the work required to add something to a codebase that was never prepared for it, I have become a staunch advocate for skipping almost all of it.
> Unfortunately in my experience people follow by example and the frog can boil for a long time before people start to realise that their time is spent mostly doing large refactors because the code just doesn't support the kind of flexibility and extensibility they need
If the engineers are doing large refactors, what in the world could they be doing besides adding the "kind of flexibility and extensibility they need?"
One thing to keep in mind when you compare two options is that unless the options involve different hiring strategies, the people executing them will be the same. If you have developers doing repeated large refactors without being able to make the codebase serve the current needs staring them in the face, what do you think will happen if you ask them to prepare a codebase for uncertain future needs? It's a strictly harder problem, so they will do a worse job, or at least no better.
Patterns and Abstractions have a HUGE cost in python. They can be zero cost in C++ due to compiler, or very low cost due to JVM JIT, but in Python the cost is very significant, especially once you start adding I/O ops or network calls
Having said that, from a practical and experience standpoint, using some of these patterns can really spiral out into an increased complexity and performance issues in Python, specially when you use already opinionated frameworks like Django which already uses the ActiveRecord pattern.
I’ve been in companies big and small using Python, both using and ignoring architectural patterns. Turns out all the big ones with strict architectural (n=3) pattern usage, although “clean”, the code is waaaay to complex and unnecessarily slow in tasks that at first glance should had been simple.
Whereas the big companies that didn’t care for these although the code was REALLY ugly in some places (huge if-else files/functions, huge Django models with all business logic implemented in them), I was most productive because although the code was ugly I could read it, understand it, and modify the 1000 lines of if-else statements.
Maybe this says something about me more than the code but I hate to admit I was more productive in the non clean code companies. And don’t get me started on the huge amount of discussions they avoided on what’s clean or not.