IntroductionWhat are design principles?How many design principles are there?Design principles and design patternsThe PrinciplesEncapsulate what variesFavor composition over inheritanceLoose couplingProgram to interfacesSingle responsibility principleOpen-closed principleLiskov’s substitution principleInterface segregation principleDependency inversion principleReferences
Introduction
What are design principles?
Design Principles
- Guidelines, not rules, or law
- Observed to result in good object-oriented designs
- Help us avoid bad object-oriented design
Symptoms of Bad Design
- Rigidity
- when you change a part of the system, it ends up leading to a whole cascade of changes
- Fragility
- When you change your code, things begin breaking into unrelated parts
- Immobility
- it’s hard to reuse
- A design principle is a guideline on top of core object-oriented concepts
- For example, favor composition over inheritance
- The results of following these design principles are design patterns
How many design principles are there?
How many?
- No standard catalog of principles
- Varies by domain
- Some have grouped principles into handy mnemonics, like SOLID
Fundamental Principles
- Encapsulate what varies
- Favor composition
- Program to interfaces
- Loose coupling
SOLID principles
- Single Responsibility
- A class should only have one reason to change
- Open-Closed
- A class should be opened for extension, but closed for modification
- Liskov substitution
- strives to have subtypes that are substitutable for the base type
- Interface segregation
- This principle encourages us to keep our interface small and cohesive
- Dependency inversion
- high-level modules should not depend on low-level modules. Both of them should depend on the abstraction
Design principles and design patterns
Creational Design Patterns
- Abstract Factory
- Builder
- Factory Method
- Prototype
- Singleton
Design Principles
- General guidelines that can guide your class structure and relationships
Design Patterns
- Tried-and-true design solution that have been found to solve specific problem
The Principles
Encapsulate what varies
- Identify the aspects of your application that vary and separate them from what stays the same
- Is the same code changing with every new requirement?
- This design principle underlies almost all design patterns
- Strategy pattern
- Adapter pattern
- Facade pattern
- Decorate pattern
- Observer pattern
- Singleton pattern
How does it work?
Separate what varies into an independent class
Encapsulate what varies
- Look for code that changes with every new requirement
- Alter or extend the code that varies without affecting code that doesn’t
- Basics of almost every design pattern
- Pay attention to how each pattern makes use of this principle
Favor composition over inheritance
IS-A is an inheritance relationship
HAS-A is a relationship of composition
classDiagram Animal <|-- Duck: is-a Automobile <|-- Taxi: is-a
classDiagram direction LR Dog --o Owner: has-a Taxi --o Passenger: has-a
Problems with inheritance
classDiagram class Coffee{ cost() prepare() }
classDiagram direction TD Coffee <|--CoffeeWithMocha Coffee <|--CoffeeWithButter Coffee <|--CoffeeWithMilk class Coffee{ cost() prepare() } class CoffeeWithMocha{ cost() prepare() } class CoffeeWithButter{ cost() prepare() } class CoffeeWithMilk{ cost() prepare() }
- A coffee shop that sells coffee
- It has 2 methods: cost and prepare
- Customers request with condiments
- The class diagrams look like this using inheritance
- How about if we want to add a caramel condiment?
- Should we add another sub-class like that? And how about if we also have other condiments?
- Or how about the cost of the milk going up? Should we go through all sub-classes with milk to update them
- Instead of a CoffeeWithButter is-a coffee, what about a Coffee has-a condiment?
Coffee HAS-A Condiment
Create Condiment Class
classDiagram Mocha --|> Condiment Butter --|> Condiment Milk --|> Condiment Caramel --|> Condiment class Condiment { cost() prepare() } class Mocha { cost() prepare() } class Butter { cost() prepare() } class Milk { cost() prepare() } class Caramel { cost() prepare() }
Add them to our coffee
classDiagram direction LR Coffee o--"1..*" Condiment class Coffee{ Condiment[] condiments cost() prepare() } class Condiment{ cost() prepare() }
With Composition
- We can add any number of condiments easily at runtime
- Implementing new condiments by adding a new class
- No code duplication
- Avoid class explosion
Favor Composition Over Inheritance
- Instead of inheriting behavior, we can compose our objects with new behaviors
- Composition often gives us more flexibility, and even allows behavior changes at runtime
- Composition is a common technique used in design patterns
Loose coupling
What is Loose Coupling
- Components should be independent, relying on knowledge of other components as little as possible
- Loose coupling reduces the dependency between components
Tight Coupling
A Weather App
classDiagram direction LR WeatherApp --o LCDScreen class WeatherApp { LCDScreen screen getTemperature() display() }
public void display() { float temp = getTemperature(); screen.printOnScreen(temp); }
Is This Design Tightly Coupled?
- WeatherApp is relying on a concrete class to do the display
- WeatherApp knows a lot about LCDScreen
- Changes to LCDScreen are going to affect WeatherApp, and vice versa
classDiagram direction LR WeatherApp --o DesktopWidget class WeatherApp { LCDScreen screen getTemperature() display() }
Improving the App
classDiagram direction TD LCDScreen ..|> Screen Widget ..|> Screen class Screen { print() } class LCDScreen { print() printOnScreen() } class Widget { print() printInWidget() }
- We create a Screen Interface
- LCDScreen and Widget will implement this Screen interface
//LCDScreen public void print(float value) { printOnScreen(value); }
Reworking WeatherApp
classDiagram direction RL WeatherApp --o Screen class WeatherApp { Screen screen getTemperature() display() } class Screen{ print() } LCDScreen ..|> Screen Widget ..|>Screen class LCDScreen { print() printOnScreen() } class Widget { print() }
//WeatherApp public void display() { float temp = getTemperature(); screen.print(temp); }
Loose Coupling Achieved
- WeatherApp has no real knowledge of the screen, other than that it implements the Screen interface
- WeatherApp and any screen can change their internal implementations and it won’t impact the other class
Program to interfaces
Program to Interfaces, Not Implementation
- Where possible, components should use abstract classes or interfaces instead of a specific implementation
Programming in an implementation
classDiagram KillerWebSystem --o CommercialDB class KillerWebSystem { CommercialDB db } class CommercialDB { select() update() delete() insert() }
//main KillerWebSystem killerWebSystem = new KillerWebSystem(); killerWebSystem.db = new CommercialDB();
- You are depending on a concrete class CommercialDB which getting stuck when you want to change to use another DB in the future, you have to rework all things regarding that concrete DB
Programming in an interface
classDiagram KillerWebSystem --o AbstractDB class KillerWebSystem { AbstractDB db } CommercialDB ..|> AbstractDB TestDB ..|> AbstractDB class AbstractDB { select() update() delete() insert() } class CommercialDB { select() update() delete() insert() } class TestDB { select() update() delete() insert() }
//main KillerWebSystem killerWebSystem = new KillerWebSystem(); killerWebSystem.db = new TestDB(); //or killerWebSystem.db = new CommercialDB();
- Now DB can set to any concrete classes
Program to Interfaces
- Use interfaces or abstract classes when possible, rather than concrete classes
- Allows you to better exploit polymorphism
- Improves extensibility and maintainability
Single responsibility principle
- A class should have only one reason to change
Consider a Rectangle class
- Rectangle class provides 2 methods: area() and draw()
- Both Math App and Graphical App using it for calculating area and draw
- Rectangle also implements GUI
- Does this design violate the Single Responsibility Principle?
Are we violating the Single Responsibility Principle?
classDiagram class Rectangle{ area() draw() }
- Actually, Rectangle has 2 responsibilities
- calculating the area of a rectangle
- draw it on the GUI
Reusing the Rectangle class
- Assume that Math Lib wants to use Rectangle for calculating the area
- We will pull that Rectangle class out, we notice that it comes along with GUI. But we don’t want a GUI in the Math Lib
- Clearly, the lack of Single Responsibility is a problem here
Consider This Design
- We have a Modem interface that provides these 4 methods
- dial() and hangup() for communication
- send() and receive() to send data
- Does this class have a single responsibility?
- Is this interface here cohesive?
- do these methods make sense together?
Separating Out Responsibilities
- We separate 2 interfaces Data and Communication
- ModemImplementation can delegate Data class and Communication class
- Do these really need to be separated?
- It depends what we need
Single Responsibility
- Look at the change in your class: are parts of it changing while other parts aren’t?
- Change only matters if it really happens
- Apply Single Responsibility when the need is real, or you’re just creating complexity
Open-closed principle
Our object-oriented designs should be open for extension but closed for modification
A Duck Class
classDiagram class Duck{ fly() }
- Every time we make a change, we’re violating the open/closed principle
//Duck Class public void fly(){ //code to flap wings, and so on } //Later we add more code for migrating public void fly(){ //code to flap wings, and so on // or if migrating, keep up with flock // or if duck is injured, don't flap wings }
What If we create some flying behaviors?
classDiagram direction BT Fly --|> FlyBehaviors MigrationFly --|> FlyBehaviors InjuredFlying --|> FlyBehaviors class FlyBehaviors{ fly() } class Fly{ fly() } class MigrationFly{ fly() }
classDiagram direction LR Duck --o MigrationFly class Duck{ FlyBehavior fly } class MigrationFly{ fly() }
Our New Design
- Our duck code is safe and doesn't need to be altered to add new behaviors
- We can add new behaviors at any time
Open and Closed
- Allows new behavior without risking changes to proven code
- Improve maintainability and extensibility of a design
- Many techniques for this, including the use of the design patterns
Liskov’s substitution principle
Consider this method
classDiagram class TypeA{ methodA() methodB() }
- Everything works well
//main public void doSomething(TypeA a) { //code does something } TypeA a = new TypeA(); obj.doSomething(a);
Now We Drive TypeB
classDiagram direction BT TypeB --|> TypeA class TypeA{ methodA() methodB() }
//main public void doSomething(TypeA a) { //code does something } TypeB b = new TypeB(); obj.doSomething(b);
- We can end up with a situation where things actually do break in a bad way
- Can we fix it by modifying the logic of doSomething method? Didn’t we violate the Open-Closed principle?
public void doSomething(TypeA a) { if (a instanceof TypeB) { //do something special here } else { //do something normal here } } TypeB b = new TypeB(); obj.doSomething(b);
Liskov Substitution Principle
- You should always be able to substitute subtypes for their base class
The Classic Rectangle/Square example
classDiagram class Rectangle{ int height int width setWidth() setHeight() getWidth() getHeight() computeArea() }
classDiagram class Rectangle{ int height int width setWidth() setHeight() getWidth() getHeight() computeArea() } Square --|>Rectangle class Square{ setWidth() setHeight() }
- We defined a Rectangle class on the left. Everything works fine
- Now, Square is a Rectangle, right?
- Let’s create Square class as a sub-type of Rectangle
- We know that a square have same width and height
- We need to override setWidth and setHeight to make it work
//Square public void setWidth(int newWidth){ width = newWidth; height = newWidth; } public void setHeight(int newHeight){ width = newHeight; height = newHeight; }
- Does it adhere to the Liskov substitution principle?
Let’s write a test method
But What Happens When We Test?
public void testRectangleArea(Rectangle r, int width, int height) { r.setWidth(width); r.setHeight(height); if ((width * height) != r.computeArea()) { System.out.println("Rectangle area failed"); } else { System.out.println("Rectangle area passed"); } }
Rectangle myRect = new Rectangle(); Rectangle mySquare = new Square(); tester.testRectangleArea(myRect, 5, 5); //Pass tester.testRectangleArea(myRect, 5, 4);//Pass tester.testRectangleArea(mySquare, 5, 5);//Pass tester.testRectangleArea(mySquare, 5,4);//Failed
- We have a case where we substitute a sub-type with a base type and the function computeArea not working correctly. We say that it violates the Liskov substitution principle.
Interface segregation principle
Consider This VendingMachine class
classDiagram class VendingMachine{ takeMoney() brewCoffee() }
classDiagram class VendingMachine{ takeMoney() brewCoffee() brewHotChocolate() brewTea() dispenseWater() dispenseSoda() dispenseSnack() }
- Initially, we create a VendingMachine class with 2 methods
- takeMoney()
- brewCoffee()
- We are requested to support hot chocolate and tea
- Let’s add them into the interface
- Now, we want to reuse the class for a Soda machine which needs to support these methods
- dispenseWater()
- dispenseSoda()
- Once, try to support Snack machine as well
- dispenseSnack()
Some issues with this design
classDiagram direction LR ClientA --o VendingMachine ClientB --o VendingMachine ClientC --o VendingMachine class VendingMachine{ takeMoney() brewCoffee() brewHotChocolate() brewTea() dispenseWater() dispenseSoda() dispenseSnack() }
- ClientA: only want to do business with hot beverage
- ClientB: work with cold beverage
- ClientC: do business with snack
- How about the ClientC needs to update dispenseSnack method only? That is something isn’t quite right
- We have a polluted interface. We are providing a lot of somewhat unrelated methods
Cohesion
- How strong are the relationships between an interfaces’s methods?
- The SnackVendingMachine class actually only implements 2 methods in the entire vending machine interface
- It’s also same with HotBeverageVendingMachine and ColdVeverageVendingMachine class
- This is a sign that cohesion is low
- Let’s segregate the VendingMachine interface
Let’s segregate the Interfaces
- Cohesion of this class is low
//SnackVendingMachine public class SnackVendingMachine implements VendingMachine { public boolean takeMoney() { if (successful) return true; } public void dispenseSnack(Item item){ if (takeMoney()){ //code to dispense item } public void brewCoffee(){ //unimplemented, does not thing } public void brewHotChocolate(){ //unimplemented, does not thing } public void brewTea(){ //unimplemented, does not thing } public void dispenseWater(){ //unimplemented, does not thing } public void dispenseSoda(){ //unimplemented, does not thing } }
classDiagram direction BT HotBeverageMachine ..|> VendingMachine ColdBeverageMachine ..|> VendingMachine SnackMachine ..|> VendingMachine class VendingMachine{ takeMoney() } class HotBeverageMachine{ brewCoffee() brewHotChocolate() brewTea() } class ColdBeverageMachine{ dispenseWater() dispenseSoda() } class SnackMachine{ dispenseSnack() }
- Looks at the methods in separated interfaces
- They are related together
- Cohesion is high
Interface Segregation
- Keep an eye on the cohesion of your interface and classes
- Highly cohesive interfaces lead to more maintainable and more flexible systems
- Segregate interfaces as necessary to keep them focused and cohesive
Dependency inversion principle
- High-level modules should not depend on low-level modules
Typical Object-Oriented Thinking
Thinking with Dependency Inversion
graph TD hlcom[Hight-Level Component]--depends on--> llcom[Low-Level Component]
graph LR hlcom[Hight-Level Component]--depends on--> abstraction[Abstraction] llcom[Low-Level Component] --depends on-->abstraction
A Remote Control
classDiagram direction LR RemoteControl --o Television class RemoteControl { Television tv click() } class Television { turnTVOn() turnTVOff() }
- For starters, this remote control only controls a tv
- How about if we want to extend this remote to control other devices such as air-conditional, fans, or fridges,…
- Currently, this RemoteControl class depends on a concrete class Television
- How should we improve this design?
Inverting the Dependency
classDiagram direction LR RemoteControl --o OnOffDevice Television ..|> OnOffDevice class RemoteControl { OnOffDevice device click() } class OnOffDevice { turnOn() turnOff() } class Television { turnTVOn() turnTVOff() }
- Now, RemoteControl class depends on an abstraction, an OnOffDevice interface
Dependency Inversion
- Frees high-level components from being dependent on the details of the low-level components
- This helps design software reusable and resilient to change
- All relationships should involve abstract classes of interfaces