spring core
spring core is all about managing the dependencies between the components of our application.
Spring core is good at managing the dependencies between any two arbitary set of classes, but it recommends us to design the classes based on design principles provided by strategy design pattern, so that we can get most benefited out of using spring framework.
What is design pattern?
For a recurring problem, there is an best applicable solution that can be applied in solving the problem under a give context is called “design pattern”
Gang of four (Enrich Gamma, John Vissides, Richard Helm, Ralph Johnson) (GOF) design patterns. These people identified the problems that will arise while building the software applications using object oriented programming principles and documented the best possible solutions and published as “GOF patterns” (core design patterns).
Out of several patterns provided aspart of the GOF, one design pattern is “Strategy design pattern”.
There are 3 design principles provided by the strategy design pattern, that should followed while writing the components of our application, so that these components becomes completely loosely-coupled.
- Favor composition over inheritance
- Always design to interfaces, never design to concrete classes
- Code should be open for extension and should be closed for modification
- Favor composition over inheritance
Every class cannot be complete by itself, a class might have to use the functionality of another class in order to fulfill the functionality.
How can a class can reuse the functionality of another class?
There are 2 ways a class can reuse the functionality of another class
1. Inheritance
2. Composition
- What is Inheritance?
Inheritance is all about reusing the functionality of one class inside the another class. Inheritance always establishes IS-A relationship between the classes
“IS-A” relationship
all the traits of one class is part of the another class
for eg..
class A {
int i;
void m1() {}
}
class B extends A {
void m2() {}
}
since the B class is inheriting from the class A, all the traits of class A are part of B class as well, which means B has i attribute and m1() method inside it.
(or)
establishes parent-child relationship where given the child holds all the traits of the parent.
In an IS-A relationship always the child can be seen as replacement of parent, because everything that is part of the parent is even exist as part of the child class.
- What is Composition?
Composition is the another way of achieving reusability instead of using Inheritance.
Incase of composition one class is declared as an attribute inside the another class, so that using the reference of another class we access the member variables and the member methods of other class
class A {
public int i;
public void m1() {}
}
class B {
A a;
void m2() {
a = new A();
a.m1(); // to reuse the functionality
}
}
Composition always establishes “HAS-A” relationship between the classes, which means there is no parent-child relationship between the components, rather one class “uses” the functionality of another class which means “HAS-A”
In the above example we are using composition, so that B class it not an A class, but B has A class functionality being used inside it. So I cannot use B as an replacement of A class as it is not an A.
Composition refers to a thing or an entity is made up of several other things included into it.
From the above we can understand there are 2 ways we can reuse the functionality of one class inside another class
1. Inheritance
2. Composition
So which one should be used when?
There are 2 circumstances under which we use Inheritance in reusing the functionality, otherwise we need to use composition as below.
- If one class wants to reuse all the functionalities/traits of another class, then we need to use Inheritance. Here in below example if B wants to reuse all the functionalities/traits of A class like (m1, m2, m3) methods, then B should extend from A
class A {
int m1(int a, int b) {}
double m2(int a) {}
void m3(float f1, float f2) {}
}
class B extends A{
}
- But not always we need to use Inheritance even though a class wants to reuse all the traits of another class.
class Printer extends Catridge{
void print(String content) {
}
}
class Catridge {
void spray(int x, int y) {
}
}
In the above example, the Printer wants to reuse the all the functionalities or traits of Catridge class, even then also we cannot use Inheritance, because Printer is not a Catridge or cannot be expressed as Catridge. Printer has a Catridge and we should use composition only.
Favor composition over Inheritance
Strategy design pattern is recommending us to use Composition over Inheritance, because there are lot of limitations or drawbacks of using Inheritance over Composition, let us explore what are those:
- In a real-world application, a class wants to reuse only few of the traits or functionalities of another class, very rarely a class wants to reuse all the functionalities of another class. By which we can understand most of the use cases are solvable through Composition only rather than Inheritance
- There are cases where even we want to use Inheritance, we cannot and are being forced to use Composition only. Most of the object oriented programming languages including java doesn’t support multiple inheritance
which means a class cannot reuse functionality of multiple other classes, in such case the only way we can reuse the functionality of multiple other classes inside our class is through “Composition” only - Inheritance makes the classes more fragile, whereas composition makes the classes less fragile.
Inheritance:
developer-1
class A {
[when we change the returnType of the m1() to float, it leaves the B class un-compilable. Because it results in duplicate methods in the B class. The only way to fix the error in B class is to change the returnType of m1() in B class to float, which in turn effects the class C. from this we can understand Inheritance makes the classes more fragile]
int m1(int i) {
float f = 0.0;
// performs operation (computed f variable)
return 10.1;
}
}
developer-2
class B extends A {
int m1(int i) {
float j = super.m1(i);
// add additional functionality
return 20;
}
}
class C {
void m2(int a, int b) {
B b = new B();
int k = b.m1(a);
// perform operation
}
}
class Test {
public static void main(String[] args) {
C c = new C();
c.m2(10, 20);
}
}
java Test // error (linkage error) = bytecode break (re-compile)
Composition:
class A {
[even we change the returnType of the m1() method to float, the B class will be effected, but the change is controllable and doesnt penetrates to all the other classes of our application unlike incase of inheritance. The only change that is needed in B to make it compilable/executable is downcast the j variable to int type, which will leaves the C untouchable as well.]
int m1(int i) {
// perform operation
return 10;
}
}
class B {
A a;
int m1(int i) {
a = new A();
int j = (int) a.m1(i);
// perform operation
return j;
}
}
class C {
void m2(int a, int b) {
B b = new B();
int k = b.m1(a);
// perform operation
}
}
- Inheritance doesn’t support testability of the classes, whereas composition supports testability
- Mode
sports
echo
drive - outcome
0 = success
1 = failed, retry
2 = failed, out of gas
3 = failed, require service
// non functional engine
class Engine {
public int start(int mode) {
// no implementation
return 0;
}
}
class Car extends Engine {
void drive(int mode) {
int status = start(mode);
if(status == 0) {
sop("driving car");
} else if(status == 1) {
sop("failed, retry to start");
}else if(status == 2) {
sop("failed, fill gas");
}else if(status == 3) {
sop("failed, call the garage for service");
}
}
}
When can a developer can say, he has finished the development?
only after the implementation & unit testing, the developer can say he has finished the development of the feature
Mock = mimicks like original and facilitates unit testing in an isolated manner
class MockSuccessEngine extends Engine {
public int start(int mode) {
return 0; // success
}
}
class MockFailedRetryEngine extends Engine {
public int start(int mode) {
return 1;
}
}
class MockFailedOutOfGasEngine extends Engine {
public int start(int mode) {
return 2;
}
}
class MockFailedNeedServiceEngine extends Engine {
public int start(int mode) {
return 3;
}
}
class Car extends MockFailedNeedServiceEngine {
void drive(int mode) {
int status = start(mode);
if(status == 0) {
sop("driving car");
} else if(status == 1) {
sop("failed, retry to start");
}else if(status == 2) {
sop("failed, fill gas");
}else if(status == 3) {
sop("failed, call the garage for service");
}
}
}
class Test {
public static void main(String[] args) {
Car car = new Car();
car.drive(“sports”); // proper
}
}
With the above code, we are testing the Car independently without the need of Engine class by using MockEngine. But with this we cannot say the Car has been completely tested. Because we can only say the Car has been completely tested, when the test we conducted covered all the paths of code inside the Car
abstract class Engine {
int start(int mode) {
// no implementation logic
return 1;
}
}
class Car {
Engine engine;
public Car(Engine engine) {
this.engine = engine;
}
void drive(int mode) {
int status = 0;
status = engine.start(mode);
if(status == 0) {
sop("driving car");
} else if(status == 1) {
sop("failed, retry to start");
}else if(status == 2) {
sop("failed, fill gas");
}else if(status == 3) {
sop("failed, call the garage for service");
}
}
}
class MockSuccessEngine extends Engine {
public int start(int mode) {
return 0; // success
}
}
class MockFailedRetryEngine extends Engine {
public int start(int mode) {
return 1;
}
}
class MockFailedOutOfGasEngine extends Engine {
public int start(int mode) {
return 2;
}
}
class MockFailedNeedServiceEngine extends Engine {
public int start(int mode) {
return 3;
}
}
class Test {
public static void main(String[] args) {
Engine successEngine = new MockSuccessEngine();
Car car1 = new Car(successEngine);
car1.drive(“sports”);
Car car2 = new Car(new MockFailedRetryEngine());
car2.drive("sports");
Car car3 = new Car(new MockFailedOutOfGasEngine());
car3.drive("sports");
Car car4 = new Car(new MockFailedNeedServiceEngine());
car4.drive("sports");
}
}
- Always design to interfaces, never design to concrete classes
When we use composition for reusing the functionality of another class, the classes would become tightly coupled with each other.
Incase of composition my class holds the concreate reference of another class inorder to communicate with another class, so any change in another class would impact my class, which is called “coupling”.
Coupling between the classes brings lot of problems:
If there is a change required in one class, since the classes are tightly coupled within the application, there could be lot of other classes would be impacted because of change in one class so that
1. The time required for performing a change is going to be very high
2. the cost of implementing change would be high
3. as we are going to modify several classes in our application, there is a chance of more bugs being introduced within the application
from these we can understand coupling makes the application components difficult to maintain.
What kind of changes within the classes are being considered to be caused because of coupling?
class A { class B {
B b;
void m1(int i) { int m2(int a) {
b = new B(); // operation
int k = b.m2(i); return 20;
/* operation */ }
} }
}
In the above code A is tightly coupled with B class, so that any change in B class will effect even the Class A.
Let us introduce a change in B class, and see how is it effecting the class A to understand the coupling:
class B {
int m2(int a, int b) {
// operation using a, b
return 30;
}
}
when we change the method m2(..) in class B to take 2 parameters, this breaks the class A and we are forced to modify the code in the A class
class A {
B b;
void m1(int i) {
int j=0;
b = new B();
// compute j
int k = b.m2(i, j);
// operation
}
}
In the above example as a change in B class m2(…) has effected the class A, can this be considered as coupling?
No this is not called coupling, here if we observe the m2() method of B class is being used by A only, unless the A wanted the change in number of parameters being passed to m2(..) to perform operation, the change will not be made in B class.
From this we can understand, the change that has been incurred in class B is due to the change of requirement rather than coupling.
Then what kind of change is considered as coupling?
class A { class B {
B b;
void m1(int i) { int m2(int a) {
b = new B(); // operation
int k = b.m2(i); return 20;
/* operation */ }
} }
}
There is a new class being brought asking to replace or switch as below
class C {
int m4(int x) {
// operation
return 32;
}
}
In order for A class to switch from B to C we need to modify the code inside the A class. whereever we have the references in talking to the B class should be removed and rewrite the code to talk to the C class, which incurs huge changes and these changes that are caused in A are due to “coupling”
class A {
C c;
void m1(int i) {
c = new C();
int k = c.m4(i);
// operation
}
}
What is coupling?
If a class is talking to another class by using concrete reference of another class, a change in other class (switch from one to another) will directly effect my class which is called “coupling”.
How to avoid coupling between the classes?
That is where designed to interfaces, never design to concrete classes.