Objects and Data Structures

Data Abstraction

We do not want to expose the details of our data. Rather we want to express our data in abstract terms.

Hiding implementation is not just a matter of putting a layer of functions between the variables. Hiding implementation is about abstractions! A class does not simply push its variables out through getters and setters. Rather it exposes abstract interfaces that allow its users to manipulate the essence of the data, without having to know its implementation.

public class Point {
     public double x;
     public double y;
}
public class Point {
     private double x;
     private double y;
     
     public double getX() {
          return x;
     }
     
     public double getY() {
          return y;
     }
     
     public void setCartesian(double x, double y) {
          this.x = x;
          this.y = y;
     }
}

Data/Object Anti-Symmetry

Data structures expose data and have no significant behavior. This makes it easy to add new functions to existing data structures but makes it hard to add new data structures to existing functions because all the functions must change.

Objects expose behavior and hide data. This makes it easy to add new kinds of objects without changing existing behaviors. It also makes it hard to add new behaviors to existing objects because all the classes must change.

In any given system, we will sometimes want the flexibility to add new data types, and so we prefer objects for that part of the system. Other times, we will want the flexibility to add new behaviors, and so in that part of the system, we prefer data types and procedures.

Example of Data structure:

public class Square {
    private double side;
}

public class Rectangle {
    private double height;
    private double width;
}

public class App {

    public void draw(Object shape) throws NoSuchShapeException {
        if (shape instanceof Square) {
            log.info("Draw Square with side=" + s.getSide());
        } else if (shape instanceof Rectangle) {
            Rectangle r = (Rectangle) shape;
            log.info("Draw Rectangle with with=" + r.getWidth() + " and height=" + r.getHeight());
        }
        
        throw new NoSuchShapeException();
    }

    // Easy to add new functions
    public void drawV2(Object shape) {
        if (shape instanceof Square) {
            log.info("DrawV2 Square with side=" + s.getSide());
        } else if (shape instanceof Rectangle) {
            Rectangle r = (Rectangle) shape;
            log.info("DrawV2 Rectangle with with=" + r.getWidth() + " and height=" + r.getHeight());
        }
        
        throw new NoSuchShapeException();
    }
    
    public void drawV3(Object shape) {
        log.info("DrawV3 shape=" + shape.getClass().getSimpleName());
    }
    
    public double area(Object shape) {
        // ...
    }
}

It's hard to add a new data structure like Circle, because must change all functions like draw() and drawV2() to have the Circle data structure.

Example of Object:

public interface Shape {
    void draw();
}

public class Square implements Shape {
    private double side;

    @Override
    public void draw() {
        log.info("Draw Square with side=" + side);
    }
}

public class Rectangle implements Shape 
    private double height;
    private double width;
    
    @Override
    public void draw() {
        log.info("Draw Rectangle with with=" + width + " and height=" + height);
    }
}

// Easy to add new data types 
public class Circle implements Shape { 
    private double radius;
    
    @Override
    public void draw() {
        log.info("Draw Circle with radius=" radius);
    }
}

It's hard to add new behaviors to existing objects Shape like adding functionsdrawV2(), because all the classes must change like Square, Rectangle, and Circle.

Avoid Hybrids

Hybrid structures are half object and half data structures.

Hybrids make it hard to add new functions but also make it hard to add new data structures. They are the worst of both worlds.

public abstract class Shape {
    public void draw() throws NoSuchShapeException {
        if (this instanceof Square) {
            log.info("Draw Square with side=" + s.getSide());
        } else if (this instanceof Rectangle) {
            Rectangle r = (Rectangle) shape;
            log.info("Draw Rectangle with with=" + r.getWidth() + " and height=" + r.getHeight());
        }

        throw new NoSuchShapeException();
    }
    
    public void drawV2() {
        if (this instanceof Square) {
            log.info("DrawV2 Square with side=" + s.getSide());
        } else if (this instanceof Rectangle) {
            Rectangle r = (Rectangle) shape;
            log.info("DrawV2 Rectangle with with=" + r.getWidth() + " and height=" + r.getHeight());
        }
        
        throw new NoSuchShapeException();
    }
    
    // Hard to add new functions
    // public abstract double area();
    
}

public class Square extends Shape {
    private double side;
}

public class Rectangle extends Shape {
    private double height;
    private double width;

}

// Hard to add new data types
// public class Circle extends Shape { }

It's hard to add a new data structure like Circle, because must change all functions like draw() and drawV2() to have the Circle data structure.

It's hard to add new behaviors to existing objects Shape like adding functions area(), because all the classes must change like Square, Rectangle.

The Law of Demeter

Refer to LoD Principle

Data Transfer Objects

The quintessential form of a data structure is a class with public variables and no functions. This is sometimes called a data transfer object, or DTO.

DTO is an object that carries data between processes. DTO is a simple object that should not contain any business logic but may contain serialization and deserialization mechanisms for transferring data over the wire.

class User {
    private String firstName;
    private String lastName;
    private String email;
    private LocalDate birthday;
    private Role role;
    private LocalDateTime createdAt;
    private LocalDateTime updatedAt;
}

public void createUser(String firstName, String lastName, String email, LocalDate birthday, Role role) {
    User user = new User();
    user.setFirstName(firstName);
    user.setLastName(lastName);
    // ...
    user.setCreatedAt(LocalDateTime.now());
    userRepository.save(user);
}
class User {
    private String firstName;
    private String lastName;
    private String email;
    private LocalDate birthday;
    private Role role;
    private LocalDateTime createdAt;
    private LocalDateTime updatedAt;
}

class UserDto {
    private String firstName;
    private String lastName;
    private String email;
    private LocalDate birthday;
    private Role role;
}

public void createUser(UserDto userDto) {
    User user = mapToUser(userDto);
    user.setCreatedAt(LocalDateTime.now());
    userRepository.save(user);
}

Last updated