design patterns in code = tropes in storytelling (ex: the story pattern of the "heroes journey)
- how you handle creating new objects
- how objects are attached to each other
- previously we studied design principles like
decomposition
and generalization` and how theyre expressed in UML class diagrams with:
- association, aggregation, composition, inheritance
depending on the relationship of objects you'd like, you can use various design patterns to suit that need.
ex: structural pattern = flavor pairings
- combo of flavor pairings = a suitable relationship for tasting good
- some ingredients are mixed together where their flavors are nearly indistinguishable (ex: garlic w chickpeas when making hummus), while others are mixed in where you can still taste distinct flavors but they pair well together (ex: ingredients in a salad), and others are more loosely paired so you dont combine ingredients at all but have the flavors separate because the flavors already complement each other so well (ex: wine and cheese).
the relationships among the food are defined by the pairings. in software, each structural pattern determines the various suitable relationships among the objects.
food pairings = object pairings.
this focus on how objects distribute work to accomplish a goal ex: think of a pit crew...each member/object has a specific job, combined they help switch out the car to achieve victory.
like a game plan, a behavioral pattern lays out the overall goal and purpose of each object.
- creational pattern
- "only one object of a class" (ex: "preferences" class in a poker game to determine design and colors of game)
- is globally accessible
- the idea of a singleton is that you just want one instance of it, 2 or more would fuck up the pattern (ex: 2 preferences classes means you dont know which class you're using, therefore your preferences in the game may/may not show up...the code becomes unreliable)
- you can codify this desire to only have one singleton within the singleton itself:
public class ExampleSingleton {
private static ExampleSingleton uniqueInstance = null;
private ExampleSingleton() {
... //singleton code..actual content
}
public static ExampleSingleton getInstance() {
if (uniqueInstance == null) {
uniqueInstance = new ExampleSingleton();
}
return uniqueInstance;
}
...
}
by hiding the regs constructor and forcing the instantiation of the ExampleSingleton object to go thru getInstance()
, we add basic gatekeeping to enforce the fact that there can only be one singleton. Now we dont have to worry about multiple devs creating multiple instances of ExampleSingleton
.
- another benefit of singletons: lazy creation - object isnt created until its actually needed (ex: we dont create
ExampleSingleton
until we actually need it).
tl;dr: provide global access to a class that is restricted to one instance.
where this is useful: preferences for an app, print queue for your printer, etc
- purpose: create objects
- ex: knife store
- we have an online store where we sell knives. when someone goes to order a knife, we create a
Knife
object (this is called 'concrete instantiation'..happens with thenew
operator).
ex:
Knife orderKnife(String knifetype) {
Knife knife;
if knifeType.equals("steak") {
knife = new SteakKnife();
} else if knifeType.equals("chefs") {
knife = new chefsKnife();
}
//prep knife
knife.sharpen();
knife.polish();
knife.package();
return knife
}
now imagine if we get more knives, thats more conditionals, but notice the methods below would stay the same. we can abstract this out into its own factory object to create new knives so theres one dedicated place to create objects of diff types.
public class KnifeStore {
private knifeFactory factory;
//if someone instantiates KnifeStore, they'll have to provide a factory
public KnifeStore(KnifeFactory factory) {
this.factory = factory;
}
Knife orderKnife(String knifetype) {
Knife knife;
knife = factory.createKnife(knifeType);
//prep knife
knife.sharpen();
knife.polish();
knife.package();
return knife
}
}
^ Here orderKnife
is a 'client' of our factory object ,it uses it.
factory object benefits: if theres multiple clients that want to use the same set of classes, but using the factory pattern, you cut out redundant code and make the software easier to modify.
tl;dr: if multiple stores need to create knives, you dont have to re-write that code over and over, plus adding new knives just needs to occur in one place.
in this sense ^, we've generalized orderKnife
...this is also called 'coding to an interface, not an implementation'
this above is a FACTORY OBJECT. lets talk about the factory method pattern
:
factory method = intent is to define an interface for creating objects, but let the subclasses decide which class to instantiate. ex:
public abstract class KnifeStore {
public Knife orderKnife(String knifetype) {
Knife knife;
//now creating a knife is not a factory, but a method in the class
knife = createKnife(knifeType);
knife.sharpen();
knife.polish();
knife.package();
return knife
}
abstract Knife createKnife(String knifeType)
}
notice here the KnifeStore
superclass is now listed as abstract
, so it cannot be instantiated as-is, but as a subclass like BudgetKnifeStore
or QualityKnifeStore
. we've also made createKnife
abstract
and empty so the subclasses must define them as well.
ex:
public BudgetKnifeSrtore extends KnifeStore {
Knife createKnife(String knifetype) {
if knifeType.equals("steak") {
return new BudgetSteakKnife();
} else if knifeType.equals("chefs") {
return new BudgetChefsKnife();
}
//more types
else return null
}
}
so now we can create many knife stores, but each store has to create its own createKnife method for object creation. This generalizes not just creation of the knife object (like we saw with the factory object before), but the KnifeStore itself becomes generalized.
the factory method pattern ALWAYS goes like this:
- a creator superclass which has an abstract factory method that its subclass must instantiate (ex:
createKnife
) - the creator subclass, known as the "concrete creator" that does "concrete instantiation", creating the factory method that actually create the objects (ex:
BudgetKnifeStore
). - the concrete product class which is created from the concrete creator class (ex:
BudgetSteakKnife
). - whatever superclass is used to create the concrete product (ex:
Knife
)
- Factory - method or object that allows you to create other objects
new
keyword - example of 'concrete instantiation' to create actual objects- factories allow client code to operate on generalizations, this is called "coding to an interface, not an implementation "
- factory objects may be useful to create objects and have a single place for them(1st ex), but you can use the factory method pattern to allow concrete creator subclasses to create the actual object by having the method to create the object exist as an abstract method (aka to create the subclass you gotta instantiate the method)
street level front of a starbucks = a facade, indicates what services are available..
facades display whats available, hiding away all the implementation details and extra work that needs to be done.
you dont need to know all the steps to create the coffee, order the beans, etc, you just need to give your order and your money.
facade patterns work like waiters, theyre a wrapper class that encapsulates a subsystem in order to hide its complexity.
ex: a customer class needs to interface with the checking, savings, and investment classes. instead of interacting with all 3 classes and having to know everything about each of the classes you can have another BankService class which the Customer class interfaces with, hiding the details of those 3 classes.
- create the interface (ex: IAccount)
- impelement the interface w 1 or more classes
public class Checking impelements IAccount {}
public class Saving impelements IAccount {}
public class Investment impelements IAccount {}
- implement interface with facade class
- use facade class to interact with subsystem.
public class Customer {
public static void main(String args[]) {
BankService myBankService = new BankService();
int mySaving = myBankService.createAccount("saving", new BigDecimal(500.00));
int myInvestment = myBankService.createAccount("investment", new BigDecimal(1000.00));
myBankService.transferMoney(mySaving, myInvestment, new BigDecimal(300.00));
}
}
key design principles this pattern uses: enapsulation, information hiding, separation of concerns
facade design pattern:
- hides complexity of a subsystem by encapsulating it behind a unifying wrapper called a 'facade class'
- removes need for client classes to manage a subsystem, delegating that to the facade class itself, decoupling the client classes from the subsystem.
- acts only as a point of entry into a subsystem.
$
in jQuery is a facade class...you dont need to know about all of the implementation details, you just need to know that you can access jQuery's methods and properties with it.
think of the apple adapter to put hdmi to thunderbolt connection (ex: plug in external monitor)
output of one system may not conform to the expected input to another system
this is especially true for your pre-existing system working with 3rd party libraries or needs to speak with other systems.
how this works
- adapter sits b/w client and adaptee, adapter conforms to the target interface that the client understands, the adapter transforms the request in a way the adaptee understands, and sends the request on over
an adapter class is an example of a wrapper class...it encapsulates the adaptee and presents a new interface that the client can interact with
think of parsers
- composes nested structures of objects and deals with the classes for these objects uniformly.
component interface serves as the super type for a set of classes so they can all be dealt with uniformly
- ^ this is polymorphism, all implementing classes conform to the same interface.
you can also use an abstract superclass instead of a supertype.
composite pattern is used to address 2 issues:
- how do we use individual objects to build a tree-like structure
- how can we treat individual types of objects uniformly without checking their types?
sometimes its easier to use a placeholder to represent someone else
- crash dummies instead of ppl used representatives to speak for a co.
- a credit card is a replaement for cash.
these are proxies...they act as a simple, lightweight version of the original object.
proxy design pattern - proxy class represents subject class.
why use a proxy
- to act as a virtual proxy - to reduce system load
- to act as a protection proxy - to protect info in the real subject class
- to act as a remote proxy - proxy class is local, real class is remote (ex: think of a google doc)
proxy delegates substantial request to object, but can do stuff on its own.
how this works: you have a real object and its proxy object. both objects implement the same interface, achieving what they call 'polymorphism' which basically means since they implement the same interface, you can call the same methods on them regardless of their internal implementations.
the client object interacts with the proxy object instead of the real object.
think of a someone buying something: client object, and warehouse object. client wants something, an order is given to the warehouse. But what if the warehouse doesnt have it in stock? what if the warehouse is too far away to have it shipped in the allotted time? what if there are multiple warehouses?
a proxy can be created, called OrderFulfullment
. This proxy class can handle seeing if the item and item amount is actually in stock, this protects the warehouse
class from having to know about the client class, and ensures it can stay just doing its own responsibilities and not worrying about logistics. This is also more efficient and thereby less intensive on the system.
steps to create the proxy pattern
- design the subject interface
public interface IOrder {
public void fulfillOrder(Order);
}
- implement the real subject class (with the interface)
- create the proxy class
proxy pattern characteristics
- proxy class wraps the real subject class using the same interface
- ^ has a polymorphic design so that the client class can expect the same interface for the proxy and the real subject class
- to use a lightweight proxy in place of a resource intensive object until its actually needed.
- to present a local representation of a system that is not in the same physical or virtual space
this patterns provide a powerful means of indirection, allowing you to build designs that are more protected and less resource-intensive.
allows us to dynamically attach behaviors and responsibilities to a class
uses aggregation to combine behaviors at runtime
aggregation - weak relationship ("has a")
think of components A,B, C:
- 'A' is a base component
- 'B' augments 'A'
- 'C' augments 'B'
^ in this case, theyre stacked like CBA
think of base component as coffee...decorators for this are milk,sugar, hot chocolate
why use the decorator pattern?
- to reduce the # of classes needed to offer a combo of behaviors
- allows object to dynamically add behaviors to others...you can build functionality by stacking objects wit this pattern.
LOOK AT THE SCREENSHOT
- decorator lets you embellish your objects by adding any number of behaviors dynamically at runtime using aggregation rather than inheritance
- aggregation lets us create a stack objects
- polymorphism is ahieved by impelemnting a single interface
- each decorator object in the stack is aggregated in a 1-1 relationship with the object below it in the stack
- by combining aggregation and polymorphism, we can recursively invoke the same behavior down the stack and have the behavior execute upwards from the concrete object
think: stack of turtles: 3. cool sunglasses turtle 2. super turtle
- base turtle
3 references 2 references 1, which then builds up the stack of turtles at runtime to create 'cool sunglasses turtle'.
this pattern ensures you dont have to write so many new objects without writing them all out
ways objects collaborate to achieve something
ex: each b-ball player has a role, they work together diff ways to win
ex: youre a chef w a chain of italian restaurants, and all recipes have similar steps (boil water, cook pasta, sauce, etc) but are slightly diff depending on recipe. you could put your generalized steps in a class, and have a subclass for the particulars different steps, so you could use fettucini alfredo and pasta bolognese in one recipe, subclassing the particulars.
the main class is the 'template'
ex: pastDish class penneAlfredo subClass spaghetti subClass
public abstract class PastaDish {
//this is the template method...final keyword= cannot be overridden by subclasses
public final void makeRecipe() {
boilWater();
addPasta();
cookPasta();
drainAndPlate();
addSauce();
addProtein();
addGarnish();
}
//this is left up to subclasses
protected abstract void addPasta();
protected abstract void addSauce();
protected abstract void addProtein();
protected abstract void addGarnish();
private void boilWater() {
//code
}
}
public PenneAlfredo extends PastaDish {
protected addPasta {
//code
}
protected addSauce {
//code
}
protected addProtein {
//code
}
protected addGarnish {
//code
}
}
template method = helpful if you have 2 classes w/ similar functionality
- you visit a dr with a weird problem, they dunno so refer you to a specialist but they dont know so they refer you to another one that actually deals w your problem.
request --> handler --> handler --> handler ^ passing of request continues until one of the objects can fix it.
think of a chain of try/catch blocks...
in software:
click --> handler super class ^ ConcreteHandler1(), ConcreteHandler2()
note: this reminds me of the current shorten_workflow.ts function that sucks.
basic idea:
- check if rule matches
- if so, do something
- if not, call the next function in the list.
theres problems with this pattern...what happens if filter1 doesnt call the next filter?
intent = avoid coupling the sender to the reciever by giving more than one object to handle the request.
- "right now..dance" - the type of dance you do is dependent on what state your body is in (standing/sitting/laying down)
state pattern = an object does stuff based on its current state
this reminds me very much of a react component..do things dependent on current state.
responsibility is given to state, do things based on state ex: interface State
- insertDollar()
- ejectMoney()
- dispense()
state classes (use State interface)
IdleState
- insertDollar()
- ejectMoney()
- dispense() HasOneDollarState
- insertDollar() - give back 'already has one dollar'
- ejectMoney() - give back $ and set state as idle
- dispense() - check stock of item, if theres more than one, dispense item and set vending machine to idle, otherwise dispense item and set item as out of stock OutOfStockState
- insertDollar()
- ejectMoney()
- dispense()
to do all this, we'll have a VendingMachine class
public class VendingMachine {
private State idleState;
private State hasOneDollarState;
private State outOfStockState;
private State currentState;
private int count;
public VendingMachine (int count) {
//make states
idleState = new State();
hasOneDollarState = new hasOneDollarState();
outOfStockState = new OutOfStockState();
if (count > 0) {
currentState = idleState;
} else {
currentState = outOfStockState;
}
//notice how clean this methods look...no conditionals for the diff states
insertDollar() {
currentState.insertDollar(this);
}
ejectMoney() {
currentState.ejectMoney(this);
}
dispense() {
currentState.dispense(this);
}
}
}
the structure goes like this context object (i.e. VendingMachine)
- state.handle() (i.e. 'dispense()')
State Interface
- handle();
ConcreteStateA class
- handle() ConcreteStateB class
- handle()
state pattern is useful when you want to change what a method does depending on changings to an object's internal state
a commander object puts an intermediary 'command object' between the sender and the receiver to decouple the sender from the reciever....the sender doesnt need to know about how the object actually fulfills the request or by who, it just needs to send something. the command object routes it to the right place
command object has an 'invoker' function that invokes the the receiever object
ex: boss (Sender) needs something done, encapsulates what to do in memos, secretary (Command object) takes these and invokes them by routing them to the workers (receivers)
pattern purposes
- store and schedule diff requests
- store requests into lists
- manipulate them before they are completed (ex: parsers!)
- put them on a queue
- allowing commands to be undone or redone
- ex: text editor...you can have a history list and a redo list
- history list = a list of commands that have been done
- redo list = a list of commands that have been undone ^ undo a command? look at 'history' list, take most recent command and tell it to undo itself, then putting on the 'redo' list
benefits
- the commander intermediatary allows the ability to manipulate commands when theyre given (within commander)
- the commander intermediatary allows the ability to put into undo/redo queues to quickly be able to undo/redo commands
- decouples sender and receiever from each other, theydont need to know about each other. this allows you also to remove application logic from your UI...ex BitlyServices.ts and calling endpoints...
command pattern decouples the 'what happens' from the 'who does it'
allows all request to run through a mediator. ex: think of the game object and player1 and player2 objects, with a mediator, you can add as many players as you want because the logic and routing lives within the mediator object itself.
- you have mediator and colleague abstract superClasses, then with concrete mediator and concrete colleague(s) subclasses
a user checks a bunch of dropdowns and boxes, and things have to change based on that...instead of those box/dropdown components talking to all those things directly, we have a mediator that manages those diff interactions.
mediator allows loose coupling between colleagues. since all colleagues only speak to the mediator, you can add/subtract as many as you want.
problem = mediator objects can get pretty huge. the benefits of this centralization must be weighed with the downsides of having a large ass, difficult to read object.
you subscribe to a blog, when theres new content, it notifies you.
blog gets a list of observers
- have a subject superclass that has 3 methods:
- allow a new observer to subscribe
- allow a current observer to unsubscribe
- notify all observers about new blog post
- also contains an attribute to keep track of all the observers
- contains an observer interface so observer that an observer can be notified to update itself
- blog class - a subclass of subject superclass
- subscriber class -impelements the observer interface
blog object
- notify() fn
- update() fn upon notification
subscriber object
- subscribe() fn to subscribe to blog
- getState() fn to get current state of blog upon update()
- unsubscribe() fn
observer pattern is great when you have many objects that rely on the state of one
makes this easy to distribute messages to different parts of a system (or diff systems themselves).
if a class S is a subtype of class B, then S can be used to replace all instances of B without changing the behaviors of the program.
if S is a subtype of B, its expected that S has all the same behaviors as B, so it could be useful in place of it without affecting software.
classes should be open to extension but closed to change.
closed = all attrs and behaviors are encapsulated and the class is stable in your system.
this doesnt mean that you can change the class, but just once you've finalized it and finished development, you can close it.
how do you open it though?
- inheritance of a superclass...so subclasses get everything from closed superclass
- class is abstract and enforces the open/closed principle via polymorphism.
this principle is used to keep the stable parts of your system away from the varying parts. while you wanna be able to add more features to your system, you dont wanna do it at the expense of disrupting something that works. by using extension over change, you can work on the varying parts without introducing unwanted side effects.
isolate the stuff that changes from the stuff that does not.
dependency = coupling
high level modules should depend on high level generalizations and not low level dertails
- client classes should depend on an interface of abstract class instead of referring to concrete resources.
- concrete resources shuold have behaviors generalized into an interface or abstract class
is this....coding to an interface?
interfaces and abstract classes = high level resource concrete resources = low level resource
interface/abstract class defines a set of behaviors, concrete classes provide the implementation of the behaviors....
its almost like high level code should deal with the interfaces rather than the details on how it is implemented?
^ all design patterns in this course are based on this principle
ex: bad example
- --> = depends on
client subsystem --> enterprise subsystem --> backend subsytem ^ low level dependency
ex:
public class ClientSubsystem {
public QuickSorting enterpriseSorting;
public void sortInput(List customerList) {
this.enterpriseSorting.quickSort(customerList);
}
}
^ now what if we change this sort to a mergeSort? then we have to change this class and its method...this could easily grow out of hand depending on the size of our subsystem and its side effects. what if we just had a generalized 'sorting' interface that we could code to instead?
generalize low-level functionality to interfaces.
good example client subsystem --> IEnterpriseSubsystem interface ... then Actual EnterpriseSubsystem --> IBackendSubsystem interface... which then has backendSubSystem
^ this way we can decouple the subsystems by generalizing their interactions via interfaces, allowing us to switch stuff out easier.
ex:
public class ClientSubsystem {
public Sorting enterpriseSorting;
public ClientSubsystem(Sorting concreteSortingClass) {
this.enterpriseSorting = concreteSorting;
}
public void sortInput(List customerList) {
this.enterpriseSorting.sort(customerList);
}
}
"dependency inversion principle" improves software systems so client classes become dependent on high level generalized rathen than low level concrete classes
TL;DR code to the idea/interface of something, rather than the specific something, that way those specific somethings can be switched out.
this principle reminds me of bitlyServices...the client hits the action which calls the endpoint via service level fn rather than the client hitting the endpoint directly.
how to reduce coupling?
indirection =
- generalization
- abstraction
- polymorphism
we wanna get a high amount of code reuse without all the problems of using inheritance.
composing objects principle = classes should achieve code reuse through aggregation rather than inheritance
big time react stuff (decorator design pattern)
"favor composition over inheritance"
look into this more
composing objects principle
- provide code reuse without using inheritance
- allow objects to dynamically add behaviors at runtime
- provide system w more flexibility
generalization of concrete classes via interfaces to increase decoupling and allow to change to occur more
if an interface contains too much tho?
interface segregation principle = a class shouldnt be forced to depend on methods it doesnt use...so if youve got a class that implements an interface with 5 methods but uses only 3, it would have to create 'dummy' fns for those remaining 2. this principle says,"hey instead of doing that, why dont you generalize and split up the interface itself?"
-
too many comments - may get out of sync, think of it as deodorant for bad smelling code
-
duplicated code - blocks of similar, but slightly different blocks of code
ex: copy/pasting similar code blocks to diff areas as opposed to having one central location code block lives and letting diff areas hit that location...
if you have the code in one location, updates become very easy.
-
large methods - maybe theres more in that method than it should be, maybe its too complex and should be broken up?
-
large classes
-
data clumps = groups of data appearing together in the isntance of variables of a class or params to methods
instead of singular params, just make an object of those values
- long parameter lists
- divergent change = you have to change a lot of different things within a class for many different reasons...this is poor separation of concerns...class should only have one specific purpose, reducing the amount of changes needed.
if you find that you're having to make tons of changes across a class/component, that could be an indicator that the class/component should be broken up into separate classes/components
- "shotgun surgery" - when you have to make a bunch of changes across diff parts of your system to do one thing.
**a change in one place requires you to fix various other areas in the code as a result...**ex: adding a feature, fixing bugs, changing algos, etc.
^ you can normally resolve this by moving methods around, or consolidating them into 1 class (if it makes sense to).
- feature envy - theres a method thats more interested in the details of a class than the one that its in
^ if this is the case, just move that method within the other class?
- inappropriate intimacy - when 2 classes talk to each other too much
^ factor out methods that both methods use into one class
-
primitive obsession - reliance on these types too much...they should occur at the lowest level of your code, higher up you should have suitable types/classes for your data...more types = better abstractions
-
switch statements - big ass conditionals...break the actual things within the conditionals within their own objects/interfaces
-
speculative generality - aka "we might need this one day"...coding to something you might need later but not now, over-engineering
-
refused bequest - you inherit a bunch of stuff from a superclass that you dont need.
code smells 1. SectionsPagerAdapter.java
public Fragment getItem(int position) {
switch (position) {
case 0:
AllItemsFragment all_items_fragment = new AllItemsFragment();
return all_items_fragment;
case 1:
AvailableItemsFragment available_items_fragment = new AvailableItemsFragment();
return available_items_fragment;
case 2:
BorrowedItemsFragment borrowed_items_fragment = new BorrowedItemsFragment();
return borrowed_items_fragment;
default:
return null;
}
}
getItem
within SectionsPagerAdapter.java
is contains a switch statement, which is a code smell as switch statements can be hard to reason about and can grow out of hand over time. To fix this, we can place the code within the cases as an object, either returning the item fragment or null if 'position' parameter provided does not exist as a key within the object
Object obj = {
0: () {
AllItemsFragment all_items_fragment = new AllItemsFragment();
return all_items_fragment;
},
1: () {
AvailableItemsFragment available_items_fragment = new AvailableItemsFragment();
return available_items_fragment;
},
2: () {
BorrowedItemsFragment borrowed_items_fragment = new BorrowedItemsFragment();
return borrowed_items_fragment;
}
}
public Fragment getItem(int position) {
obj[position]() || return null;
}
public class Dimensions {
private String length;
private String width;
private String height;
public Dimensions(String length, String width, String height) {
this.length = length;
this.width = width;
this.height = height;
}
public String getLength() {
return length;
}
public String getWidth() {
return width;
}
public String getHeight() {
return height;
}
}
Dimensions.java is a code smell is it is a data class that just has an implicit setter within instantiation and getters for length, width, and height. This data class is only instantiated and used within Item.java, so why not combine the Dimensions class within Item.java as a variable? Additionally, having Dimensions as its own class when only being used in one place is an example of the "speculative generality" code smell (ex: "lets break this out into its own class because we might need to eventually").