This is just a simple compilation of programming design patterns
Language & domain independent strategies for solving common OO design problems
Class contains objects of other class, when it is destroyed, the contained objects will also be destroyed eg.
Paragraph contains sentence
Paragraph <.>--- Sentence
Same as composition but when the instance is destroyed, the aggregated instances are not destroyed
Car < >---- Passenger
Represent relationships between classes, but does not imply a class holds another class
uses
Application ------------ Database
Driver uses feature of car
drives
Driver ------------> Car
Design classes and relationships
- A class should have only one reason to change
- Should only have one responsibility
- Having more than one will break with minor changes
class Notes{
void add(){}
void remove(){}
void dispaly(){} // Display can be performed in many different ways
};
Solution
class Notes{
void add(){}
void remove(){}
};
class View{
void display(Notes ¬es){}
};
- Modules should be open for extension but close for modification
- Add new code to change or add features but not change existing code
class Notes{
void add(){
if(conains('!'){/*...*/} // modify existing function breaks O-C principle
}
void remove(){}
};
Solution
class Notes{
virtual void add(){}
void remove(){}
};
class TagedNotes : public Notes {
void add() override {
if(conains('!'){/*...*/}
}
void remove(){}
};
- Subtypes must be substitutable for their base types
- Ensure a subclass has all the behaviors the base class has
- It should provide a different behavior than the base class has but it should not change or modify behavior
- Changing behaviour could mean that, even returning the same type, that type could be understood in a different way
- By following Liskov principle, we automatically follow Open-Close Principle
class Operation {
virtual int ResultOf(int *begin, int *end){}
};
void Operate(Operation *op) {
int arr[4]{};
auto result = op->ResultOf(begin(arr), end(arr));
std::cout << result;
}
Suppose we need to add a boolean operator:
class BoolOperation : public Operation {
int ResultOf(int *begin, int *end) override {
// return bool
}
};
// We had to modify existing code (Operate) when creating BoolOperation -> violates Open-Close principle
// BoolOperation cannot be substitutable by Operation -> violates Liskov-Substitution principle
void Operate(Operation *op) {
int arr[4]{};
auto result = op->ResultOf(begin(arr), end(arr));
if(typeid(*op) == typeid(BoolOperation)){ // -> this breaks Liskov
// assume bool
} else{
// assume int
}
}
Solution
using ReturnType = std::variant<int, bool>;
struct IOperation {
virtual ReturnType resultOf(int *begin, int *end) = 0;
virtual ~IOperation() = default;
};
struct SumOperation : IOperation {
ReturnType resultOf(int *begin, int *end) { return std::accumulate(begin, end, 0); }
};
struct BoolOperation : IOperation {
virtual ReturnType resultOf(int *begin, int *end)
{
return true;
}
};
// helper type for the visitor
template <class... Ts>
struct overloaded : Ts... {
using Ts::operator()...;
};
template <class... Ts>
overloaded(Ts...) -> overloaded<Ts...>;
void operate(IOperation *op)
{
int arr[4]{1, 2, 3, 4};
auto result = op->resultOf(std::begin(arr), std::end(arr));
// Use visitation for type safe access
std::visit(overloaded{
[](int result) { std::cout << "Integer: " << result << '\n'; },
[](bool result) { std::cout << "Boolean: " << std::boolalpha << result << '\n'; },
},
result);
}
- Clients should not be forced to depend on methods they don't use
- An interface with too many methods will be complex to use (fat interface)
- Separate the interface and put methods based on the client usage
struct IFile {
virtual void read() = 0;
virtual void write() = 0;
virtual ~IFile() = default;
};
Fat interface, some users will just simply want to read
struct IRead {
virtual void read() = 0;
virtual ~IRead() = default;
};
struct IWrite {
virtual void write() = 0;
virtual ~IWriet() = default;
};
- Abstractions should not depend on details. Details should depend on abstractions.
- Abstractions => interfaces
- Details => classes
- Program the interface not the implementation
- Using concrete class directly creates a dependency -> difficult to modify
- Invert the dependency by using an interface rather a concrete class
class ImageReader {
virtual void decode() = 0;
virtual ~ImageReader() = default;
};
class BitmapReader : public ImageReader {
void decode() override{}
};
class ImageViewer {
BitmapReader *m_reader{}; // Dependency
void display(){}
}
Violated Open-Close and Liskov-Substitution principles
Solution
class ImageViewer {
ImageReader *m_reader{}; // Now relay on the abstract interface
void display(){}
}