COMP2511

25T3 Week 4

Tuesday 10am-1pm (T10A)

 

Start 10:05am

Today

  • Assignment Tips
  • Design Principles
    • Law of Demeter
    • Liskov Substitution Principle
  • Design Patterns
    • Composite Pattern
    • Factory Pattern
  • Assignment 1 is due next Wednesday 3pm!

Assignment Tips

Use .equal() for String (or any Class) comparison instead of ==

public class EqualsExample {
    public static void main(String[] args) {
        String s1 = "Hello"; // string literal
        String s2 = new String("Hello"); // string class
        System.out.println(s1 == s2); // false, as its comparing the addresses of s1 and s2
        System.out.println(s1.equals(s2)); // true, compares the values at the addresses
        //                    ^ do this
    }
}

Correctness + style issue

Assignment Tips

Avoid the use of magic numbers, assign them to final variables (readability - style issue)

public class MagicNumbers {
    List<String> files = new ArrayList<>();

    public void doSomething() {
        if (files.size() == 5) { // 5 here is a magic number
                                 // What does it mean?
        }
    }
}
public class MagicNumbers {
    List<String> files = new ArrayList<>();
    private final static int MAX_FILES = 5; 
    // declare constant with good name ^
    public void doSomething() {
        if (files.size() == MagicNumbers.MAX_FILES) {};
        double angle = 750; // ^ use constant here
        angle = angle % 360; // This is more OK, since theres context
    }
}

Assignment Tips

Don't use super.x to set attributes in the super class in the subclass's constructor. Pass variable into super constructor as argument (single responsibility - design issue)

// Example: What not to do
public class Shape {
    private String name;
    private double length;

    public Shape(String name) {
        this.name = name;
        // length is currently unset
    }

    public void setLength(double length) {
        this.length = length;
    }
}

public class Rectangle extends Shape {
    public Rectangle(String name, double length) {
        super(name);
        this.length(length); // bad
        this.length = length; // also bad if not private
    }
}

Assignment Tips

Don't use super.x to set attributes in the super class in the subclass's constructor. Pass variable into super constructor as argument (single responsibility - design issue)

// Example: What to do
public class Shape {
    private String name;
    private double length;

    public Shape(String name, double length) {
        this.name = name;
        this.length = length;
        this.setLength(length); // this is ok as well
    }
}

public class Rectangle extends Shape {
    private String somethingElse;

    public Rectangle(String name, double length, String somethingElse) {
        super(name, length);
        this.somethingElse = somethingElse;
    }
}

Assignment Tips

Use instanceof for type comparison (open/close - design issue)

public abstract class Shape {
    public abstract String getType();
}

public class Rectangle extends Shape {
    public void doSomething(List<Shape> shapes) {
        for (Shape s : shapes) {
         	// v1: kinda ok but overly verbose, does not include subclass
            if (s.getClass().equals(Rectangle.class)) {
                // do something only on exactly the Rectangle class
            }
            // v2: very bad, relies on string which can be incorrectly handled
            if (s.getType().equals("Rectangle")) { 
               // do something on all rectangles
            }
            // v3: good, supports polymorphism + subclasses
            if (s instanceof Rectangle) { 
                // do something on all rectangles
            }
        }
    }
}

public class Square extends Rectangle {}

Assignment Tips

Polymorphism is preferred over typechecking to perform a specific action (open/closed - design issue explored next week)

// Example: What not to do
public abstract class Shape {
    public abstract String getType();
}
public class Rectangle extends Shape {}

public class Square extends Rectangle {
    public static void main(String[] args) {
        List<Shape> shapes = new ArrayList<>();
        shapes.add(new Rectangle());
        shapes.add(new Square());
        for (Shape s : shapes) {
            if (s.getType().equals("Rectangle")) {
                // calculate the area this way
            } else if (s.getType().equals("Square")) {
                // calculate the area a different way
            }
        }
    }
}

Assignment Tips

Polymorphism is preferred over typechecking to perform a specific action (open closed - design issue explored next week)

// Example: What to do
public abstract class Shape {
    public abstract String getType();
    public abstract double area();
    // ^ declare method in superclass
}

public class Rectangle extends Shape {
    public double area() {
        // calculate the area this way
    }
}

public class Square extends Rectangle {
    public double area() {
        // calculate the area a different way
    }
    public static void main(String[] args) {
        List<Shape> shapes = new ArrayList<>();
        shapes.add(new Rectangle());
        shapes.add(new Square());
        for (Shape s : shapes) {
            s.area(); // no more type checking
        }
    }
}

Law of Demeter

"Principle of least knowledge"

Law of Demeter

What is it?

Law of Demeter (aka principle of least knowledge) is a design guideline that says that an object should assume as little as possible knowledge about the structures or properties of other objects.


It aims to achieve loose coupling in code.

Law of Demeter

Cohesion: how closely related individual functions and responsibilities of a class/module are

Coupling: how much a class is reliant on other classes

We want to aim for high cohesion & low coupling

Law of Demeter

What does it actually mean?

A method in an object should only invoke methods of:

  • The object itself
  • The object passed in as a parameter to the method
  • Objects instantiated within the method
  • And not those of objects returned by a method

 

E.g., don't do this

o.get(name).get(thing).remove(node)

*Caveat is that sometimes this is unavoidable

Code Review

Law of Demeter

Code Review

In the  unsw.training  package there is some skeleton code for a training system.

  • Every employee must attend a whole day training seminar run by a qualified trainer
  • Each trainer is running multiple seminars with no more than 10 attendees per seminar

In the TrainingSystem class there is a method to book a seminar for an employee given the dates on which they are available. This method violates the principle of least knowledge (Law of Demeter).

Code Review

In the unsw.training package there is some skeleton code for a training system.

  • Every employee must attend a whole day training seminar run by a qualified trainer
  • Each trainer is running multiple seminars with no more than 10 attendees per seminar

In the TrainingSystem class there is a method to book a seminar for an employee given the dates on which they are available. This method violates the principle of least knowledge (Law of Demeter).

/**
 * An online seminar is a video that can be viewed at any time by employees. A
 * record is kept of which employees have watched the seminar.
 */
public class OnlineSeminar extends Seminar {
    private String videoURL;
    private List<String> watched;
}
/**
 * An in person all day seminar with a maximum of 10 attendees.
 */
public class Seminar {
    private LocalDate start;
    private List<String> attendees;

    public LocalDate getStart() {
        return start;
    }

    public List<String> getAttendees() {
        return attendees;
    }
}
public class TrainingSystem {
    private List<Trainer> trainers;

    public LocalDate bookTraining(String employee, List<LocalDate> availability) {
        for (Trainer trainer : trainers) {
            for (Seminar seminar : trainer.getSeminars()) {
                for (LocalDate available : availability) {
                    if (seminar.getStart().equals(available) &&
                            seminar.getAttendees().size() < 10) {
                        seminar.getAttendees().add(employee);
                        return available;
                    }
                }
            }
        }
        return null;
    }
}
/**
 * A trainer that runs in person seminars.
 */
public class Trainer {
    private String name;
    private String room;
    private List<Seminar> seminars;

    public List<Seminar> getSeminars() {
        return seminars;
    }
}

How and why does it violate this principle?

What other properties of this design are not desirable?

/**
 * An online seminar is a video that can be viewed at any time by employees. A
 * record is kept of which employees have watched the seminar.
 */
public class OnlineSeminar extends Seminar {
    private String videoURL;
    private List<String> watched;
}
/**
 * An in person all day seminar with a maximum of 10 attendees.
 */
public class Seminar {
    private LocalDate start;
    private List<String> attendees;
    public LocalDate getStart() {
        return start;
    }

    /**
     * Try to book this seminar if it occurs on one of the available days and
     * isn't already full
     * @param employee
     * @param availability
     * @return The date of the seminar if booking was successful, null otherwise
     */
    public LocalDate book(String employee, List<LocalDate> availability) {
        for (LocalDate available : availability) {
            if (start.equals(available) &&
                    attendees.size() < 10) {
                attendees.add(employee);
                return available;
            }
        }
        return null;
    }
}
public class TrainingSystem {
    public List<Trainer> trainers;
    /**
     * Try to booking training for an employee, given their availability.
     *
     * @param employee
     * @param availability
     * @return The date of their seminar if booking was successful, null there
     * are no empty slots in seminars on the day they are available.
     */
    public LocalDate bookTraining(String employee, List<LocalDate> availability) {
        for (Trainer trainer : trainers) {
            LocalDate booked = trainer.book(employee, availability);
            if (booked != null)
                return booked;
        }
        return null;
    }
}
/**
 * A trainer that runs in person seminars.
 */
public class Trainer {
    private String name;
    private String room;
    private List<Seminar> seminars;

    public List<Seminar> getSeminars() {
        return seminars;
    }

    /**
     * Try to book one of this trainer's seminars.
     * @param employee
     * @param availability
     * @return The date of the seminar if booking was successful, null if the
     * trainer has no free slots in seminars on the available days.
     */
    public LocalDate book(String employee, List<LocalDate> availability) {
        for (Seminar seminar : seminars) {
            LocalDate booked = seminar.book(employee, availability);
            if (booked != null)
                return booked;
        }
        return null;
    }
}
  1. TrainingSystem no longer has knowledge of Seminar
  2. Each class has their own responsibility (good cohesion)

Liskov Substitution Principle

Liskov Substitution Principle

What is it?

Liskov Substitution Principle (LSP) states that objects of a superclass should be replaceable with objects of its subclasses without breaking the application.

*inheritance arrows are the other way around

Liskov Substitution Principle

Solve the problem without inheritance

  • Delegation - delegate the functionality to another class
  • Composition - reuse behaviour using one or more classes with composition
     

Design principle: Favour composition over inheritance

If you favour composition over inheritance, your software will be more flexible, easier to maintain, extend.

That being said, inheritance can have its place!

Liskov Substitution Principle

/**
 * An online seminar is a video that can be viewed at any time by employees. A
 * record is kept of which employees have watched the seminar.
 */
public class OnlineSeminar extends Seminar {
    private String videoURL;
    private List<String> watched;
}
/**
 * An in person all day seminar with a maximum of 10 attendees.
 */
public class Seminar {
    private LocalDate start;
    private List<String> attendees;

    public LocalDate getStart() {
        return start;
    }

    public List<String> getAttendees() {
        return attendees;
    }
}

Where does OnlineSeminar violate LSP?

OnlineSeminar doesn't require a list of attendees

Design Patterns

  • Design patterns are typical solutions to commonly occurring problems in software design
  • They are not a one-size-fits-all solution, but rather a template/concept to help solve a specific problem
  • The remainder of the course will be going over different design patterns
  • They can be useful to help achieve better design within your code
    • This does not mean you should force it when it doesn't fit!

Questions for thought

  • If you were hanging a painting with nails, would you use a sledgehammer or a normal hammer?
  • If you needed a new chair, would you buy a bunk bed and use the bottom bunk bed as a seat?
  • If you needed to make a quick sandwich for lunch, would you lookup a recipe for a 5 course meal?

Technically, they could work out, but you probably shouldn't.

Design Patterns

  • Three main types of design patterns:
    • Behavioural: concerned with algorithms and the overall behaviour of specific objects
    • Structural: explains how to create and assemble objects into larger structures while keeping them flexible
    • Creational: describes various object creation mechanisms to keep them flexible and reduce repetition

Design Problem/Code Smells

  • Design problems and code smells are not the same
  • Code smells are small issues that help you detect a specific problem exists
  • The actual problem is the design problem at hand
  • For instance, if you smell rotten food - the code smell is the literal smell, but it isn't the problem
  • The problem of course is the rotten food!

Refactoring Guru

  • Extremely useful website for design patterns/code smells
  • When in doubt for differing content, this course is the source of truth

Composite Pattern

Composite Pattern

What type of design pattern is composite?

Structural

Structural design pattern are patterns that ease the design by identifying a simple way to realize relationships among entities.

They explain how to assemble objects and classes into large structures, while keeping structures flexible and efficient

Composite pattern is useful for aggregating different objects/data. The aim is to be able to manipulate a single instance of an object just as you would manipulate a group of them.

Tree like structure of objects

Composite Pattern

  • Component (or Node) - the common interface
  • Leaf - terminal node, just contains data (consider a regular file in your file manager)
  • Composite -  "intermediate", contains more nodes (and can also contain data, like a folder)

Composite Pattern

Code Demo

Calculator.java - Composite pattern

Code Demo

Inside src/calculator, use the Composite Pattern to write a simple calculator that evaluates an expression. Your calculator should be able to:

  • Add two expressions
  • Subtract two expressions
  • Multiply two expressions
  • Divide two expressions

Code Demo

{1 + [2 * (4 + 3)]}

Expression 1

Expression 2

Expression 3

Factory Pattern

Factory Pattern

What type of design pattern?

Creational

Creational patterns provide various object creation mechanisms, which increase flexibility and reuse of existing code.

 

Factory method provides an interface for creating objects in a superclass, but allows subclasses to alter the type of objects that will be created.

Reduces coupling and shotgun surgery, as all classes are created using the same method.

Factory Pattern

There are a few variants of the factory pattern:

  • "Standard" - the one you can find on RefactoringGuru
  • Static - what we will often use/encounter in this course
  • Abstract - a more extensible version

 

Let's get to know them!

Factory Pattern - "Standard"

  1. The Product declares the interface, which is common to all objects that can be produced by the Creator and its subclasses.
  2. Concrete products are different implementations of the product interface
  3. The Creator class declares the factory method and returns new product objects
  4. Concrete Creators override the base factory method so it returns a new type of product

Factory Pattern - "Standard"

interface Vehicle {
    void drive();
}

class Car implements Vehicle {
    public void drive() { System.out.println("Driving a car 🚗"); }
}

class Bike implements Vehicle {
    public void drive() { System.out.println("Riding a bike 🚲"); }
}

class VehicleFactory {
    static Vehicle createVehicle(String type) {
        return switch (type.toLowerCase()) {
            case "car" -> new Car();
            case "bike" -> new Bike();
        };
    }
}

public class FactoryExample {
    public static void main(String[] args) {
        Vehicle vehicle = VehicleFactory.createVehicle("car");
        vehicle.drive(); // Output: Driving a car 🚗
    }
}

Factory Pattern - "Standard"

  • Keeps details hidden away from user - abstraction
  • Keeps object creation in one spot for easy extension and management
  • Avoids giant switch/if statements in client code
  • Particularly useful when you expect lots of changes/additions or creating the object is complex!

Static Factory Pattern

  • Most likely to encounter in this course
  • Only requires one class, unlike the "standard" factory pattern. Each type of object we are trying to create simply has a static method
public class StaticFactory {
	public static A createA() {
    
    }
    
    public static B createB() {
    
    }
    
    public static C createC() {
    
    }
}

Abstract Factory Pattern

  • Abstract factories can be used to create different types of families of objects
  • Consider making a GUI app with a light and dark mode. We might create an abstract factory to provide a common interface for our different factory types
// Abstract Products
interface Button { void render(); }
class LightButton implements Button { public void render() { System.out.println("🟡 Light Button"); } }
class DarkButton implements Button { public void render() { System.out.println("⚫ Dark Button"); } }

// Abstract Factory
interface GUIFactory { Button createButton(); }
class LightFactory implements GUIFactory { public Button createButton() { return new LightButton(); } }
class DarkFactory implements GUIFactory { public Button createButton() { return new DarkButton(); } }

// Client Code
public class AbstractFactoryExample {
    public static void main(String[] args) {
        GUIFactory factory = new DarkFactory(); // Switch to LightFactory for light mode
        factory.createButton().render(); // ⚫ Dark Button
    }
}

Code Demo

Thrones.java - Factory Pattern

Code Demo

Inside src/thrones, there is some code to model a simple chess-like game. In this game different types of characters move around on a grid fighting each other. When one character moves into the square occupied by another they attack that character and inflict damage based on random chance. There are four types of characters:

  • A king can move one square in any direction (including diagonally), and always causes 8 points of damage when attacking.
  • A knight can move like a knight in chess (in an L shape), and has a 1 in 2 chance of inflicting 10 points of damage when attacking.
  • A queen can move to any square in the same column, row or diagonal as she is currently on, and has a 1 in 3 chance of inflicting 12 points of damage or a 2 out of 3 chance of inflicting 6 points of damage.
  • A dragon can only move up, down, left or right, and has a 1 in 6 chance of inflicting 20 points of damage.

Code Demo

We want to refactor the code so that when the characters are created, they are put in a random location in a grid of length 5.

  1. How does the Factory Pattern (AKA Factory Method) allow us to abstract construction of objects, and how will it improve our design with this new requirement?
    • Abstract the construction of the character objects. We don't deal with the constructor, instead call a general factory method that handles the number.
  2. Use the Factory Pattern to create a series of object factories for each of the character types, and change the main method of Game.java to use these factories.

COMP2511 Week 4 25T3

By Sam Zheng

COMP2511 Week 4 25T3

  • 153