Skip to content

Instantly share code, notes, and snippets.

@bbrt3
Last active August 10, 2021 19:06
Show Gist options
  • Save bbrt3/17bdca0081320d7da43fdbca6571b3d0 to your computer and use it in GitHub Desktop.
Save bbrt3/17bdca0081320d7da43fdbca6571b3d0 to your computer and use it in GitHub Desktop.
Architecture
Strangler
Bridge
Factory
Abstract Factory
Mediator
Adapter
Decorator
Null object
Builder
Prototype
Singleton
Composite
Facade
Flyweight
Proxy
Chain of responsiblity
Visitor
Strategy
Observer
/*
Encapsulation refers to the bundling of data, along with the methods
that operate on that data, into a single unit.
Many programming languages use encapsulation frequently
in the form of classes.
A class is a program-code-template that allows developers to create
an object that has both variables (data) and behaviors (functions or methods).
Encapsulation may also refer to a mechanism of restricting the direct access
to some components of an object, such that users cannot access state values
for all of the variables of a particular object.
Getters/setters/access modifiers are all also a part of encapsulation.
Access modifiers:
a) Public
b) Private
c) Protected
d) Internal
e) Protected internal
Encapsulation can be used to hide both data members and data functions or methods
associated with an instantiated class or object.
Abstraction and encapsulation are related features in OOP.
Abstraction allows making relevant information visible and
encapsulation enables a programmer to implement the desired level of abstraction.
Why do we use encapsulation?
a) better control of class attributes and methods
b) class attributes can be made read-only or write-only
c) flexible: the programmer can change one part of the code without affecting other parts
d) increased security of data
*/
/*
Class elements that belong together are cohesive.
Classes that have many responsibilities will tend to have less cohesion
than classes tha have a single responsibility.
Cohesion represents relationships within each class.
If there are no relationships between elements of the class,
it may be worth considering why they happened to be grouped together.
Coupling represents relationships between classes.
---------------NOT-MUCH-COHESIVE-CLASS-------------------
FIELD A FIELD B FIELD C
| |_______ |
|______________________|______|
| |
METHOD 1 METHOD 2
|
___________|
|
METHOD 3
(private)
----------------------------------------------------------
-----------------HIGH-COHESIVE-CLASSES--------------------
CLASS 1 | CLASS 2
|
FIELD A FIELD C | FIELD B
|_______________| | |
| | |
METHOD 1 | METHOD 2
| |
| |
| METHOD 3
| (private)
----------------------------------------------------------
*/
/*
Coupling represents relationships between classes.
Tight Coupling
Binds two (or more) details together in a way that's difficult to change.
Loose Coupling (usually prefered)
Offers a modular way to choose which details are involved
in a particular operation.
*/
/*
Command Query Separation Principle
Commands
a) mutable state
b) can invoke queries
Queries
a) not mutable state
b) idempotent (state of the system won't ever change)
You can ask a question as many times as you want
but it won't change the answer.
c) safe to invoke
It's okay to invoke query from a command.
If command returns something it has side effects we should eliminate.
If query doesn't return anything then we should repair that too.
CQS makes it easier to reason about the code
(as long as everybody in our projects follows it).
It makes it simpler to read and understand code.
*/
// Commands
// They return nothing
void Save(Order order);
void Send(T message);
void Associate(IFoo foo, Bar bar);
// Queries
// They all return something
Order[] GetOrders(int userId);
IFoo Map(Bar bar);
T Create();
/*
High level modules should not depend on low-level modules.
Both should depend on abstractions.
Abstractions should not depend on details.
Details should depend on abstractions.
How do I know if something depends on something else?
Dependencies in C#
a) References required to compile
b) References required to run
If you follow DIP, those references should point away from
your low-level infrastructure code and towards high-level abstractions
and business logic.
What's difference between high-level and low-level?
High-level:
a) More abstract
b) Business rules
c) Process-oriented
d) Further from input/output (I/O)
Low-level:
a) Closer to I/O
b) "Plumbing" code
c) Interacts with specific external systems and hardware
DIP is all about separating concerns.
What's an abstraction?
a) Interfaces
b) Abstract base classes
c) "Types you can't instantiate"
What about details?
Abstractions shouldn't be coupled to details.
Database, file system, configuration, web apis, etc.
Abstractions describe what needs to happen
- Send a mail
Details specify how
- Send an SMTP email over port 25
*/
// example of abstraction dependent on detail
public interface IOrderDataAccess
{
// hidden dependency!
// users of IOrderDataAccess are force-dependent
// on SqlDataReader
SqlDataReader ListOrders(SqlParameterCollection params);
}
// Fix
public interface IOrderDataAccess
{
List<Order> ListOrders(Dictionary<string, string> params);
}
/*
Hidden Direct Dependencies
a) Direct use of low level dependencies
b) Static calls and new (NEW IS GLUE!, it creates coupling)
c) Causes pain
- tight coupling
- difficult to isolate and unit test
- duplication
By adding interfaces we changed tigh coupling to loose coupling.
DIP often goes hand-in-hand with Dependency Injection (DI).
Cleint injects dependencies as:
- constructor arguments
- properties
- method arguments
DI is a implementation of strategy pattern.
*/
/*
ALTERNATIVE VERSION
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.
We should favour composition over inheritance.
Inheritance is problematic because most programming languages
only allow single inheritance, so if class derives from another class,
it cannot derive from something else.
That's where composition comes in.
If programming language would allow multiple inheritance,
then there would be no need for interfaces! (Eiffel language)
Composite pattern
Composite is a special implementation of an interface.
It works well with COMMANDS (void return type).
*/
// before
public MessageStore(DirectoryInfo workingDirectory)
{
this.workingDirectory = workingDirectory;
this.cache = new StoreCache();
this.log = new StoreLogger();
this.fileStore = new FileStore(workingDirectory);
}
public void Save(int id, string message)
{
new LogSavingStoreWriter().Save(id, message);
this.Store.Save(id, message);
this.Cache.Save(id, message);
new LogSavedStoreWriter().Save(id, message);
}
// after
public class CompositeStoreWriter : IStoreWriter
{
private readonly IStoreWriter[] writers;
public CompositeStoreWriter(params IStoreWriter[] writeres)
{
this.writeres = writeres;
}
public void Save(int id, string message)
{
// all writers will use their Save implementation!
foreach (var w in this.writeres)
{
w.Save(id, message);
}
}
}
public MessageStore(DirectoryInfo workingDirectory)
{
this.workingDirectory = workingDirectory;
this.cache = new StoreCache();
this.log = new StoreLogger();
this.fileStore = new FileStore(workingDirectory);
// we add all classes that we want to write something to our composer
this.writer = new CompositeStoreWriter(
new LogSavingStoreWriter(),
this.fileStore,
this.cache,
new LogSavedStoreWriter()
);
}
public void Save(int id, string message)
{
// much simpler, just one call!
this.writer.Save(id, message);
}
/*
Decorator design pattern
Good for dealing with queries (commands as well)!
*/
public class StoreCache : IStoreCache, IStoreWriter
{
private readonly IStoreWriter _writer;
// STORECACHE IS A DECORATOR FOR ISTOREWRITER
public StoreCache(IStoreWriter writer)
{
_writer = writer;
}
public virtual void Save(int id, string message)
{
this.writer.Save(id, message);
}
}
// BABUSHKA!!
// decorator aka rossian doll model
/*
Explicit Dependencies Principle
Your classes shouldn't surprise clients with dependencies.
List them up front, in the constructor
Think of them as ingredients in a coking recipe
When having hidden dependencies in our classes
it's like calling for some ingredient that wasn't on the list.
Don't create your own dependencies, instead you should depend on abstractions.
Request dependencies from client.
*/
/* Clients should not be forced to depend on methods they do not use.
You should prefer small, cohesive interfaces to large, "fat" ones.
What does interface mean in ISP?
C# interface type/keyword
public (or accessible) interface of a class
What's a client?
In this context, the client is the code that is interacting
with an instance of the interface. It's the calling code.
Violating ISP results in classes thatr depend on things they don't need.
More dependencies means:
a) more coupling
b) more brittle code
c) more difficult testing
d) more difficult deployments
Detecting ISP violations
a) Large interfaces
b) NotImplementedException
c) Code uses just a small subset of a larger interface
ISP is related to LSP, Cohesion and SRP
Use PDD!
Fixing ISP Violations
a) Break up large interfaces into smaller ones
- compose fat interfaces from smaller ones for backward compability
b) To address large interfaces you don't control
- create a small, cohesive interface
- use the adapter design pattern so your code can work with the Adapter
c) Clients should own and define their interfaces
Where do interfaces live in our apps?
Client code should define and own the interfaces it uses
Interfaces should be declared where both cliend code
and implementations can access it.
*/
public interface INotificationService
{
// two concerns - violation of ISP
// we always have to implement both features
void SendSMS();
void ReceiveMail();
}
public class SMSNotificationService : INotificationService
{
void SendSMS()
{
// sends sms
}
void ReceiveMail()
{
// we don't need receiving mail in our SMSNotification service
// so we chose to not implement ReceiveMail method
// which is a violation of LSP
throw new NotImplementedException();
}
}
// Fix - create interfaces for each of the concerns
// which will result in separation of concerns and interface segregation
// interfaces are smaller now!
public interface ISMSService
{
void SendSMS();
}
public interface IMailService
{
void ReceiveMail();
}
// specific service that uses only what we need and nothing else
public class SMSService : ISMSService
{
void SendSMS()
{
System.Console.WriteLine("SENT SMS");
}
}
/*
unit bias
if you want to lose weight - use smaller plate
if you want to gain weight - use bigger plate
for humans a plate of food is a plate of food no matter the size!
lego vs duple example
duplo dragon vs lego dragon
lego pieces are smaller so its harder to build a dragon from them
if you only have few pieces
duplo pieces are bigger so the same amount of blocks as in lego
will let you build a dragon that actually looks like a dragon for other people
without telling them that it is one
having a codebase that has many classes seems scary
but must of those classes are small, so we shouldnt be afraid of that
we don't really have to understand all of them
many trivial or simple classes
vs
ten classes that each of them has thousands of lines of code
it's all about the correct level of granuality
ISP:
Clients should not be forced to depend on methods they don't use.
So who knows/defines the interfaces?
Interfaces exist to introduce loose coupling.
It's not the class that needs the interface, it's the client,
so the client owns / defines what he needs in the interface.
Interface is not defined by the class that uses it,
it's defined by the client that consumes the interface.
Since the client is the one defining what he needs in interface,
there's no reason to have big interfaces.
You should favour role interfaces over header interfaces.
Header interface is a extracted (or just big) interface that has a lot of members,
it is called header interface, because look like good old C++ header files.
If you find yourself having a header interface, it is unlikely that you will
ever find another concrete class that will implement it without throwing lots of errors.
Role interface is a interface that defines small amount of members.
Extreme role interface is a interface that has single member.
Solving ISP problems may help you address LSP issues.
Because when we introduce more smaller interface, then the chances are
we will only have methods that we need and no other redundant ones,
so no error throwing!
f
*/
Subtypes must be substitutable for their base types.
LSP states that the IS-A relationship is insufficient and
should be replaced with IS-SUBSTITUTABLE-FOR.
public class Rectangle
{
public virtual int Height { get; set; }
public virtual int Width { get; set; }
}
public class Square : Rectangle
{
private int _height;
public override int Height
{
get { return _height; }
set
{
_width = value;
_height = value;
}
}
// width implemented similarly
}
public class AreaCalculator
{
public static int CalculateArea(Rectangle r)
{
return r.Height * r.Width;
}
}
// Problem
// Code expects to work with rectangle
// but the square is used instead
Rectangle myRect = new Square();
myRect.Width = 4;
myRect.Height = 5;
Assert.Equal(20, AreaCalculator.CalculateArea(myRect));
// Actual result: 25
// Solutions
// 1. Eliminating the Square class
// and using property for identifying if rectangle is square
public class Rectangle
{
public int Height { get; set; }
public int Width { get; set; }
public bool IsSquare => Height == Width;
}
// 2. Eliminating inheritance
// no dependencies!
public class Rectangle
{
public int Height { get; set; }
public int Width { get; set; }
}
public class Square
{
public int Side { get; set; }
}
// Symptoms of LSP violations
// 1. Type checking with is or as in polymorphic code
foreach (var employee in employees)
{
if (employee is Manager)
{
Helpers.PrintManager(employee as Manager);
break;
}
Helpers.PrintEmployee(employee);
}
// corrected version (1)
foreach (var employee in employees)
{
// Each custom type implements print method
// that tells us which role is theirs
// so no need for ifs!
employee.Print();
}
// corrected version (2)
foreach (var employee in employees)
{
// helpers method knows who is who
// ifs can be inside it
Helpers.PrintEmployee(employee);
}
// 2. Null checks
// Very familiar behavior to checking the type
foreach (var employee in employees)
{
if (employee == null)
{
Helpers.PrintManager("Employee not found.");
break;
}
Helpers.PrintEmployee(employee);
}
// NULLS BREAK LSP
// check null object pattern
// LSP is a subset of polymorphism
// polymorphism that doesn't follow LSP is kind of broken
// 3. NotImplementedException
public interface INotificationService
{
void SendText(string SmsNumber, string message);
void SendEmail(string to, string from, string subject, string body);
}
public class NotificationService : INotificationService
{
void SendText(string SmsNumber, string message)
{
// send text logic
}
void SendEmail(string to, string from, string subject, string body)
{
// LSP VIOLATION!!
throw new NotImplementedException();
}
}
//Fixing LSP Violations
// 1. Follow the "Tell, don't ask" principle
// Instead of asking if something is of some type
// you should be able to tell that something is of some type.
// 2. Minimize null checks with C# Features such as null conditional operators,
// null coalescing operators
// 3. Guard clauses
// 4. Null object design pattern
// 5. Make sure you fully implement interface when you inherit from it
// alternative
Liskov Substitution Principle
Subtypes must be substitutable for their base types
Client should be able to consume any implementation without changing the correctness of the system.
If you have a client that talks to the version A of the interface and that doesn't cause the entire system to crash, if that client starts to talk to implementation B of the same interface, and that causes the system co crash, then you have changed the correctness of the system.
If implementation B also doesn't cause system to crash, then you probably haven't changed correctness of the system.
Correctness of the system is superset of boundaries that limit your changes on the system, the simplest one being application crashing.
Violations of LSP:
a) throw(NotSupportedException)
There are members of the interface that cannot possibly work correctly.
Example:
ReadOnlyCollection<T> : ICollection<T>
Add, Clear, Remvoe methods are throwing NotSupportedException because they don't apply to read-only type of collection.
This changes the correctness of the system, because before the system would be running without any exceptions being thrown and then afterwards the exceptions would stop being thrown.
b) Downcasts
as, is keywords, checking if something really is implementation of some type
List<int> a = new List<int>();
if (a is List<int>)
{
// behavior
}
foreach (elem in a as IEnumerable<a>)
{
// behavior
}
c) Extracted interfaces
LSP is often violated by attempts to remove features.
Either you implement entire interface or you don't implement it at all.
Reused Abstractions Principle compilance indicates LSP compilance.
/*
Software entities (classes, modules, functions, etc.) should be open for extension and closed for modification.
It should be possible to change the behavior of a method without editing its source code.
OPEN TO EXTENSION
- new behavior can be added in the future
- code that is closed to extension has fixed behavior
CLOSED TO MODIFICATION
- changes to source or binary code are not required
- the only way to change the behavior of code
that is closed to extension is to change the code itself
Why should code be closed to modification?
Less likely to introduce bugs in code we don't touch or redeploy.
Less likely to break dependent code when we don't have to deploy updates.
Fewer conditionals in code that is open to extension results in simpler code.
Bug fixes are okay.
We should balance abstraction and concreteness.
Abstraction adds complexity.
We need to predict where variation is needed and apply abstraction as needed.
"NEW IS GLUE"
How can you predict future changes?
- start concrete
- modify the code the first time or two
- by the third modification, consider making the code open to extension
for that axis of change
Typical approaches to OCP
a) Parameters
public class DoOneThing
{
public void Execute()
{
Console.WriteLine("Hello");
}
}
public class DoOneThing
{
public void Execute(string message)
{
Console.WriteLine(message);
}
}
b) Inheritance
public class DoOneThing
{
public virtual void Execute()
{
Console.WriteLine("Hello");
}
}
public class DoOneThing
{
public override void Execute()
{
Console.WriteLine("Goodbye");
}
}
c) Composition / Injection
public class DoOneThing
{
private readonly MessageService _messageService;
public DoOneThing(MessageService messageService)
{
_messageService = messageService;
}
public void Execute()
{
Console.WriteLine(_messageService.GetMessage());
}
}
Prefer implementing new features in new classes.
Why use a new class?
- design class suits the problem at hand
- nothing in current system depends on it
- can add behavior without touching existing code
- can follow Single Responsiblity Principle
- can be unit tested (no dependencies!)
*/
// another version
Reused Abstraction Principle
If you have abstractions and they are not being reused by being implemented by various different concrete classes, then you probably have poor abstracations.
Abstraction is the elimination of the irrelevant and amplification of the essential.
If our code has interfaces that have only one implementation, then we are breaking Reused Abstraction Principle.
Solution:
Instead of creating interfaces up-front, start by creating the concrete behavior of the system and then when you've added some concrete behavior, start looking for features or behaviors that those classes have in common.
Interfaces are not designed, they are discovered as the system grows.
OCP
Class should be open for extensibility, but closed for modification.
After you put your code to the production (is used by clients), you are no longer allowed to make changes to that class (BUG FIXING IS OKAY).
Instead everyone should be able to extend the functionality of that class.
Once someone relies on class that you wrote, changing its behavior may have bad effects on working of the application.
We should favour composition over inheritance.
One of the ways you can favour composition over inheritance is by using Strategy design pattern.
Strategy pattern is a way where you can vary part of the implementation of the class without changing the class itself.
Inheritance-based way of opening class for extensibility:
USE VIRTUAL KEYWORD ON YOUR METEHODS
By doing that, other developers that would want to modify the behavior of your class, will be able to override those methods. They can also call the base implementation if they want.
Factory method (design pattern)
It is a method that creates an instance of a polymorphic class.
protected virtual FileStore Store
{
get { return this.fileStore; }
}
In C# readable properties are actually compiled into IL methods, the example above would be named get_store.
They have special attributes that describe them as properties but really they are methods.
This change allows us to derive from this class and just override the property that we want to redefine, while keeping the overall working of class.
INHERITANCE-BASED METHOD IS NOT PREFFERED!!!
Essentialy what the OCP says is that a class should be open for extensibility but closed for modification.
Closed for modification means you can't update or delete the class, you can only create the class and read its source code.
Strangler pattern
Instead of changing the existing system we add new behavior or new implementations around that old system and then we gradually move the clients from using the old system to using the new system.
After that we end up with a new system and then we can finally turn off the old system.
Open Closed Principle is all about not changing something that someonoe depends on but instead trying to replace it with another implementation that may or may not use the base implementation inside.
/*
Programs should be separated into distinct sections,
each addressing a separate concern,
or set of information that affects the program.
High-level code should not know about how low-level implementation details
are implemented, nor should it be tightly coupled to the specific details.
Imagine a fridge in which we keep food, drinks,
but also there are cleaning products, toilet stuff, and your bike.
*/
/*
Each software module (class) should have one and only one reason to change.
Multipurpose tools don't perform as well as dedicated tools
Dedicated tools are easier to use
A problem with one part of a multipurpose tool can impact all parts
What is a responsibility?
It is a decision our code is making about the specific implementation details
of some part of what the application does.
It's the answer to the question of how something is done.
Examples of responsibilities:
a) Persistance
b) Logging
c) Validation
d) Business logic
Responsibilities change at different times for different reasons.
Each responsibility is an axis of change.
When two or more responsibilities are mixed in the same class,
it produces tight coupling between these responsibilities.
If the details change at different times for different reasons,
it's likely to cause problems with class in the future.
When classes have multiple responsibilities it is difficult to test them.
We can apply SRP by splitting responsibilities into separate classes and then delegating them,
instead of using hardcoded method that might change in the future.
It will let us test all of the responsibilities separately, which is much simpler.
*/
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment