1

on design stage , after finding my main objects , I am always find my self writing "manager" or "controllers" classes in order to handle the connection between them. some of the logic is in those classes.

i know it is an antipattern Anemic Domain Model and that each class should have its own behavior.

my problem is that sometimes when we give a class a behavior we also couple it to other classes.

for example in this site anemic model refactor instead of having two (super) anemic classes with only set\get and a manager\ usecase class - "PaperBoyRoundService"

they offering some code like this :

public class Customer {

public void buyPaper(PaperBoy paperBoy) 

}

so all the logic goes into Customer instead of some use case / class manager.

but then we have very strong coupling and dependency for Customer with PaperBoy.

if i will need my Customer in other project i will need the paperboy class. and changes in paperboy will cause changes in Customer, that is the kind of things leading me to "Manager classes" on the first place.

but it is very procedural leading to other known problems

how to solve this conflict? i really dont like classes to know about each others too much to avoid "You wanted a banana but you got a gorilla holding the banana"

2 Answers 2

4

Some coupling must exist, otherwise a program would not be able to do anything (if nothing knows about anything, no calls can be made). This kind of coupling certainly exists in your "manager" classes. The thing that's bad is coupling that makes your code hard to change, and most of it is kind of accidental.

You start with certain best practices, and a certain way of doing things; this is OK to get you off the ground, but as the project evolves, accidental coupling creeps in, and in a big part because of inertia - people blindly following generic "best practices" and "established conventions" way beyond the point of their usefulness, without stopping to rethink/redesign before it's all a complicated mess. Best practices, conventions, linters, style checkers - that all has it's place, but none of it automatically gives you good code. Nobody wants to write bad code, however, bad code, in large part, gets written very systematically.

So, when it comes to bad coupling, you have this kind of coupling even within your "manager" classes, as well as in and between classes that use them - it's just not as obvious until a change causes you to modify 20 different files. They are coupled in subtle ways - like depending on a specific data format, on a specific field being there, on calling order, on things needing to match or follow the same pattern in different places, etc.

The thing that you want to do is not to eliminate coupling completely, but to minimize it and control it, and be strategic about it.

"but then we have very strong coupling and dependency for Customer with PaperBoy"

Sure, but, putting a type name in a constructor parameter list makes that type explicitly a part of the public interface of that class. It explicitly states what depends on what. Being explicit is good, and if this coupling is deliberate, and if Customer and PaperBoy are fairly small and focused classes, and not these large procedural bags of functions, than that's a way of controling coupling.

Note that on a most basic level, the Customer class doesn't depend on the internal implementation of PaperBoy, just on its public interface (the set of public methods, including their parameter and return types). Behind that, you can change how PaperBoy is implemented - you can change its internal code, its internal data structures, you can pull out parts of PaperBoy into a separate class, you can do all kinds of things - without affecting Customer.

If you need more flexibility than that, maybe you can derive a subtype of PaperBoy, or perhaps make the constructor parameter type be an interface instead of a concrete class.

"and changes in paperboy will cause changes in Customer"

The most likely cause of that is that you're treating PaperBoy as just a data structure, and your Customer is pulling data out of PaperBoy to manipulate it itself, instead of telling PaperBoy what to do (by calling a method - almost in a fire and forget style). What's happening there is that you're not defining/constraining the way Customer talks to PaperBoy - you're not thinking carefully (or at all) about the public interface of PaperBoy, you're just saying "Here's some data, hope the format doesn't change! (wink, wink)". This problem is not solved by having "manager" classes - changes in the data structures they depend on will have the same effect.

"if i will need my Customer in other project"

Don't get this the wrong way, I'm by no means trying to talk down to you or anything like that, but:

skipping steps

When people say "reusable", it really means the ability to use the same code/function/object in a different context: perhaps in a test, perhaps in a different part of the same project, or as a thing you can plug your newly developed component into. Or, if the function or class or module (or whatever) turns out to be more generally useful, perhaps in a different project, or maybe in a library.

Think about what you need for something simple like a function to be reusable. You need it to have well defined inputs and outputs. These are often data, but note that some of those inputs (and even the output) may be a functions themselves (look at the IEnumerable.Select method in C#, or Array.map in JavaScript). You need the function to not depend on obscure global variables. It's even more reusable if it has no side effects at all. It's similar with objects: well-defined inputs and outputs; they can take other objects (or functions, for that matter) as parameters, that they can then call; they need to be protective of their internals, but also need to limit what they know about the rest of the system.

When you can't easily reuse something in a different context, it's not because it's a class coupled to some other class, it's because it wasn't designed to be reusable.

Now, I'm not saying that the only way to deal with this is OOP; however, whatever paradigm you're using, these same principles apply (it's just that their expression may take a different form). Also note that I'm not advocating for (or against) purity: some aspects of different paradigms can be mixed and can complement each other nicely. So I'm not necessarily saying that your domain model must not be anemic - just that that you have to control the coupling; if it happens in functions that work on anemic data structures, then you have to control it there.

I've been thinking recently, the usual advice regarding going from an anemic domain model to a proper OO design is to put behavior into those data structures; however, I think is not necessarily immediately obvious how to go about this change, even to experienced designers, but they can find their way by relying on their experience and design skills. But for someone who designed themselves into an anemic domain model, the path from one point to the other might be completely obscured.

Instead, maybe a better way to approach things is to look where your behavior is now, inside your "manager" classes, and try to decouple that. That's where your objects hide. Keep treating your domain model as inputs and outputs for the time being, but in your procedural code, try to find groups of things that are not really related to other things, and isolate them. Find groups of lines that have a comment on top explaining what they do - extract each into a meaningfully named function. Reduce conceptual duplication (code doing the same thing, even though it's written slightly differently) - extract those into methods. Find those member variables that are used to track things internally, but are only used by some methods. That looks like a class with some state; combine those and pull out a class.

Notice when a change causes you to modify several files. Try to understand why, and see if you can restructure things so that this proliferation doesn't happen, especially if the kind of change that caused it is a common one - if there's a history of similar change requests, and you can expect more in the future. Maybe some of the steps above will make such restructuring easier.

Keep doing this over time, gradually arriving to a more decoupled OO system. And remember, it's OK for a class to be small - they should be small, and focused on a specific responsibility. Small means less opportunity for coupling, but doesn't prevent your code from doing "bigger" things - objects achieve this through collaboration. Small and focused, with a well defined public interface and the associated rules, is what's reusable.

2
  • i really liked your offer to refactor my manage classes to small classes instead of trying to think on the correct OO structure from the start , maybe that what i should do. it is true that manager classes has coupling ,but i am trying to keep it the "only" place where the coupling is , so i will know where to change things . Jun 11, 2021 at 7:08
  • great explanation with the stairs picture ! Jun 11, 2021 at 7:10
2

Looking at this method from the customer in their example:

    public void buyPaper(PaperBoy paperBoy) {
        final MonetaryAmount price = paperBoy.getPaperPrice();
        if (wantsPaper(price)) {
            final MonetaryAmount money = wallet.takeMoney(price.getNumber());
            final Optional<Paper> paper = paperBoy.sellPaper(money);
            if (paper.isPresent())
                this.setPaper(paper.get());
            else
                wallet.add(money);
        }
    }

As you identified, if we wanted to re-use customer elsewhere then we'd need to include our PaperBoy code. The problem gets worse as you introduce other things you want the customer to buy: buyCar(CarSalesman, buyMedicine(Pharmacist) etc.

If you consider what these new methods would look like, you'd start to see some pretty similar structures emerge:

    public void buyCar(CarSalesman carSalesman) {
        final MonetaryAmount price = carSalesman.getCarPrice();
        if (wantsCar(price)) {
            final MonetaryAmount money = wallet.takeMoney(price.getNumber());
            final Optional<Car> car = carSalesman.sellCar(money);
            if (car.isPresent())
                this.setCar(car.get());
            else
                wallet.add(money);
        }
    }

I think the solution is to take the paper related concepts out of customer and have it focus on just buying things. Ideally we'd like to be able to add products to our system without needing to change our customer at all or take it to another system and use it to buy different things over there.

    public void buy(Vendor vendor) {
        final MonetaryAmount price = vendor.getPrice();
        if (canAfford(price)) {
          sold = vendor.sell(money, inventory)
          // Seems odd that a customer decides if they should 
          // get their money back but i'll leave that alone...
          if (!sold) {
             wallet.add(money)
          }
        }
    }

Now in order to interact our PaperBoy could depend on the Customer module and implement the Vendor abstraction or you could implement an Adapter in your application and keep the two separated.

There are still a lot of question marks with what I've proposed like:

  • how does a customer decide if they want a particular product
  • how does inventory work

but the point is we want the behaviour of our objects to be cohesive so working with them is like connecting lego blocks (this piece can connect with that piece) instead of gluing concepts together and creating a giant ball of mud.

Of course after proposing all of this, I also want to mention YAGNI. Remember that designing for re-use means increasing indirection\complexity and extra work! It is perfectly fine to have paper centric customers in an application that just sells paper.

1
  • 1
    "It is perfectly fine to have paper centric customers in an application that just sells paper" i think this is the part that i have to learn , not everything should be generic and reused Jun 11, 2021 at 6:53

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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