206

I am junior developer among seniors and am struggling a lot with understanding their thinking, reasoning.

I am reading Domain-Driven Design (DDD) and can't understand why we need to create so many classes. If we follow that method of designing software we end up with 20-30 classes which can be replaced with at most two files and 3-4 functions. Yes, this could be messy, but it's a lot more maintainable and readable.

Anytime I want to see what some kind of EntityTransformationServiceImpl does, I need to follow lots of classes, interfaces, their function calls, constructors, their creation and so on.

Simple math:

  • 60 lines of dummy code vs 10 classes X 10 (let's say we have totally different such logics) = 600 lines of messy code vs. 100 classes + some more to wrap and manage them; do not forget to add dependency injection.
  • Reading 600 lines of messy code = one day
  • 100 classes = one week, still forget which one does what, when

Everyone is saying it's easy to maintain, but for what? Every time you add new functionality, you add five more classes with factories, entities, services, and values. I feel like this kind of code moves a lot slower than messy code.

Let's say, if you write 50K LOC messy code in one month, the DDD thing requires lots of reviews and changes (I do not mind tests in both cases). One simple addition can take week if not more.

In one year, you write lots of messy code and even can rewrite it multiple times, but with DDD style, you still do not have enough features to compete with messy code.

Please explain. Why do we need this DDD style and lots of patterns?

UPD 1: I received so many great answers, can you guys please add comment somewhere or edit your answer with the link for reading list (not sure from which to start, DDD, Design Patterns, UML, Code Complete, Refactoring, Pragmatic ,... so many good books), of course with sequence, so that I can also start understanding and become senior as some of you do.

  • 80
    I think there is a good question here but it's hiding behind hyperbole and frustration. @user1318496, you might benefit from rephrasing your question a bit. – MetaFight Apr 10 '18 at 16:42
  • 48
    At the risk of stepping on toes, because your language sucks. Don't take that for more than it is: a sucky language is often the correct technical choice for a variety of reasons, but that doesn't make it non-sucky. As for your actual question, correctness is more important than readability. Or to put that slightly differently: readability matters insofar as in enables reasoning about correctness. So which is easier to test, your 100 classes or your monolithic god class? I'm betting 100 simple classes. – Jared Smith Apr 10 '18 at 19:27
  • 87
    "Yes this could be messy, but its a lot more maintainable and readable.". If its messy, how can it be readable and maintainable? – Polygnome Apr 10 '18 at 19:56
  • 37
    @Polygnome: I was about to type the exact same comment. Another choice comment characteristic of junior developers is "I feel like this kind of code moves a lot slower then messy code." It does you no good to run as fast as you can if you spend half your time running into walls! Slow is smooth, smooth is fast. – Eric Lippert Apr 10 '18 at 20:07
  • 40
    You don't, unless you're doing Java. When all you have is Java, everything looks like a class. – hobbs Apr 11 '18 at 6:27

10 Answers 10

332

This is an optimization problem

A good engineer understands that an optimization problem is meaningless without a target. You can't just optimize, you have to optimize for something. For example, your compiler options include optimizing for speed and optimizing for code size; these are sometimes opposite goals.

I like to tell my wife that my desk is optimized for adds. It's just a pile, and it's very easy to add stuff. My wife would prefer it if I optimized for retrieval, i.e. organized my stuff a bit so I can find things. This makes it harder, of course, to add.

Software is the same way. You can certainly optimize for product creation-- generate a ton of monolithic code as quickly as possible, without worrying about organizing it. As you have already noticed, this can be very, very fast. The alternative is to optimize for maintenance-- make creation a touch more difficult, but make modifications easier or less risky. That is the purpose of structured code.

I would suggest that a successful software product will be only created once but modified many, many times. Experienced engineers have seen unstructured code bases take on a life of their own and become products, growing in size and complexity, until even small changes are very difficult to make without introducing huge risk. If the code were structured, risk can be contained. That is why we go to all this trouble.

Complexity comes from relations, not elements

I notice in your analysis you are looking at quantities-- amount of code, number of classes, etc. While these are sort of interesting, the real impact comes from relations between elements, which explodes combinatorially. For example, if you have 10 functions and no idea which depends on which, you have 90 possible relations (dependencies) you have to worry about-- each of the ten functions might depend on any of the nine other functions, and 9 x 10 = 90. You might have no idea which functions modify which variables or how data gets passed around, so coders have a ton of things to worry about when solving any particular problem. In contrast, if you have 30 classes but they are arranged cleverly, they can have as few as 29 relations, e.g. if they are layered or arranged in a stack.

How does this affect your team's throughput? Well, there are fewer dependencies, the problem is much more tractable; coders don't have to juggle a zillion things in their head whenever they make a change. So minimizing dependencies can be a huge boost to your ability to reason about a problem competently. That is why we divide things into classes or modules, and scope variables as tightly as possible, and use SOLID principles.

  • 103
    I would note, however, that an overabundance of classes and indirection does NOT help either understanding NOR maintenance. And nor does convoluted control flows (aka, callback hell). – Matthieu M. Apr 11 '18 at 8:33
  • 9
    @JohnWu this explanation is way the best and easier to understand one that I ever read. – Adriano Repetti Apr 11 '18 at 9:04
  • 13
    Well written, but is it maybe missing why "created once but modified many, many times" is important? (If all the code is in EntityTransformationServiceImpl, you are forced to learn how the whole thing works before you can fix it - but if e.g. something is wrong with the format and a Formatter class handles that, you only need to learn how that part works. Plus reading your own code after ~3 months is like reading a stranger's code.) I feel like most of us subconsciously thought of this, but that might be because of experience. Not 100% sure about this. – R. Schmitz Apr 11 '18 at 14:48
  • 17
    I'd go for the full 10×10=100 for the combinatorial: those functions could well be recursive, and in particularly poor code, not necesarrily obviously so. – KRyan Apr 11 '18 at 15:06
  • 32
    "The alternative is to optimize for maintenance-- make creation a touch more difficult, but make modifications easier or less risky. That is the purpose of structured code." I take issue with the implicit notion that more classes = more maintainability. In particular, scattering logic across many classes often makes it difficult to find the overarching logical concepts in the code (especially without a very intentional eye on ensuring the concepts are very visible), which degrades maintainability enormously. – jpmc26 Apr 11 '18 at 17:51
66

Well, first of all, readability and maintability are often in the eye of the beholder.

What is readable to you may not be to your neighbour.

Maintainability often boils down to discoverability (how easily is a behaviour or concept discovered in the codebase) and discoverability is another subjective thing.

DDD

One of the ways DDD helps teams of developers is by suggesting a specific (yet still subjective) way of organising your code concepts and behaviours. This convention makes it easier to discover things, and therefore easier to maintain the application.

  • Domain concepts are encoded as Entities and Aggregates
  • Domain behaviour resides in Entities or Domain Services
  • Consistency is ensured by the Aggregate Roots
  • Persistence concerns are handled by Repositories

This arrangement is not objectively easier to maintain. It is, however, measurably easier to maintain when everyone understands they're operating in a DDD context.

Classes

Classes help with maintainability, readability, discoverability, etc... because they are a well known convention.

In Object Oriented settings, Classes are typically used to group closely related behaviour and to encapsulate state that needs to be controlled carefully.

I know that sounds very abstract, but you can think about it this way:

With Classes, you don't necessarily need to know how the code in them works. You just need to know what the class is responsible for.

Classes allow you to reason about your application in terms of interactions between well defined components.

This reduces the cognitive burden when reasoning about how your application works. Instead of having to remember what 600 lines of code accomplishes, you can think about how 30 components interact.

And, considering those 30 components probably span 3 layers of your application, you probably only every have to be reasoning about roughly 10 components at a time.

That seems pretty manageable.

Summary

Essentially, what you're seeing the senior devs do is this:

They are breaking down the application into easy to reason about classes.

They are then organising these into easy to reason about layers.

They are doing this because they know that, as the application grows, it becomes harder and harder to reason about it as a whole. Breaking it down into Layers and Classes means that they never have to reason about the entire application. They only ever need to reason about a small subset of it.

  • 5
    Besides smaller classes are easier to replace/refactor. – Zalomon Apr 10 '18 at 16:43
  • 11
    Overengineering does not give more maintainable or understandable code - quite the opposite, despite what you claim. Who needs an AddressServiceRepositoryProxyInjectorResolver when you can solve the problem more elegantly? Design patterns for the sake of design patterns just leads to unecessary complexity and verbosity. – vikingsteve Apr 11 '18 at 12:35
  • 27
    Yeah, overengineering is bad. So is killing puppies. Nobody here is advocating for either. logicallyfallacious.com/tools/lp/Bo/LogicalFallacies/169/… – MetaFight Apr 11 '18 at 12:40
  • 5
    Also, "elegance" in the software engineering context is often a Weasel Word. en.wikipedia.org/wiki/Weasel_word – MetaFight Apr 11 '18 at 12:47
  • 11
    The same could be said of any approach to organising code. Everything hinges on the maintenance team understanding why the code is organised as it is. That's the whole point of using well established and understood conventions. – MetaFight Apr 11 '18 at 14:03
29

Please explain me, why do we need this DDD style, lots of Patterns?

First, a note: the important part of DDD is not the patterns, but the alignment of the development effort with the business. Greg Young remarked that the chapters in the blue book are in the wrong order.

But to your specific question: there tend to be a lot more classes than you would expect, (a) because an effort is being made to distinguish the domain behavior from the plumbing and (b) because extra effort is being made to ensure that concepts in the domain model are expressed explicitly.

Bluntly, if you have two different concepts in the domain, then they should be distinct in the model even if they happen to share the same in memory representation.

In effect, you are building a domain specific language that describes your model in the language of the business, such that a domain expert should be able to look at it and spot errors.

Additionally, you see a bit more attention paid to separation of concerns; and the notion of insulating consumers of some capability from the implementation details. See D. L. Parnas. The clear boundaries empower you to change or extend the implementation without the effects rippling throughout you entire solution.

The motivation here: for an application that is part of the core competency of a business (meaning a place where it derives a competitive advantage), you will want to be able to easily and cheaply replace a domain behavior with a better variation. In effect, you have parts of the program that you want to evolve rapidly (how state evolves over time), and other parts that you want to change slowly (how state is stored); the extra layers of abstraction help avoid inadvertently coupling one to the other.

In fairness: some of it is also Object Oriented Brain Damage. The patterns originally described by Evans are based on Java projects that he participated in 15+ years ago; state and behavior are tightly coupled in that style, which leads to complications you might prefer to avoid; see Perception and Action by Stuart Halloway, or Inlining Code by John Carmack.

No matter what language you work in, programming in a functional style provides benefits. You should do it whenever it is convenient, and you should think hard about the decision when it isn't convenient. Carmack, 2012

  • 2
    I was going to add my own answer until I read this. I would emphasize the first paragraph, that the aim is to model business concepts in software to align the software with the business requirements. In fairness, the analyst driving the project should also express how much of a driver time to delivery is, and code/test quality or completeness should be adjusted accordingly. – Sentinel Apr 11 '18 at 5:41
  • 1
    John Carmack is a great example IMO, since he's well known for writing code that's both clean and easy to understand, while also being well performing. This is especially visible as you track his code with advancing technologies - with better CPUs and memory, you can see a lot of things pushed from "this needs to be really terse" to "this needs to be really clear/isolated/...". One of the most pragmatic programmers I know of :) – Luaan Apr 12 '18 at 9:26
  • I think this is the best answer here. While I am by no means sold on DDD, this answer certainly at least explains what it really is and an actual motivation for it, rather than appealing to common platitudes that mean nothing. – jpmc26 Apr 14 '18 at 16:16
29

There are many good points in the other answers, but I think they miss or don't emphasize an important conceptual mistake you make:

You are comparing the effort to understand the complete program.

This is not a realistic task with most programs. Even simple programs consist of so much code that it is simply impossible to manage all of it in the head at any given time. Your only chance is to find the part of a program that is relevant to the task at hand (fixing a bug, implementing a new feature) and work with that.

If your program consists of huge functions/methods/classes this is almost impossible. You'll have to understand hundreds of lines of code just to decide if this chunk of code is relevant to your problem. With the estimates you gave it becomes easy to spend a week just to find the piece of code you need to work on.

Compare that to a code base with small function/methods/classes named and organized into packages/namespaces that makes it obvious where to find/put a given piece of logic. When done right in many cases you can jump right to the correct place to solve your problem, or at least to a place from where firing up your debugger will bring you to the right spot in a couple of hops.

I have worked in both kinds of systems. The difference can easily be two orders of magnitude in performance for comparable tasks and comparable system size.

The effect this has on other activities:

  • testing becomes much easier with smaller units
  • less merge conflicts because chances for two developers to work on the same piece of code are smaller.
  • less duplication because it is easier to reuse pieces (and to find the pieces in the first place).
19

Because testing code is harder than writing code

A lot of answers have given good reasoning from a developer's perspective - that maintenance can be reduced, at the cost of making the code more strenuous to write in the first place.

However, there is another aspect to consider - testing can only be fine-grained as your original code.

If you write everything in a monolith, the only effective test you can write is "given these inputs, is the output correct?". This means any bugs found, are scoped to "somewhere in that giant pile of code".

Of course, you can then have a developer sit with a debugger and find exactly where the problem started and work on a fix. This takes a lot of resources, and is a bad use of a developer's time. Imagine that ever minor bug you have, results in a developer needing to debug the entire program again.

The solution: Many smaller tests that pinpoint one specific, potential failure each.

These small tests (for example Unit Tests), have the advantage of checking a specific area of the codebase and helping to find errors within a limited scope. This not only speeds up debugging when a test fails, but also means that if all your small tests fail - you can more easily find the failure in your larger tests (i.e. if it's not in a specific tested function, it must be in the interaction between them).

As should be clear, to make smaller tests means your codebase needs to be split into smaller testable chunks. The way to do that, on a large commercial codebase, often result in code that looks like what you're working with.

Just as a side-note: This isn't to say people won't take things "too far". But there is a legitimate reason for separating codebases into smaller/less connected parts - if done sensibly.

  • 5
    While your answer addresses only testing and testability, that is the single most important issue which some of the other answers completely ignored so +1. – skomisa Apr 11 '18 at 19:19
17

Please explain me, why do we need this DDD style, lots of Patterns?

Many (most...) of us really don't need them. Theoreticians and very advanced, experienced programmers write books about theories and methodologies as the result of lots of research and their deep experience - that doesn't mean that everything they write is applicable to every programmer in their everyday practice.

As a junior developer, it's good to read books such as the one you mentioned to broaden your perspectives, and make you aware of certain issues. It will also save you from getting embarrassed and flummoxed when your senior colleagues use terminology you are unfamiliar with. If you find something very difficult and doesn't seem to make sense or seem useful, don't kill yourself over it - just file it away in your head that there is such a concept or approach.

In your day to day development, unless you are an academic, your job is to find workable, maintainable solutions. If the ideas you find in a book don't help you achieve that goal, then don't worry about it right now, as long as your work is deemed satisfactory.

There may come a time when you'll discover that you can use some of what you read about but didn't quite "get" at first, or maybe not.

  • 12
    While on a pure level this is correct it sounds like DDD and other Patterns are just theory. They aren't. They are important tools to achieve readable and maintainable code. – Jens Schauder Apr 11 '18 at 5:47
  • 9
    @PeteKirkham the OPs dilemma is the overuse of design patterns. Do you really need an AccountServiceFacadeInjectResolver (real example I just found in a system at work) - answer is most probably no. – vikingsteve Apr 11 '18 at 12:55
  • 4
    @vikingsteve OP is not a programming guru commenting on the general overuse of design patterns. OP is an apprentice and thinks they are overused at his shop. I know that beginner-me would have thought the same about the code I write nowadays, but beginner-me had a lot of personal projects fail because they ended up too convoluted. – R. Schmitz Apr 11 '18 at 15:11
  • 3
    @R.Schmitz That said, beginner me had plenty of personal projects fail because they tried too hard to make things well-designed, organized, structured, OOP... All of these are tools. They need to be understood and used properly, and you can hardly blame the hammer for being poor at screwing. Surprisingly, it seems to take a lot of insight and experience to understand this simple thing :) – Luaan Apr 12 '18 at 9:22
  • 3
    @Luaan If you already cared about well-designed, organized, structured, OOP, I'd hardly call that beginner-you, but yeah overuse is bad. However, the question is more about how OP doesn't really understand which problems those things solve. If you don't know the reason, it seems like there is no reason - but actually, you just don't know it yet. – R. Schmitz Apr 12 '18 at 10:38
8

As your question is covering a lot of ground, with a lot of assumptions, I'll single out the subject of your question:

Why do we need so many classes in design patterns

We do not. There is no generally accepted rule that says that there must be many classes in design patterns.

There are two key guides for deciding where to put code, and how to cut your tasks into different units of code:

  • Cohesion: any unit of code (be it a package, a file, a class or a method) should belong together. I.e., any specific method should have one task, and do that one well. Any class should be responsible for one larger topic (whatever that may be). We want high cohesion.
  • Coupling: any two units of code should depend as little on each other as possible - there should especially be no circular dependencies. We want low coupling.

Why should those two be important?

  • Cohesion: a method that does a lot of things (e.g., an old-fashioned CGI script that does GUI, logic, DB access etc. all in one long mess of code) becomes unwieldy. At the time of writing, it is tempting to just put your train of thought into a long method. This works, it is easy to present and such, and you can be done with it. Trouble arises later: after a few months, you may forget what you did. A line of code at the top might be a few screens away from a line on the bottom; it is easy to forget all details. Any change anywhere in the method may break any amount of behaviours of the complex thing. It will be quite cumbersone to refactor or re-use parts of the method. And so on.
  • Coupling: anytime you change a unit of code, you potentially break all other units that depend on it. In strict languages like Java, you might get hints during compile time (i.e., about parameters, declared exceptions and so on). But many changes are not triggering such (i.e. behavioural changes), and other, more dynamic, languages, have no such possibilities. The higher the coupling, the tougher it gets to change anything, and you might grind to a halt, where a complete re-write is necessary to achieve some goal.

These two aspects are the base "drivers" for any choice of "where to put what" in any programming language, and any paradigm (not only OO). Not everybody is explictly aware of them, and it takes time, often years, to get a really ingrained, automatic feeling on how these influence software.

Obviously, those two concepts do not tell you anything about what to actually do. Some people err on the side of too much, others on the side of too little. Some languages (looking at you here, Java) tend to favour many classes because of the extremely static and pedantic nature of the language itself (this is not a value statement, but it is what it is). This becomes especially noticeable when you compare it to dynamic and more expressive languages, for example Ruby.

Another aspect is that some people subscribe to the agile approach of only writing code that is necessary right now, and to refactor heavily later, when necessary. In this style of development, you would not create an interface when you only have one implementing class. You would simply implement the concrete class. If, later, you need a second class, you would refactor.

Some people simply do not work that way. They create interfaces (or, more generally, abstract base classes) for anything that ever could be used more generally; this leads to a class explosion quickly.

Again, there are arguments for and against, and it does not matter which I, or you, prefer. You will, in your life as software developer, encounter all extremes, from long spaghetti methods, through enlightened, just-big-enough class designs, up to incredibly blown-up class schemes that are much overengineered. As you get more experienced, you will grow more into "architectural" roles, and can start to influence this in the direction you wish to. You will find out a golden middle for yourself, and you will still find that many people will disagree with you, whatever you do.

Hence, keeping an open mind is the most important bit, here, and that would be my primary advice to you, seeing that you seem to be much in pain about the issue, judging by the rest of your question...

7

Experienced coders have learned:

  • Because those smallish programs and clasess start to grow, especially successful ones. That simple patterns that worked at a simple level don't scale.
  • Because it may seem burdensome to have to add/change several artifacts for every add/change but you know what to add and it is easy to do so. 3 minutes of typing beats 3 hours of clever coding.
  • Lots of small classes is not "messy" just because you don't understand why the overhead is actually a good idea
  • Without the domain knowledge added, the 'obvious to me' code is often mysterious to my team mates... plus the future me.
  • Tribal knowledge can make projects incredibly hard to add team members to easily and hard for them to be productive quickly.
  • Naming is one of the two hard problems in computing is still true and lots of classes and methods is often an intense exercise in naming which many find to be a great benefit.
  • 5
    But is the overhead necessary? In many cases that I see, design patterns are overused. The result of overcomplexity are increased difficulty in understanding, maintaining, testing and debugging code. When you have 12 classes and 4 maven modules around one simple service class (a real example I just saw), it quickly becomes less manageable than you might think. – vikingsteve Apr 11 '18 at 12:59
  • 4
    @vikingsteve You keep referring to a single example of a flawed implementation hoping that it will prove that structured code, in general, is a bad idea. That just doesn't track. – MetaFight Apr 11 '18 at 14:56
  • 2
    @MetaFight OP isn't talking about structure code. He's talking about, what he sees as, too many abstractions. If you have too many abstractions, I argue, you quickly get very very unstructured code and a lot of nearly identical pieces, simply because people can't cope with the amount of stuff they have to deal with. – Clearer Apr 12 '18 at 8:58
  • 4
    @Clearer I'm pretty sure everyone here is in agreement that the hypothetical misapplication of structured code is a bad thing. The OP says they're Junior. That's why people are assuming he/she is unfamiliar with the benefits of structured code and reiterating them. – MetaFight Apr 12 '18 at 11:03
2

The answers so far, all of them good, having started with the reasonable assumption that the asker is missing something, which the asker also acknowledges. It's also possible that the asker is basically right, and it's worth discussing how this situation can come about.

Experience and practice are powerful, and if the seniors gained their experience in large, complex projects where the only way to keep things under control is with plenty of EntityTransformationServiceImpl's, then they become fast and comfortable with design patterns and close adherence to DDD. They would be much less effective using a lightweight approach, even for small programs. As the odd one out, you should adapt and it will be a great learning experience.

While adapting though, you should take it as a lesson in the balance between learning a single approach deeply, to the point you can make it work anywhere, versus staying versatile and knowing what tools are available without necessarily being an expert with any of them. There are advantages to both and both are needed in the world.

-4

Making more class and function as per use will be a great approach to resolve problems in a specific manner that help in future to resolve any issues.

Multiple classes help to identify as their basic work and any class can be called at any time.

However if you many class and functions from their getable name they are in easy to call and manage. This is called a clean code.

  • 9
    Sorry, but what are you saying? – Deduplicator Apr 13 '18 at 15:18
  • @Deduplicator let's take an example in java if we design we used jframes threads and classes for their inheritance. From only one class we can't able to use some works of java for design so we need to make more classes – Sanjeev Chauhan Apr 13 '18 at 15:22
  • 2
    This answer needs to present the idea(s) that it's purporting to present in clearer English. It seems like that the core idea in the answer is that smaller classes, each with one well-defined functionality, is a good idea, but the terrible grammar makes it almost impossible to understand. Also, by itself, this assertion may not be enough. – talonx Apr 16 '18 at 8:39

protected by gnat Apr 13 '18 at 15:37

Thank you for your interest in this question. Because it has attracted low-quality or spam answers that had to be removed, posting an answer now requires 10 reputation on this site (the association bonus does not count).

Would you like to answer one of these unanswered questions instead?

Not the answer you're looking for? Browse other questions tagged or ask your own question.