Software Craftsmanship
Design Patterns
September 20, 2016
Helps developers reuse successful designs by basing new designs on prior experience.
Improves understandability for developers familiar with the pattern.
Identifies participating classes and instances, their roles and collaborators, and the distribution of responsibilities. This helps ensure adherence to Design Principles.
A design pattern is a general reusable solution to a commonly occurring problem within software design. It is a description or template for how to solve a problem that can be used in many different situations.
Common processes or solutions that, although they might seem effective, have been found to be ineffective.
Creational
Structural
Behavioral
Creation of objects for you rather than instantiating objects directly
Composition of classes and objects to obtain new functionality
Communication and interaction between objects
Restricts the number of instantiations of a class to one single instance.
Use when:
Don't use when:
Singletons make your API a liar. You should be able to understand an API without memorizing all of the source code.
testDebitAccount()
{
Account c = new Account("1234123412341234", "360", 5000);
c.debit(200);
assert(c.availableBalance == 4800);
}In the background there is some "stuff" going on:
However, we don't know this is the case just by looking at the API. The API clearly states that you can create an account and then debit it.
testDebitAccount()
{
AccountTransactionQueue.getInstance();
AccountTransactionProcessor.getInstance();
Account c = new Account("1234123412341234", "360", 5000);
c.debit(200);
assert(c.availableBalance == 4800);
}testDebitAccount()
{
AccountTransactionQueue atQueue =
new AccountTransactionQueue();
AccountTransactionProcessor atProcessor =
new AccountTransactionProcessor(atQueue);
Account c = new Account("1234123412341234", "360", 5000);
atProcessor.debit(c, 200);
assert(c.availableBalance == 4800);
}
public class Singleton {
private static Singleton instance = null;
private Singleton() { }
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
Encapsulates object creation logic by delegating to implementations of a common factory interface without specifying the exact class of object to create.
Use when:
Logger logger = LoggerFactory.getLogger(HelloWorld.class);
logger.info("Hello World");CreditCardAccountFactory accountFactory = new CreditCardAccountFactory();
foreach (Transaction transaction : customer.getTransactionHistory())
{
AccountType accountType = transaction.getAccountType();
CreditCardAccount account = accountFactory.AccountFactory(accountType);
totalRewards += account.calculateRewards();
}
Factories are useful for creating "curtains" where systems need to be plug and play or details may not be known at compile time.
The JDBC allows you to interact with various RDBMS without knowing about the specifics of each connector.
At runtime the GetConnection method iterates though all of the drivers loaded into the JVM (information not known at compile time) and returns the driver that is able to create a connection using the provided URL.
Connection conn = DriverManager.getConnection(URL, USER, PASS);https://github.kdc.capitalone.com/kat269/FactoryExercise
Solves the "telescoping constructor" problem by encapsulating construction steps so that they can be invoked individually.
Use when:
sourcemaking.com
Sandwich blt = new Sandwich("BLT", Bread.WHEAT, 3);
blt.addCondiment(Condiment.MAYO);
blt.addMeat(Meat.BACON);
blt.addVegetable(Vegetable.Lettuce);
blt.addVegetable(Vegetable.Tomato);
. . .
public class Sandwich {
// private instance variables
public Sandwich() { }
public Sandwich(String name) {
this();
this.name = name;
}
// telescoping constructors
public Sandwich(String name, Bread bread) {
this(name, bread, bread.minNumSlices);
}
public Sandwich(String name, Bread bread, int numSlices) {
this(name);
this.bread = bread;
this.numBreadSlices = numSlices;
}
public void addCondiment(Condiment meat) { ... }
public void addMeat(Meat meat) { ... }
public void addVegetable(Vegetable veggie) { ... }
}Sandwich blt = new Sandwich.Builder()
.name("BLT")
.withBread(Bread.WHEAT, 3)
.withCondiment(Condiment.MAYO)
.withMeat(Meat.BACON)
.withVegetable(Vegetable.Lettuce)
.withVegetable(Vegetable.Tomato)
.build();
. . .
public class Sandwich {
// private instance variables
public Sandwich() { }
public void addCondiment(Condiment meat) { ... }
public void addMeat(Meat meat) { ... }
public void addVegetable(Vegetable veggie) { ... }
public class Builder {
//private instance variables
public Sandwich build() {
Sandwich s = new Sandwich();
s.setName(this.name);
...
return s;
}
public SandwichBuilder withBread(Bread bread, int numSlices) {
this.bread = bread;
this.numBreadSlices = numSlices;
}
}
}The card must have the following.
The card may have an arbitrary number of incentives/rewards.
The card may have an arbitrary number of fees associated with it.
Hint: Create an incentives and fees base class.
Creates a "bridge" between two incompatible interfaces.
Use when:
Cables are a real-life example of an adapter. They allow us to connect two previously incompatible devices. e.g. connect your laptop to your monitor, speakers to your smartphone, etc.
class LegacySquarePeg {
private double width;
public LegacySquarePeg(double w) { width = w; }
public double getWidth() { return width; }
public void setWidth(double w) { width = w; }
}
class RoundHole {
private int radius;
public RoundHole( int r ) { radius = r; }
public double getRadius() { return radius; }
public int getRadius() { return radius; }
}
class SquarePegAdapter {
private LegacySquarePeg sp;
public SquarePegAdapter(double w) {
sp = new LegacySquarePeg(w);
}
public void makeFit(RoundHole rh) {
double amount = sp.getWidth() - rh.getRadius() * Math.sqrt(2);
if (amount > 0) {
sp.setWidth(sp.getWidth() - amount);
}
}
}DRY - Don't Repeat Yourself
Requirements, understanding, and environments change. Your code will need to change also.
It's not a matter of if you'll remember, but when you'll forget.
A facade object provides a simplified interface to a larger body of code to make a subsystem easier to use.
Use when:
public boolean createNewFile() throws IOException {
SecurityManager security = System.getSecurityManager();
if (security != null) security.checkWrite(path);
return fs.createFileExclusively(path);
}
public boolean delete() {
SecurityManager security = System.getSecurityManager();
if (security != null) {
security.checkDelete(path);
}
return fs.delete(this);
}
public String getName() {
int index = path.lastIndexOf(separatorChar);
if (index < prefixLength) return path.substring(prefixLength);
return path.substring(index + 1);
}
}// a simple facade that masks the various browser-specific methods
function addEvent( element, event, callback ) {
if( window.addEventListener ) {
element.addEventListener( event, callback, false );
}
else if( document.attachEvent ) {
element.attachEvent( 'on' + event, callback );
}
else {
element[ 'on' + event ] = callback;
}
}Encapsulates and decouples the algorithm for traversing a data structure, providing a way to access the elements sequentially without exposing the structure's underlying implementation.
Use when:
The "seek" button on a radio is a real-life example of the iterator pattern. Pressing the seek button is similar to using a ".next()" method that returns the next available radio station.
Uses a command object to represent the information needed to call a method at a later time. Command information includes the method's name, object that owns the method, and values for the method's parameters.
Use when:
sourcemaking.com
interface TransactionExecutor {
void addTransactionCommand(TransactionCommand transaction);
}
interface TransactionCommand {
void execute();
}
class DepositCommand implements TransactionCommand {
Account account;
double amount;
void execute() {
account.addFunds(amount);
}
}
class WithrawalCommand implements TransactionCommand {
Account account;
double amount;
void execute() {
account.removeFunds(amount);
}
}An object (the subject) maintains a list of its dependents and notifies them of any state changes.
Use when
sourcemaking.com
A common example of the observer pattern is the Model-View-Controller (MVC) architecture that is used extensively in web applications.
There are many situations when cases only differ by their behavior. It is a good idea to isolate the algorithms into different classes to be able to be chosen at runtime.
Use when:
Credit
Debit
//Strategy pattern
DamageResistedStrategy dmgStrat = null;
switch (damage.getType()) {
case MAGIC:
dmgStrat = new MagicDamageResistedStrategy(damageAmount, target);
case PHYSICAL:
dmgStrat = new PhysicalDamageResistedStrategy(damageAmount, target);
case PURE:
dmgStrat = new PureDamageResistedStrategy(damageAmount, target);
}
resistedDamageAmount = dmgStrat.calculateResistance();
damageAmount -= resistedDamageAmount;Design Pattern Group Activity