1

I have a question about encapsulation and I read these two topic (this & this) but I got more confused.

I've been reading Head First Object-Oriented Analysis and Design book and I'm trying to learn oop.

in one of the pages of this book, I've read these lines:

The idea behind encapsulation is to protect informationin one part of your application from the other parts of your application. In its simplest form , you can protect the data in your class from the rest of your app by making that data private. But sometimes the information might be an entire set of properties—like the details about a guitar—or even behavior—like how a particular type of duck flies. When you break that behavior out from a class, you can change the behavior without the class having to change as well. So if you changed how properties were stored, you wouldn’t have to change your Guitar class at all, because the properties are encapsulated away from Guitar.

I'm totally confused. especially in this sentence:

When you break that behavior out from a class, you can change the behavior without the class having to change as well.

Finally, in both scenarios, a class changes. So what is the advantage of the second scenario compared to the first scenario?

suppose this class:

public class EncapsulationExample {
.   
.     
.     


    public Pancake orderPancake(String type) {

        Pancake pancake = null;

        if (type.equals("classic"))
            pancake = new ClassicPancake();
        else if (type.equals("blueberry")) {
            pancake = new BlueberryPancake();
        } else if (type.equals("banana")) {
            pancake = new BananaPancake();
        }

        pancake.cook();
        pancake.plate();
        pancake.addButter();

        return pancake;
    }
}

We know that over time, we are going to end up with all the code that is selecting the type of pancake to instantiate, that's gonna keep changing with every new requirement that we have. articles and books say that this is a terrible situation because that code that we're changing, it's in the same file and in the same class with a bunch of important code that's controlling the production of the pancakes themselves, it's cooking the pancake, it's plating it, it's adding the topping on it, and then it's returning it. If we make a mistake updating the menu, we can bring down the entire pancake house because all this code is in the same place.

I cannot understand why being these two part of code in the same place can cause catastrophe and disaster. in this situation, they say you should encapsulate the part that varies and have something like this:

public class SimplePancakeFactory {

    public Pancake createPancake(String type) {
        Pancake pancake = null;

        if (type.equals("classic"))
            pancake = new ClassicPancake();
        else if (type.equals("blueberry")) {
            pancake = new BlueberryPancake();
        } else if (type.equals("banana")) {
            pancake = new BananaPancake();
        }

        return pancake;
    }
}

and use it this way:

public class EncapsulationExample2 {

    private SimplePancakeFactory simplePancakeFactory;

    public Pancake orderPancake(String type) {

        Pancake pancake = simplePancakeFactory.createPancake(type);

        pancake.cook();
        pancake.plate();
        pancake.addButter();

        return pancake;
    }
}

I can't understand what is the superiority and advantage of the second scenario compared to the first scenario.

in the first scenario, if I want to add another type of pancake to the conditional statements, we have just ONE change in one place (EncapsulationExample#orderPancake()). and in the second scenario, if I want to add another type of pancake to the conditional statements, again we have just ONE change in one place (SimplePancakeFactory#createPancake())

in both case, we just have one change in one place. how encapsulation helped us?

I would appreciate if you could give me some clear example. sorry for my bad english. Thank you.


Edit:

Eric Freeman in Advanced Design Patterns: Design Principles course talks about "encapsulate what varies" principle and he says:

this principle suggests that you identify the aspects of your application that vary and separate them from what stays the same. In other words, if you've got some aspect of your code that's changing, say with every new requirement that you get, well, then you know, you've got a behavior that really needs to be pulled out and separated from all the stuff that doesn't change. Why? So that later you can alter or extend the parts that vary, but do it without affecting the parts that don't vary.

and about the if-else part of orderPancake, he said:

So over time, we're going to end up with all the code that is selecting the type of pancake to instantiate that's going to keep changing with every new requirement that we have, and worse, that code that we're changing, it's in the same file and in the same class with a bunch of important code that's controlling the production of the pancakes themselves, it's cooking the pancake, it's plating it, it's adding the topping on it, and then it's returning it. If we make a mistake updating the menu, we can bring down the entire pancake house because all this code is in the same place.

my question is just about the bold parts. (none of the answers were my answer.) when Freeman says "you can alter or extend the parts that vary, but do it without affecting the parts that don't vary". I want to know (and see in code) what will happen if it affects?

when he says "If we make a mistake updating the menu, we can bring down the entire pancake house because all this code is in the same place". I just want to know (and see in code) what this bringing down looks like in? how is it? (in code)

when he says "that code that we're changing, it's in the same file and in the same class with a bunch of important code that's controlling the production of the pancakes themselves". I just want to know what is the problem of both(varying part and constant part) being in the same file?

please pay attention to my question. my question is not about After Encapsulation. my question is about Before encapsulation. the factory part and other things is not my concerns right now.

please show me in code. Thank you so much guys.

3
  • 3
    "I cannot understand why being these two part of code in the same place can cause catastrophe and disaster." - it's (probably) not going to cause catastrophe and disaster. It's going to (eventually) cause you working long hours fixing silly bugs, and debugging, and addressing PR comments because you missed to update similar code in a different part of the system that shouldn't really have anything with this, but somehow it does, and whatnot. In other words, a system with a lot of unintended/accidental coupling is going to waste your life and your intellectual energy on irrelevant nonsense. Sep 8, 2023 at 14:38
  • 2
    "I can't understand what is the superiority and advantage of the second scenario compared to the first scenario." - it's hard to see on a toy example, but it's like reading a technical book that kind of provides some value, but is poorly written and disorganized and all over the place, and is contradicting itself in different parts, vs reading one that's nicely divided into well-titled chapters, with well presented thoughts and ideas, that pedagogically limits the amount of information while allowing the reader to dig deeper if they need to. Sep 8, 2023 at 14:38
  • @FilipMilovanović Thank you Filip for your clear and useful explanations.
    – Mehdi
    Sep 13, 2023 at 21:23

6 Answers 6

2

It is the nature of an example that it is simple and that a reader can easily grasp its entire content without needing an elaborate explanation.

It is the nature of real production code that things are usually not plainly understandable by quickly looking over it, instead requiring either historical experience, documentation, or a guided explanation.

You don't see the problem with the "bad" example, because you are looking at a very simple example. What you're forgetting that this example has been oversimplified for the purpose of teaching you the basic principle.

When you apply this principle to real production code, you will see that it is not as straightforward to ensure that you make a change to one thing without impacting another. By putting a clear layer of separation between two distinct considerations, you ensure that it is very clear that they are separate and one does not inherently impact the other.

If you're dealing with very simple considerations, yeah it's not going to be difficult to make sure that your code respects both of them at the same time. Hardly anyone would struggle maintaining the code in the bad example if that were real production code.

But when you're dealing with very complex and ugly considerations, it's going to be near impossible to change one while making sure that you haven't inadvertently impacted the other.

When you look at an example in a textbook, try to remember that you're being given a beginner's oversimplification for a real world problem that would be prohibitively complex to demonstrate to a beginner.

3
  • Thank you Flater for your helpful explanation. so you mean that it is because of the simple nature of the example that I cannot see the real impact of the principle. am I right?
    – Mehdi
    Sep 22, 2023 at 10:26
  • @Mehdi Yeah that's my point. It is the curse of learning about things that avoid complexity. It is best shown by showing the kind of complexity that you'd have to suffer if you didn't heed the advice, but any example is inherently limited in how complex it can be for an explanation to be palatable to a student/reader. There's also the possibility of getting stuck in "ah but you could also do [X] instead" cycles where a reader who is so inclined can always come up with a different solution that seems equivalent but is very difficult to explain the nuanced reason why it's not equivalent.
    – Flater
    Sep 22, 2023 at 13:06
  • @Mehdi: Personally, I learn from experience. I don't learn well from pre-emptive advice about a problem that I haven't experienced yet. Therefore, as a developer I have had to work in bad code quality situations and struggled with all of the complexities and hardships of it. This is what taught me why good practice advice exists and what heeding it will do for you. It's not easy to describe - if it were, a lot of people would easily start doing it. The choice is yours. Follow it on good faith, or choose the route that shows you the bad side so you'll understand the good side. Both options work
    – Flater
    Sep 22, 2023 at 13:09
3

As always the question for the "best" software design depends on the specific context and requirements, therefore there will likely be no definite answer, but lets introduce some concepts and discuss how they are useful and how they relate to your code.

In your code you are discussing the use of a Factory. A factory is a class creating objects for you in a way you do not care how the object is created. The SimplePancakeFactory would be an example of such a factory, but the best way to use it is a bit different:

public interface PancakeFactory {       
    public Pancake createPancake(String type);
}

public class SimplePancakeFactory implements PancakeFactory {
    // create a pancake from a name 
    public Pancake createPancake(String type) {
        Pancake pancake = null;
        ...    
        return pancake;
    }
}

and in your class:

public class PancakePreparator {
    private PancakeFactory factory;

    public PancakePreparator(PancakeFactory factory) {
         this.factory = factory;
    }

    public Pancake preparePancake(String type) {
        Pancake pancake = factory.createPancake(type);
        ...
        return pancake;
    }
}

So what could be the reasons to use a factory?

First of all, you will notice that by making the Factory an actual interface the PancakePreparator does not care anymore which class created the pancake, so now you have the possibility to create different factories, e.g.:

public class KidsMenuFactory implements PancakeFactory {
     public Pancake createPancake(String type) {
            // make them smaller 
     }
}

public class SpecialCustomerMenuFactory implements PancakeFactory {
     public Pancake createPancake(String type) {
            // give them extra sugar or discount
     }
}

You can even add new receipes without changing the existing factory, by using a Delegate pattern:

// Add a receipe without changing the existing classes. 
public class SuperNewReceipesFactory implements PancakeFactory {
     PancakeFactory delegate

     SuperNewReceipesFactory(PancakeFactory delegate) {
        this.delegate = delegate;
     }

     public Pancake createPancake(String type) {
            if ("spaghetti".equals(type)) {
                return new SpaghettiPancake();
            } else {
                return delegate.createPancake(type); 
            }
     }
}

If that makes a lot of sense in this case is a question (why would you not change the factory), but its a possibility.

You can initialize your PancakePreparator with any of them. The good thing about this is, that you are able to extend your code without changing existing classes. In OOP this is referred to as the "Open/Closed Principle". You want your code be open for extension (e.g. introducing new receipes, etc.) but without changing your existing classes.

The PancakePreparator should have only one responsibility: Preparing the pancake, not dealing with how its created. By making an interface of the PancakeFactory we excercise Dependency Inversion - which means instead of the PancakePreparator depending on a specific implementation:

PancakePreparator  ->   SimplePancakeFactory 

we now eliminated this dependency (which makes the code extensible by implementing different factories):

PancakePreparator  ->  PancakeFactory  <-  SimplePancakeFactory

Note that we pass the PancakeFactory as a constructor parameter to the PancakePreparator now. This is called Dependency Injection and it allows you to use the PancakePreparator with different implementations of the PancakeFactory interface. Dependency Injection also helps you a great deal with testing. Your code is pretty linear and there is hardly anything to test, but lets imagine making a pancake is actually a very expensive operation, it retrieving a receipe from the type, looking up the ingredients inside an inventory, checking if those ingredients are actually available, remove them from the inventory (to know when you need to order new ingredients), etc. If you want to test if your Pancake Preparator actually does process the pancake correctly, you may not want to use a real pancake factory here. Instead you just want to use a "MockFactory", which is a test class, just producing a Pancake like thing, that you can inspect if it was processed correctly by the preparator. So Mocking is another principle to look up.

As a conclusion I would say the following:

You need to see the concept of design patterns like factories or other things in the context of a large software project.

If you are coding your small tool, there is no need for interfaces, factories or anything. Put everything in one class. In large software projects it is absolutely vital to know about principles that help you make code complexity manageble. Without following these principles, your code will become unmaintainable.

So take it not as "This is the better code". Take it as: As a software programmer you need to know about Patterns and the principles of clean code. Some of them I tried to touch here in this post:

  • Dependency Inversion
  • Single Responsibility
  • Open Closed Principle
  • Dependency Injection
  • Factories
  • Delegates
  • Mocks

There is a lot more to talk about, but I hope this post gives you a context on WHEN you may want to use a factory and why.

0

Software engineering is about insuring against future, as yet unknown change.

As long as all your program never changes, i.e. all it does is serve pancakes, and pancakes have no other use than being served, both versions are equivalent. But if one of the elements finds another use later, adding that functionality will be harder and more error-prone if your classes mix things that don't belong together from a domain point of view.

For instance, a pancake may be served - but perhaps it will may also be billed (by the accounting module), or submitted to a random health inspection (by the compliance subsystem), or trigger another order of flour (through the restocking mechanism)... When this happens, you don't want to have to repeat the part that goes into the object factory in several other subsystems, you really want that code to exist only once.

In a real system, you never know what additional requirements will appear for a running system, and guessing has historically been unsuccessful. Separating code by responsibility has a better chance of helping with whatever unexpected change will turn up in the future than choosing a random solution, or even the currently "simplest" solution.

1
  • For instance, a pancake may be served - but perhaps it will may also be billed (by the accounting module), or submitted to a random health inspection (by the compliance subsystem), or trigger another order of flour (through the restocking mechanism)... When this happens, you don't want to have to repeat the part that goes into the object factory in several other subsystems, you really want that code to exist only once. Could you please show me this part in code?
    – Mehdi
    Sep 8, 2023 at 12:27
0

First of all: your pancakes know how to cook and plate themselves. In my opinion that's a code smell. It also obscures why you would want to encapsulate the pancakes.

Assume your EncapsulationExample is a PancakeRestaurant class. It serves pancakes to local customers, but also offers a delivery service. In your design you'd need two methods like this:

public void servePancakeLocally(String type) {
    Pancake pancake = null;

    if (type.equals("classic"))
        pancake = new ClassicPancake();
    else if (type.equals("blueberry")) {
        pancake = new BlueberryPancake();
    } else if (type.equals("banana")) {
        pancake = new BananaPancake();
    }
    // The ugly part
    pancake.cook();
    pancake.plate();
    pancake.addButter();
    pancake.serve();
}

public void deliverPancakeRemotely(String type) {
    Pancake pancake = null;

    if (type.equals("classic"))
        pancake = new ClassicPancake();
    else if (type.equals("blueberry")) {
        pancake = new BlueberryPancake();
    } else if (type.equals("banana")) {
        pancake = new BananaPancake();
    }
    // Even uglier
    pancake.cook();
    pancake.wrap();
    pancake.putInBag();
    pancake.addButterToBag();
    pancake.pushToDelivery();
}

In this example you already see why pancakes should be objects, not actors, but the more important point is: the if-cascade determining the pancake type is now duplicated. That's dangerous, because now you have to change code in two locations once you add more pancake recipes.

So you probably want a PancakeFactory type. The implementation of that would then know the recipes for your pancakes. And maybe that's an AbstractFactory, and you have a NineToFiveICookPancakesFactory and OutsideOfficeHoursIThawFrozenPancakesFactory. They would both know what a BlueberryPancake is and how to make it, but not more.

PancakeDelivery would then be another type. PancakeWaiter would know that the pancake has to be plated and soaked in butter, and PancakeDeliveryBoy would know that the pancake has to be wrapped in foil, and a small container with maple syrup has to be put into the paper bag. They would also know what a Blueberry pancake is and how to deliver it, but not more.

You would still have if-cascades that distinguish between ClassicPancake and BlueberryPancake, but you'd have them only where the differences between the pancakes actually lead to different code behaviour. That's the motivation behind it.

3
  • This is actually a very bad example duplicating code. If you have two different ways of preparing a pancake the same principles of clean code apply. In your PancakeRestaurant you have a "preparePancake(String type)" method, which you implement through polymorphism twice (or two different methods if you feel like it). The common code (creating a pancake) is done by a shared method. Using Polymorphism or interfaces leaves you open to later add other methods (Gift Wrapping, Virtual Pancakes in Games, etc.). Downvote, because of the code duplication. Sep 8, 2023 at 15:29
  • Hm, where exactly do you see code duplication? PreparePancake is cooking the pancake from scratch in one implementation, and taking it out of the freezer and into the microwave in the other. Call me European, but i would argue that these are very different.
    – wallenborn
    Sep 10, 2023 at 16:31
  • The code duplication is inside the "factory" part that actually creates the pancakes. You should not promote this kind of duplication. Factor out the pancake creation to a method and better to a separate class. Sep 11, 2023 at 13:22
0

in both case, we just have one change in one place. how encapsulation helped us?

It didn't or it helped by keeping in one place, EncapsulationExample.class, the code about pancakes. EncapsulationExample refactored to EncapsulationExample2 is just the incipient phase of abstracting the pancake knowledge: preparing, selecting, ordering, delivering and so on. Next step after EncapsulationExample2 is "introducing an extra level of indirection"1 to decouple ordering from selection of pancake. On this route encapsulation of entire pancake knowledge is loosen in favour of encapsulation per specialised stages of pancake concept to enhance extension and maintenance.

1 The Fundamental theorem of software engineering: "We can solve any problem by introducing an extra level of indirection."

1
  • "introducing an extra level of indirection"1 to decouple ordering from selection of pancake. On this route encapsulation of entire pancake knowledge is loosen in favour of encapsulation per specialised stages of pancake concept to enhance extension and maintenance. I can't understand this part. can u explain it in code?
    – Mehdi
    Sep 8, 2023 at 12:19
0

Follow-up upon answer's comment:

"introducing an extra level of indirection"1 to decouple ordering from selection of pancake. On this route encapsulation of entire pancake knowledge is loosen in favour of encapsulation per specialised stages of pancake concept to enhance extension and maintenance. I can't understand this part. can u explain it in code?

Following is intended just to detail a way to implement with extensibility and maintainability in mind. It can be argued that ExtraLevelOfIndirection could end up with countless dependencies and it would be fair, though the purpose of the code example is to depict encapsulation and abstraction working together to enhance extensibility and maintainability.

public class SimplePancakeFactory {

    public Pancake createPancake(String type) { . . . }
}

public class ExtraLevelOfIndirection {

    private EncapsulationExample2 encapsulationExample2;
    private SimplePancakeFactory simplePancakeFactory;

    public ExtraLevelOfIndirection(EncapsulationExample2 encapsulationExample2
                                  , SimplePancakeFactory simplePancakeFactory) { 
        . . .
    }

    public void orderPancake(String type) {
        Pancake pancake = encapsulationExample2.orderPancake(simplePancakeFactory.getPancake(type));
        . . . 
    }
}

public class EncapsulationExample2 {

    public Pancake orderPancake(Pancake pancake) { . . . }
}

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.