The Trap of Over-Decomposition Software developers often treat Clean Code as a rigid checklist: small classes, short methods, and abstractions everywhere. However, Arjan Codes warns that applying these rules blindly leads to **over-decomposition**. This creates a design that looks tidy on the surface but hides a "huge monster in the closet." The problem arises when you optimize for smallness rather than **cohesion**. In the initial example, a sales report script used a Protocol and a Container for dependency injection. While these are technically advanced patterns, they served no functional purpose, merely masking a messy 200-line "run" method that handled loading, filtering, and exporting simultaneously. Real clean code isn't about the number of lines; it is about making the reasons for change visible and grouping logic that belongs together. Refactoring the Fake Abstraction To begin the cleanup, we must strip away abstractions that solve imaginary problems. If a container exists just to instantiate one class that is never swapped out, it is dead weight. By deleting the `ReportService` protocol and the `Container` class, we bring the logic back to the `main` function where it can be directly controlled. Next, we introduce a Data Class for configuration. Instead of passing five or six separate arguments through every function, we group them into a cohesive `ReportConfig` object. This allows for sensible defaults—like UTF-8 encoding or comma delimiters—while making the settings explicit and easy to modify in one location. ```python @dataclass(frozen=True) class ReportConfig: country: str = "Netherlands" min_revenue: float = 10.0 allow_negative: bool = False delimiter: str = "," encoding: str = "utf-8" ``` Making the Pipeline Explicit One of the most significant design improvements involves breaking the monolithic run method into a clear, linear pipeline: **Load → Summarize → Export**. By separating these concerns into pure functions, we gain massive flexibility and efficiency. For instance, by moving the loading logic out of the core processing function, we can load the data once and run multiple summaries against it without re-reading the file from the disk. We also introduce a `TypeAlias` for our data structures to improve readability without the overhead of heavy class hierarchies. ```python from typing import TypeAlias Data: TypeAlias = list[dict[str, str]] def load_data(source: Path, config: ReportConfig) -> Data: # File loading logic here return data ``` Modeling Results with Cohesion Instead of letting the summary data float around as raw numbers, we move behavior into the `Summary` object itself. If you need to convert a summary to text or JSON, that logic belongs to the summary, not the exporter. This shift moves the "how" of the report into the object that owns the "what." We also centralize business logic. By creating an internal `is_valid` helper within the summarization function, we isolate the filtering rules (e.g., checking revenue thresholds or refund status) from the aggregation logic. This makes the primary loop significantly cleaner and focuses the `summarize` function on its actual job: calculating totals. ```python def summarize(data: Data, config: ReportConfig) -> Summary: def is_valid(row: dict) -> bool: return float(row["revenue"]) >= config.min_revenue valid_rows = [row for row in data if is_valid(row)] revenue_sum = sum(float(row["revenue"]) for row in valid_rows) return Summary(count=len(valid_rows), revenue_sum=revenue_sum) ``` Practical Examples and Syntax Notes This approach shines when extending the system. To add a JSON export, we simply define an `export_json` function. Because our pipeline is explicit, we don't have to touch the loading or summarizing code. We just plug the new exporter into our main execution flow. When using Python for these patterns, utilize `Path` objects from `pathlib` rather than strings to handle file system operations more robustly. Additionally, the `asdict` utility from the `dataclasses` module is perfect for quickly converting your cohesive objects into formats suitable for `json.dumps` while maintaining control over the final output structure. Tips and Gotchas Avoid the temptation to abstract early. Wait until you have at least two or three different implementations before reaching for a Protocol. The most common mistake is "hiding" behavior inside services, which makes debugging difficult. High cohesion means things that change together stay together; it doesn't mean every function has to be three lines long. Focus on visibility and meaningful boundaries to keep your code truly maintainable.
Clean Code
Books
- Mar 27, 2026
- Feb 14, 2023
- Jan 6, 2023
- Dec 6, 2022
- Sep 6, 2022
The Evolution of a Developer Perspective When we look at the trajectory of a successful software project or a career, we often obsess over the end state. We see the 100,000 subscribers, the five million views, or the robust production application. But the reality of growth is far more chaotic and experimental. My own journey with ArjanCodes didn't begin as a Python channel. It started as a reflection on the mistakes I made while running a startup. I wanted to talk about picking the wrong libraries, choosing the wrong platforms, and making poor architectural decisions. The pivot to technical tutorials happened because the audience responded to the "how" and the "why" of code. It wasn't about being a Python guru; it was about the discipline of software design. I realized that while many people know the syntax of a language, fewer understand how to get from a problem description to something that actually makes sense in code. That process—the translation of logic into maintainable architecture—is the most interesting part of programming. It transcends specific frameworks or the flavor-of-the-month library. It’s about building systems that don't crumble under their own weight the moment you need to change a requirement. The Iteration Mindset in Code and Life One of the most frequent questions I get is about the production quality of my work. People want to know the "secret." There isn't one. The only principle that matters is iteration. In the startup world, we talk about it constantly, but we rarely apply it to our personal development or our coding practices with enough rigor. If you look at my videos from a year ago, I look like a green alien with terrible lighting. I didn't wait until I had a perfect studio to start; I started, noticed a problem, and refused to tolerate it. This is exactly how we should approach software development. You don't have to write perfect code on the first pass. In fact, if you try, you'll likely over-engineer a solution to a problem you don't fully understand yet. Instead, take small steps. Improve the lighting. Tweaking the microphone. Refactor that one function. This persistent, incremental improvement has incredible results over the long term. As Ray Dalio mentions in his book Principles, you must be perceptive enough to notice problems and adamant enough to fix them. Whether it’s a bug in your deployment pipeline or a bad shadow in a video frame, the process of fixing it is what builds expertise. The Paradigm Shift: Functional vs. Object-Oriented There is a long-standing tension in the industry between Object-Oriented Programming (OOP) and Functional Programming. For a long time, we were told that if you’re building "serious" business applications, you must use OOP. This is a rigid way of thinking. Python is unique because it supports both paradigms strongly, and I find myself moving more toward functional approaches every day. Functions often lead to shorter, more readable code because they require less boilerplate. However, classes still have a vital role in representing structured data. If you use Pydantic or data classes, you get validation and type safety that are harder to achieve with just dictionaries or tuples. My rule of thumb is simple: use classes for data and state, but use functions for behavior. If a method in a class starts getting too long, I split it out. I don't care about purity; I care about readability. You aren't marrying a paradigm. You are using tools to solve a problem. If the code is easy for another developer to understand six months from now, you’ve won. If you followed every design pattern but made the code unreadable, you’ve failed. Navigating the Framework Marriage Choosing a framework like Django or React is a major life event for your code. As Robert C. Martin points out, you are essentially marrying the framework. You have to follow its rules, its directory structures, and its philosophy. If you try to fight the stream, you’ll just end up with a mess. For example, if you're in Django, you should do things the "Django way." But that doesn't mean you should let the framework bleed into everything. You still need a layer of your own business logic that is independent. This is why I focus so much on Software Architecture. Whether you're using Node.js or Python, the principles of dependency injection and modularity remain the same. The goal is to make the framework a detail, not the entire story. This becomes critical when you look at deployment. Tools like Docker and Kubernetes have standardized how we think about infrastructure, but even there, simpler is often better. I find myself reaching for AWS Lambda or Google Cloud Run more often because they remove the burden of infrastructure management entirely. Education, Degrees, and the Job Market Is a Master's degree in Computer Science worth it? Years ago, I would have said yes without hesitation. Today, the answer is more nuanced. Universities are in a strange position. Computer science moves so fast that a four-year curriculum is often out of date by the time a student reaches their senior year. Academic environments are naturally geared toward theory, which is great for learning Mathematics, but less effective for learning how to deploy a microservice at scale. Furthermore, the grading system in traditional education can actually hinder learning. Research shows that once you grade someone, they stop being interested in the material and start being interested in the grade. My advice to anyone starting out is to treat your first job as your true education. Bootcamps are excellent for pivoting careers quickly, but if you already have the basics, nothing beats working on a real-world project. Developing your analytical skills—the ability to cut a problem into small pieces—is the only "advanced" skill that actually matters. Syntax is easy; logic is hard. The Motivation of the Lifelong Learner Staying motivated in this field isn't about chasing the highest salary or the trendiest language. It’s about intrinsic curiosity. I've been coding since I was 10 years old, starting on a Commodore 16. Even if I weren't doing this for a career, I’d still be doing it for fun. The beauty of teaching on YouTube is that it forces me to learn in the open. Every time I prepare a video on Infrastructure as Code or a specific library like Pulumi, I have to dive deep into the documentation. I have to defend my choices to a community of 100,000 people. This feedback loop keeps me sharp. It’s a reminder that we are all students. If you view yourself as a guru, you stop growing. If you view yourself as a learner, every critical comment is an opportunity to rethink your design and every new framework is a playground. Focus on the basics, iterate relentlessly, and keep your logic simple. That is the only path to becoming a pro.
Jul 29, 2022