mardi 10 novembre 2020

OOP - design decisions

I have following requirements:

I have few vehicles (implementing SingleVehicle) and those can be SingleCar, SingleMotorcycle and SingleTruck. There can be only single instance of a vehicle at a given time. CarPlant is producing vehicles, and saves those in producedVehicles map. Special type of vehicle is SingleTruck, since it has to have loaded at least one car or motorcycle. If SingleMotorcycle is produced, then it will load that one - otherwise it will take produced SingleCar and load that one. CarPlant implements observable interface in order to notify truck in case that SingleMotorcycle is produced - so truck can load that one. SingleTruck implements NotifiableVehicle in order to be able to receive notification from CarPlant.

Here is the code:

class Program{
    public static void main(String[] args) {
        CarPlant plant = new CarPlant();
        plant.produce(SingleVehicle.Name.SINGLE_CAR);
        plant.drive(SingleVehicle.Name.SINGLE_CAR,123);
        plant.produce(SingleVehicle.Name.SINGLE_TRUCK);
        plant.drive(SingleVehicle.Name.SINGLE_TRUCK,456);
        plant.produce(SingleVehicle.Name.SINGLE_MOTORCYCLE);
        plant.drive(SingleVehicle.Name.SINGLE_TRUCK,789);
    }
}

class CarPlant implements Observable{

    SingleVehicle lastAdded;
    Set<NotifiableVehicle> observers = new HashSet<>();
    NavigableMap<SingleVehicle.Name, SingleVehicle> producedVehicles = new TreeMap<>();
    VehicleFactory factory = name -> switch (name) {
        case SINGLE_CAR -> new SingleCar();
        case SINGLE_MOTORCYCLE -> new SingleMotorcycle();
        case SINGLE_TRUCK -> {
            //should this logic be here, since it is accessing outer producedVehicles?
            SingleTruck truck;
            if (producedVehicles.containsKey(SingleVehicle.Name.SINGLE_MOTORCYCLE)) {
                truck = new SingleTruck(producedVehicles.get(SingleVehicle.Name.SINGLE_MOTORCYCLE), CarPlant.this);
            } else {
                truck = new SingleTruck(producedVehicles.get(SingleVehicle.Name.SINGLE_CAR), CarPlant.this);
            }
            yield truck;
        }
    };

    void produce(SingleVehicle.Name name){
        lastAdded = factory.getVehicle(name);
        producedVehicles.put(name, lastAdded);
        //notify observers that something changed
        //should I always notify, or should I have here filter -> if motorcycle or car then notify?
        observers.forEach(o -> o.notifyMe());
    }

    void drive(SingleVehicle.Name name, Integer speed){
        producedVehicles.get(name).drive(speed);
    }

    @Override
    public SingleVehicle getLastAdded() {
        return lastAdded;
    }

    @Override
    public void addObserver(NotifiableVehicle notifiable) {
        observers.add(notifiable);
    }
}

interface VehicleFactory{
    SingleVehicle getVehicle(SingleVehicle.Name name);
}

interface Drivable{
    void drive(Integer speed);
}

abstract class SingleVehicle implements Drivable{

    enum Name{
        SINGLE_CAR,
        SINGLE_MOTORCYCLE,
        SINGLE_TRUCK
    }

    @Override
    public void drive(Integer speed) {
        System.out.println("Drive " + this.getClass().getName() + " at speed " + speed);
    }
}

interface NotifiableVehicle{
    void notifyMe();
}
interface Observable{
    SingleVehicle getLastAdded();
    void addObserver(NotifiableVehicle notifiable);
}

class SingleCar extends SingleVehicle{}
class SingleMotorcycle extends SingleVehicle{}
class SingleTruck extends SingleVehicle implements NotifiableVehicle {
    SingleVehicle loadedVehicle;
    Observable observable;
    SingleTruck(SingleVehicle toCarry, Observable observable){
        loadedVehicle = toCarry;
        this.observable = observable;
        //should I register observer here? I need reference anyway
        //but I think it is bad that "this" escapes before constructor is done?
        observable.addObserver(this);
    }

    @Override
    public void drive(Integer speed) {
        super.drive(speed);
        System.out.println("    ----> but having inside " + loadedVehicle.getClass().getName());
    }

    @Override
    public void notifyMe() {
        if(observable.getLastAdded() instanceof SingleCar || observable.getLastAdded() instanceof SingleMotorcycle) {
            loadedVehicle = observable.getLastAdded();
        }
    }
}

Questions are:

  1. I saw that using static factory, it is implemented using Strings. Why is this the case, and what is advantage of this over using enums as in VehicleFactory factory = name -> switch (name) {....
  2. Is there any other best-practice, than using SingleVehicle.Name.SINGLE_CAR and class SingleCar? Seems that I always have to create enum for each vehicle class that I create. What comes up to my mind is using Strings, but then I loose type safety. Or using Class<? extends SingleVehicle> - but not sure how much is this user friendly.
  3. Should the factory (VehicleFactory factory) has a reference to outer producedVehicles, in order to decide which argument to pass to SingleTruck when initializing it?
  4. Should observer be registered inside SingleTruck (as is now), or inside VehicleFactory factory (after creation)?
  5. Should I create that enum Name inside SingleVehicle, since SingleVehicle is more abstract, and whenever I add new Vehicle, I would need to modify that enum and recompile SingleVehicle without any reason.

Aucun commentaire:

Enregistrer un commentaire