Object-Oriented Analysis And Design — Design Principles (Part 5)
Programming is full of rules. When you learn a language, you spend a lot of time memorizing what can and can’t be done with that language, from syntax rules, keywords, and sometimes things like memory management.
But in anyway, the situation is easy. If you do these things, you’ll have an obvious problem, your code won’t compile or your program will crash.
With object oriented design, it’s not that straightforward.
· If you have a situation where you could use inheritance and you don’t and instead create several classes that duplicate 90% of each other, the program won’t crash, there will be no error messages.
· If you make every member of every class public and violate encapsulation having every object reach directly into every other object, again, the program will compile, and it will run.
· If you combined every single concept in your application into one massive class that acted like a completely procedural program, well, you could do that and no alarm bells would ring.
But none of these would be good, and you’d be creating code that’s hard to read, code that breaks easily, that’s much harder to maintain, and you’ll hate adding a new feature or doing basic bug fixing, because you’ll have fragile software, and that one small modification could break the entire system.
So, good object orientation practices do not automatically get imposed, it’s up to us. We might not have enforced rules, but we do have guidelines, and we have principles that we can use.
They are general principles, things to stay aware of, and occasionally check back with as you create and iterate through your class design and building your software.
These principles aren’t as generic as just the concepts of abstraction, polymorphism, inheritance, and encapsulation. They use those ideas as a starting point and give you some more guidelines to have a better design.
Now, we’re going to discuss some popular object-oriented design principles.
It’s stands for “Keep It Simple, Stupid”. You may notice that developers at the beginning of their journey tries to implement complicated, ambiguous design.
What this principles states that “most systems work best if they are kept simple rather than making them complex; therefore simplicity should be a key goal in design and unnecessary complexity should be avoided”.
If you tried to keep it simple as much as you can, you definitely will end up having a system that’s easier to maintain and debug, easier to test, easier to be documented, and negotiate if there is a problem.
This is really important, because imagine yourself after some days, or some weeks, you figured out a problem, and you or one of your team is assigned to solve this problem. Now, Can you identify the problem and understand your code and know what it’s actually trying to do?.
“Don’t Repeat Yourself”. Try to avoid any duplicates, instead you put them into a single part of the system, or a method.
Imagine that you have copied and pasted blocks of code in different parts in your system. What if you changed any of them?, You will need to change and check the logic of every part that has the same block of code.
Definitely you don’t want to do that. This is an extra cost that you don’t need to pay for, all what you need to is to have a single source of truth in your design, code, documentation, and even in the database schema.
“You Ain’t Gonna Need It”. If you run into a situation where you are asking yourself, “What about adding extra (feature, code, …etc.) ?”, you probably need to re-think about it.
Because you implement only what’s needed, even if you are sure that you’ll need it in the future. You implement only what’s needed at this moment, under the current requirements.
This is a waste of time and efforts, who knows, maybe these features that you think you will need it, it will be changed then, or not needed at all.
Adding extra features, means adding more code to write, to maintain, to test and debug.
An object should have one and only one responsibility.
You don’t need to have an object that does different or many tasks. An object can have many behaviors and methods, but all of them are relevant to it’s single responsibility.
So, whenever there is a change that needs to happen, there will be only one class to be modified, this class has one primary responsibility.
Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification.
Whenever you need to add additional behaviors, or methods, you don’t have to modify the existing one, instead, you start writing new methods.
Because, What if you changed a behavior of an object, where some other parts of the system depends on it?. So, you need to change also every single part in the software that has a dependency with that object, and check the logic, and do some extra testing.
A super class can be replaced by any of it’s inheriting sub classes at any parts of the system without any change in the code.
It means that the sub classes should extend the functionality of the super class without overriding it.
That’s why we’ve mentioned ealier in Class Diagram that it’s not a good case practice to override the methods of the super class in inheritance.
Interfaces should be specific rather than doing many and different things.
That’s because any implementing class will only implement the specific needed interfaces rather than being forced to implement methods that it doesn’t need it.
So, large interfaces should be decomposed into smaller, more specific ones.
Try to minimize the dependency between objects by using abstraction.
If for example you have a App class that depends on very specialized classes; Database and Mail (dependencies).
Instead, we could have App object that deals with Service class, which is more abstract, rather than something very specific. So, now the App class is not dependent on the concrete classes, but on abstraction.
And the benefit of that is we are able to replace and extend the functionality of Service class without changing the App class at all.
Perhaps we can replace the Database and Mail classes, or add additional classes like Logger and Auth as well.
A common design pattern that applies this principle is called Dependency injection. We’re going to discuss design patterns in a more detail in the next tutorial.
General Responsibility Assignment Software Patterns (GRASP) is another set of design principles.
The principles here take a slightly different perspective than the principles in SOLID, although there is certainly some crossover.
GRASP tends to take a responsibility focus, like who creates this object, who is in charge of how these objects talk to each other, who takes care of passing all messages received from a user interface?, etc.
Now SOLID and GRASP don’t conflict with each other, they are not competing sets, you might choose to use one or both or neither.
When you assign a responsibility in form of a method, or fields, you assign it to the object that has the most information about it.
Imagine that you have a class called customer and order.
The customer tries to know all the orders placed by him, a common mistake is to assign this responsibility to the customer class, since the customer who will trigger this method.
But, this is not the responsibility of the customer, the order class is the one which as all the information about the orders.
It tries to determine who is taking the responsibility of creating the objects.
You try to answer these question:
· Who is responsible for creating the objects?, or, how those objects are created in the first place?
· Does one object contain another (composition relationship)?
· Does one object very closely use another, or, Will one object know enough to make another object?
And if so, it would seem to make sense to nominate those objects as taking that creatorrole and making it obvious which objects are responsible for creating other objects.
A common design pattern that applies this principle is called Factory Pattern.
It means you try to reduce the dependency between your objects.
If one object needs to connect tightly to five other objects and call 20 different methods just to work, you have a high coupling.
Lots of dependencies meaning lots of potential for breaking things if you make a change to any of these objects.
Now low coupling does not mean no coupling. Objects do need to know about each other, but as much as possible they should do what they can with the minimum of dependencies.
The more you have a class that has relevant and focused responsibilities, the higher cohesion you will have.
You try to make the responsibilities of your classes relevant, related as much as you can. You may need to break a class into some classes and distribute the responsibilities, instead of having a single class that does everything.
If, for example, we have a user interface and also some business related classes.
We don’t want to have high coupling between them to actually tie them directly together, where the user interface object has to know about the business objects and the vice-versa.
It’s very common to create a controller class just for the purpose of handling the connection between the user interface and the business related objects.
It’s perfectly normal for object to exist that takes a role in a program that isn’t a real world object as long as it has a well defined responsibility.
There is a common architectural design pattern called Model View Controller (MVC) which is an example of having a controller class.
What if there’s something that needs to exist in the application that doesn’t announce itself as an obvious class or real-world object?. What if you have behavior that doesn’t naturally fit in existing classes?
Well, rather than force that behavior into an existing class where it doesn’t belong, which means we are decreasing cohesion, we instead invent, we fabricate a new class.
That class might not have existed in our conceptual model, but it needs to exist now. And there’s nothing wrong with creating a class that represents pure functionality as long as you know why you’re doing it.
This is the idea that we can decrease coupling between objects.
If you have multiple objects that need to talk to each other, it’s very easy to have high coupling between them, where there is a lot of dependencies.
And what we can do instead is reduce those direct connections by putting an indirection object between them to simplify the amount of connections that each object has to make.
Having an object that can take the shape of several different objects. This allows us to trigger the correct behavior.
If, for example, we have an interface that’s implemented by several classes, you can assign or pass an instance of any of the sub classes to a reference variable that has the interface as it’s type. This will allow you to trigger the right methods, for the implementing class.
// Animal class is a generic class where Dog, Duck, & Kangaroo inherits from.
Dog shepherd = new Dog("Jack", "gold");
Duck mallard = new Duck("Daffy", "green");
Kangaroo rock = new Kangaroo("Steve", "red", 1.5);
Animal animals [] = { shepherd, mallard, rock };
/* Now, you should notice we called the display() method, without knowing exactly what the type of object, and it did displayed the correct method for each animal object. */
for(Animal animal: animals) {
animal.display();
}
How to design a system so that changes and variations have the minimum impact on what already exists.
Identify the parts of the system that are more likely to change, separate them from what stays the same, and then, encapsulate every part that vary in the system.
Most of the concepts we have been exploring are simply way of doing this, things like encapsulation and data-hiding, making your attributes private.
Interfaces are another area where we can wrap the unstable parts with an interface, and using polymorphism to create various implementations of this interface.
The Liskov substitution principle, where the child classes should always work when treated as their parent classes is another way.
The open/closed principle that we can add, but we try not to change code that works already is yet another.
Code Smells are a great term for when reading code, the code may be valid, it may work, but there is something about it that just doesn’t smell right.
It’s often a clue, a warning sign of a deeper problem, that there is a part in the code indicates violation of fundamental design principles and negatively impact design quality.
And here are just a few examples of what we mean by a code smell.
One would be the idea of a long method. We open up a method to read it, it has got many lines. This is the kind of thing that really needs to be split up into much smaller methods.
Working with very short or very long identifiers. Aside from using letters like ‘i’ for indexes and iteration, we shouldn’t be expecting to see variables called A and B and C in real code.