Design Principles Flashcards
What is DRY?
The Don’t Repeat Yourself (DRY) principle states that duplication in logic should be eliminated via abstraction; duplication in process should be eliminated via automation.
- Once and Only Once can be considered a subset of the DRY principle.
- The Open/Closed Principle only works when DRY is followed.
- The Single Responsibility Principle relies on DRY.
What is KISS?
The KISS principle, or Keep It Simple, Stupid, spans many trades, industries, and professions. The more complex something is, the more ways there are for it to fail, and the more difficult it is to explain to someone else who needs to understand it.
What does Encapsulate what varies mean?
Encapsulating details is about loose coupling between the “model” and the implementation details. The less tied is the “model” to the implementation details, the more flexible is the solution. And it makes easier to evolve it. “Abstract yourself from the details”.
You can write code that looks like this:
if (pet.type() == dog) { pet.bark(); } else if (pet.type() == cat) { pet.meow(); } else if (pet.type() == duck) { pet.quack() }
or you can write code that looks like this:
pet.speak();
If what varies is encapsulated then you don’t have to worry about it. You just worry about what you need and whatever you’re using figures out how to do what you really need based on what varied.
Encapsulate what varies and you don’t have to spread code around that cares about what varied. You just set pet to be a certain type that knows how to speak as that type and after that you can forget which type and just treat it like a pet. You don’t have to ask which type.
“Varies” here means “may change over time due to changing requirements”. This is a core design principle: To separate and isolate pieces of code or data which may have to change separately in the future. If a single requirement changes, it should ideally only require us to change the related code in a single place. But if the code base is badly designed, i.e. highly interconnected and logic for the requirement spread out in many places, then the change will be difficult and have a high risk of causing unexpected effects.
The thing that is “varying” in this phrase is the code. Christophe is on point in saying that it is usually something that may vary, that is you often anticipate this. The goal is to protect yourself from future changes in the code. This is closely related to programming against an interface.
“Encapsulate what varies” refer to the hiding of implementation details which may change and evolve.
Find what varies and encapsulate it. — This approach is the opposite of focusing on the cause of redesign. Instead of considering what might force a change to a design, consider what you want to be able to change without redesign. The focus here is on encapsulating the concept that varies.
Rational:
- When a varying concept is properly encapsulated in a single module, only this module is affected in case of a change. This reduces maintenance effort and ripple effects.
- When the varying concept is implemented as an protocol, variation can be introduced without changing existing and tested code. This reduces testing effort of existing code.
What is the Dependancy Inversion Principle?
The Dependency Inversion Principle (DIP) states that high level modules should not depend on low level modules; both should depend on abstractions. Abstractions should not depend on details. Details should depend upon abstractions.
- High-level modules should not depend on low-level modules. Both should depend on abstractions.
- Abstractions should not depend upon details. Details should depend upon abstractions.
Dependency Inversion Principle states that we should decouple high level modules from low level modules, introducing an abstraction layer between the high level classes and low level classes. Further more it inverts the dependency: instead of writing our abstractions based on details, the we should write the details based on abstractions.
Dependency Inversion or Inversion of Control are better know terms referring to the way in which the dependencies are realized. In the classical way when a software module(class, framework, �) need some other module, it initializes and holds a direct reference to it. This will make the 2 modules tight coupled. In order to decouple them the first module will provide a hook(a property, parameter, �) and an external module controlling the dependencies will inject the reference to the second one.
By applying the Dependency Inversion the modules can be easily changed by other modules just changing the dependency module. Factories and Abstract Factories can be used as dependency frameworks, but there are specialized frameworks for that, known as Inversion of Control Container.
What is the Single Responsibility Principle?
- The Single Responsibility Principle relies on DRY.
- Every object should have a single responsibility.
- All object’s services should be focused on carrying that single responsibility (SRP).
- The Single Responsibility Principle is closely related to the concepts of coupling and cohesion. Coupling refers to how inextricably linked different aspects of an application are, while cohesion refers to how closely related the contents of a particular class or package may be.
- All of the contents of a single class are tightly coupled together, since the class itself is a single unit that must either be entirely used or not at all (discounting static methods and data for the moment). When other classes make use of a particular class, and that class changes, the depending classes must be tested to ensure they continue to function correctly with the new behavior of the class. If a class has poor cohesion, some part of it may change that only certain depending classes utilize, while the rest of it may remain unchanged. Nonetheless, classes that depend on the class must all be retested as a result of the change, increasing the total surface area of the application that is affected by the change. If instead the class were broken up into several, highly cohesive classes, each would be used by fewer other elements of the system, and so a change to any one of them would have a lesser impact on the total system.
- In this context a responsibility is considered to be one reason to change. This principle states that if we have 2 reasons to change for a class, we have to split the functionality in two classes. Each class will handle only one responsibility and on future if we need to make one change we are going to make it in the class which handle it. When we need to make a change in a class having more responsibilities the change might affect the other functionality of the classes.
- Single Responsibility Principle was introduced Tom DeMarco in his book Structured Analysis and Systems Specification, 1979. Robert Martin reinterpreted the concept and defined the responsibility as a reason to change.
A class with more than one reason to change has more than one responsibility, that is, it is not cohesive. This introduces serveral problems:
- It is difficult to understand and therefore difficult to maintain.
- It cannot easily be reused.
- With responsibilities intertwined in the same class, it can be difficult to change one of these responsibilities without compromising others (rigidity) and it may end up breaking other parts of the software (fragility).
- The class ends up having an excessive number of dependencies, and therefore is more subject to changes due to changes in other classes as well.
Some examples of responsibilities to consider that may need to be separated include:
- Persistence
- Validation
- Notification
- Error Handling
- Logging
- Class Selection / Instantiation
- Formatting
- Parsing
- Mapping
What is the Liskov Substitution Principle?
- This principle is just an extension of the Open Close Principle in terms of behavior meaning that we must make sure that new derived classes are extending the base classes without changing their behavior. The new derived classes should be able to replace the base classes without any change in the code.
- The Liskov Substitution Principle (LSP) states that subtypes must be substitutable for their base types.
- When this principle is violated, it tends to result in a lot of extra conditional logic scattered throughout the application, checking to see the specific type of an object. This duplicate, scattered code becomes a breeding ground for bugs as the application grows.
- Most introductions to object-oriented development discuss inheritance, and explain that one object can inherit from another if it has an “IS-A” relationship with the inherited object. However, this is necessary, but not sufficient. It is more appropriate to say that one object can be designed to inherit from another if it always has an “IS-SUBSTITUTABLE-FOR” relationship with the inherited object.
- A very common violation of this principle is the partial implementation of interfaces or base class functionality, leaving unimplemented methods or properties to throw an exception (e.g. NotImplementedException). In code that you know is only going to be used by one client that you control, this is fine, but if such classes are going to be in a shared codebase, or worse, framework code that is shipped to third parties, such implementations should be avoided. If a given interface has more features than you require, follow the Interface Segregation Principle and create a new interface that includes only the functionality your client code requires, and which you can implement fully.
- A common code smell that frequently indicates an LSP violation is the presence of type checking code within a code block that should be polymorphic. For instance, if you have a foreach loop over a collection of objects of type Foo, and within this loop there is a check to see if Foo is in fact Bar (subtype of Foo), then this is almost certainly an LSP violation. If instead you ensure Bar is in all ways substitutable for Foo, there should be no need to include such a check.
Example: A common way to demonstrate this is with a set of geometric classes. Consider a class Rectangle, with properties for Length and Height. Now to model a Square, we will inherit from Rectangle, because of course a Square “IS-A” special case of a Rectangle. When we implement Square, we can simply enforce its “squareness” by forcing both Length and Height to be set whenever one of these properties is set. This ensures our Square will never have Length != Height. However, this violates an invariant of the base class, Rectangle. In this case, the invariant was never explicitly stated, but there is an expectation among clients of class Rectangle that its Height and Width can be set independently of one another, and that doing so will not have any side effects. Imagine now a bit of code that takes in a Rectangle, sets Height and Width to different values (let’s say 3 and 4), and then returns the area of the Rectangle by multiplying its Height by its Width. If one passes in an actual base Rectangle type, the result of this operation will be that the rectangle will have a Height of 3 and a Width of 4 and an area value of 12 will be returned. However, if a Square is passed in, instead, the result of the operation will be a Square with a Height of 4 and a Width of 4 and an area of 16, which is probably not what was expected.
What is the open closed principle?
- The Open/Closed Principle only works when DRY is followed.
But as we’ve learned over the years and as other authors explained in great details, e.g., Robert C. Martin in his articles about the SOLID principles or Joshua Bloch in his book Effective Java, inheritance introduces tight coupling if the subclasses depend on implementation details of their parent class.
That’s why Robert C. Martin and others redefined the Open/Closed Principle to the Polymorphic Open/Closed Principle. It uses interfaces instead of superclasses to allow different implementations which you can easily substitute without changing the code that uses them. The interfaces are closed for modifications, and you can provide new implementations to extend the functionality of your software.
The main benefit of this approach is that an interface introduces an additional level of abstraction which enables loose coupling. The implementations of an interface are independent of each other and don’t need to share any code. If you consider it beneficial that two implementations of an interface share some code, you can either use inheritance or composition.
The Open-Closed Principle (OCP) states that software entities (classes, modules, methods, etc.) should be open for extension, but closed for modification.
In practice, this means creating software entities whose behavior can be changed without the need to edit and recompile the code itself. The simplest way to demonstrate this principle is to consider a method that does one thing. Let’s say it writes to a particular file, the name of which is hard-coded into the method. If the requirements change, and the filename now needs to be different in certain situations, we must open up the method to change the filename. If, on the other hand, the filename had been passed in as a parameter, we would be able to modify the behavior of this method without changing its source, keeping it closed to modification.
The Open-Closed Principle can also be achieved in many other ways, including through the use of inheritance or through compositional design patterns like the Strategy pattern.
OPC is a generic principle. You can consider it when writing your classes to make sure that when you need to extend their behavior you don�t have to change the class but to extend it. The same principle can be applied for modules, packages, libraries. If you have a library containing a set of classes there are many reasons for which you�ll prefer to extend it without changing the code that was already written (backward compatibility, regression testing, �). This is why we have to make sure our modules follow Open Closed Principle.
When referring to the classes Open Close Principle can be ensured by use of Abstract Classes and concrete classes for implementing their behavior. This will enforce having Concrete Classes extending Abstract Classes instead of changing them. Some particular cases of this are Template Pattern and Strategy Pattern.
What is the Interface Segregation Principle?
- The Interface Segregation Principle (ISP) states that a client should not be exposed to methods it doesn’t need. Declaring methods in an interface that the client doesn’t need pollutes the interface and leads to a “bulky” or “fat” interface.
- It is better to have many small interfaces than just a few large interfaces.
- Clients should not be forced to implement necessary methods that they do not use.
- Interfaces should belong to clients, not to libraries or hierarchies.
- Application developers should favor thin, focused interfaces to “fat” interfaces that offer more functionality than a particular class or method needs.
- Ideally, your thin interfaces should be cohesive, meaning they have groups of operations that logically belong together. This will prevent you from ending up with one-interface-per-method most of the time in real-world systems (as opposed to the above trivial example).
- Another benefit of smaller interfaces is that they are easier to implement fully, and thus less likely to break the Liskov Substitution Principle by being only partially implemented.
- They also provide greater flexibility in how you implement functionality, since parts of a larger interface can be implemented in different ways.
- This principle teaches us to take care how we write our interfaces. When we write our interfaces we should take care to add only methods that should be there. If we add methods that should not be there the classes implementing the interface will have to implement those methods as well. For example if we create an interface called Worker and add a method lunch break, all the workers will have to implement it. What if the worker is a robot?
- As a conclusion Interfaces containing methods that are not specific to it are called polluted or fat interfaces. We should avoid them.
Example:
We’ll create some code for a burger place where a customer can order a burger, fries or a combo of both:
interface OrderService { void orderBurger(int quantity); void orderFries(int fries); void orderCombo(int quantity, int fries); }
Since a customer can order fries, or a burger, or both, we decided to put all order methods in a single interface.
Now, to implement a burger-only order, we are forced to throw an exception in the orderFries() method:
class BurgerOrderService implements OrderService { @Override public void orderBurger(int quantity) { System.out.println("Received order of "+quantity+" burgers"); }
@Override public void orderFries(int fries) { throw new UnsupportedOperationException("No fries in burger only order"); }
@Override public void orderCombo(int quantity, int fries) { throw new UnsupportedOperationException("No combo in burger only order"); } }
Similarly, for a fries-only order, we’d also need to throw an exception in orderBurger() method.
And this is not the only downside of this design. The BurgerOrderService and FriesOrderService classes will also have unwanted side effects whenever we make changes to our abstraction.
Let’s say we decided to accept an order of fries in units such as pounds or grams. In that case, we most likely have to add a unit parameter in orderFries(). This change will also affect BurgerOrderService even though it’s not implementing this method!
By violating the ISP, we face the following problems in our code:
- Client developers are confused by the methods they don’t need.
- Maintenance becomes harder because of side effects: a change in an interface forces us to change classes that don’t implement the interface.
Violating the ISP also leads to violation of other principles like the Single Responsibility Principle.
Another indication of an ISP violation is when we have to pass null or equivalent value into a method. In our example, we can use orderCombo() to place a burger-only order by passing zero as the fries parameter. This client does not require the fries dependency, so we should have a separate method in a different interface to order fries.
As in our burger example, if we encounter an UnsupportedOperationException, a NotImplementedException, or similar exceptions, it smells like a design problem related to the ISP. It might be a good time to refactor these classes.
Refactoring: For example, we can refactor our burger place code to have separate interfaces for BurgerOrderService and FriesOrderService:
interface BurgerOrderService { void orderBurger(int quantity); }
interface FriesOrderService { void orderFries(int fries); }
What is loose coupling?
Loose coupling is an approach to interconnecting the components in a system or network so that those components, also called elements, depend on each other to the least extent practicable. Coupling refers to the degree of direct knowledge that one element has of another.
Loose coupling is achieved by means of a design that promotes single-responsibility and separation of concerns.
A loosely-coupled class can be consumed and tested independently of other (concrete) classes.
Interfaces are a powerful tool to use for decoupling. Classes can communicate through interfaces rather than other concrete classes, and any class can be on the other end of that communication simply by implementing the interface.
Example: A Hat is “loosely coupled” to the body. This means you can easily take the hat off without making any changes to the person/body. When you can do that then you have “loose coupling”. See below for elaboration.
What is tight coupling?
Tight coupling is when a group of classes are highly dependent on one another.
This scenario arises when a class assumes too many responsibilities, or when one concern is spread over many classes rather than having its own class.
What is the difference between high and low cohesion?
Cohesion refers to what the class (or module) can do. Low cohesion would mean that the class does a great variety of actions - it is broad, unfocused on what it should do. High cohesion means that the class is focused on what it should be doing, i.e. only methods relating to the intention of the class.
Cohesion is an indication of how related and focused the responsibilities of an software element are.