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.

Mehdi
  • 29

6 Answers6

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.

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.

Flater
  • 58,824
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.

Kilian Foth
  • 110,899
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(&quot;classic&quot;))
    pancake = new ClassicPancake();
else if (type.equals(&quot;blueberry&quot;)) {
    pancake = new BlueberryPancake();
} else if (type.equals(&quot;banana&quot;)) {
    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(&quot;classic&quot;))
    pancake = new ClassicPancake();
else if (type.equals(&quot;blueberry&quot;)) {
    pancake = new BlueberryPancake();
} else if (type.equals(&quot;banana&quot;)) {
    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.

wallenborn
  • 1,980
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."

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) { . . . }

}