mercredi 15 juillet 2015

Encapsulation between view and controller

I'm working on a school project where we need to create a basic application that manages tasks and projects. We follow the MVC-pattern for our project. The focus lays on the design of the application and our team has been struggling with a certain design issue:

Encapsulation of data passed between view and controller

This means that we want to make sure that the view does not have any references to the real data. We tried to fix this by creating value classes, but it's a huge workaround. These are final classes that essentially are copies of the normal model classes. For example if you have a class Project then you will also have a final class called ProjectValue with all the same fields as Project except that they are all final to make the objects of the value class immutable, so nothing can be changed in the view. It just doesn't feel right to kind of duplicate all these classes to get some extra encapsulation, there must be an easier way.

I'll try to explain the problem with an example:

A user wants to see all projects. Therefore he will start the application and click on a button labeled "Show projects". The button will initiate a method in the controller called getAll():

public PList<ProjectValue> getAll()
{
    PList<ProjectValue> projects = PList.empty();

    for (BranchOffice office : company.getBranchOffices())
    {
        for (Project project : office.getProjects())
        {
            projects = projects.plus(project.getValue());
        }
    }

    return projects;
}

First it loops over all the branch offices. For every project in a branch office it takes the value object of the project (project.getValue()) and puts that in the list instead of the normal project.

An example of a model class and his inner value class:

public class Resource implements Serializable, Comparable<Resource> {

/**
 * Variable registering the name for this resource.
 */
private String            name;

private BranchOffice office;

/**
 * Variable registering the resource type for this resource.
 */
private ResourceType      type;

/**
 * Variable registering the reservations that reserve this resource.
 */
private Set<Reservation> reservations;


/**
 * Initializes the resource with a given name and type.
 * 
 * @param  name
 *         The name for the resource, f.e. Audi
 * @param  type
 *         The type of the resource, f.e. Car
 * @throws InvalidResourceException 
 */
public Resource(String name, BranchOffice office,  ResourceType type)
        throws InvalidResourceException
{
    try
    {
        setName(name);
        setBranchOffice(office);
        setType(type);
        setReservations(null);
    } catch (InvalidRequiredStringException
            | InvalidRequiredResourceTypeException e)
    {
        throw new InvalidResourceException(e.getMessage(), this);
    }
}

/**
 * @return the key
 */
public String getKey() { return name; }
/**
 * @return the type
 */
private ResourceType getType() { return type; }
private String getName() { return name; }
public Set<Reservation> getReservations() { return reservations; }

public BranchOffice getBranchOffice()
{
    return office;
}

/**
 * @param name the name to set
 * @throws InvalidRequiredStringException 
 */
private void setName(String name) throws InvalidRequiredStringException
{
    if (name != null && !name.trim().isEmpty())
        this.name = name;
    else
        throw new InvalidRequiredStringException(INVALID_NAME, name);
}

private void setBranchOffice(BranchOffice office)
{
    if (office == null) {
        throw new IllegalArgumentException(INVALID_OFFICE);
    } else {
        this.office = office;
    }

}

/**
 * @param type the type to set
 * @throws InvalidRequiredResourceTypeException 
 */
private void setType(ResourceType type)
        throws InvalidRequiredResourceTypeException
{
    if (type == null)
        throw new InvalidRequiredResourceTypeException(INVALID_TYPE, type);
    else
        this.type = type;
}

/**
 * Set the list of reservations to a given list.
 * 
 * @param reservations
 *        | The list you want to set the reservations to.
 */
private void setReservations(Set<Reservation> reservations)
{
    if (reservations != null) this.reservations = new HashSet<>(reservations);
    else this.reservations = new HashSet<>();
}

/**
 * Adds a given reservation to the list of reservations.
 * 
 * @param reservation
 *        | The reservation you want to add  to the reservations.
 */
private void addReservation(Reservation reservation)
{
    this.reservations.add(reservation);
}

/**
 * Checks if this resource conflicts with a given resource.
 * 
 * @param resource
 *        The resource you want to check against.
 * @return
 *        True if this resource conflicts with the given resource.
 */
public boolean conflictsWith(Resource resource)
{
    if (getType().hasConflictWith(resource.getType())) return true;
    else return false;
}

/**
 * Checks if a resource if available for a given timespan
 * 
 * @param  timespan
 * @return True if the timespans do not overlap.
 */
public boolean isAvailable(TimeSpan timespan)
{
    if (reservations != null && !reservations.isEmpty())
    {
        for (Reservation reservation : reservations)
            if (reservation.overlapsWith(timespan))
                return false;
        // TODO: checken of resource beschikbaar is binnen timespan (bv.
        // datacenter enkel beschikbaar tussen 12:00 en 17:00
        return true;
    }
    return true;
}


@Override
public int hashCode()
{
    final int prime = 31;
    int result = 1;
    result = prime * result + ((name == null) ? 0 : name.hashCode());
    return result;
}

@Override
public boolean equals(Object obj)
{
    if (this == obj)
        return true;
    if (obj == null)
        return false;
    if (getClass() != obj.getClass())
        return false;
    Resource other = (Resource) obj;
    if (name == null)
    {
        if (other.name != null)
            return false;
    } else if (!name.equals(other.name))
        return false;
    return true;
}

public ResourceValue getValue()
{
    return new ResourceValue(this);
}

@Override
public int compareTo(Resource o)
{
    return this.getKey().compareTo(o.getKey());
}

public boolean isOfType(ResourceType other)
{
    return getType().equals(other);
}

public void reserve(Reservation newReservation) throws InvalidReservationException
{
    for(Reservation reservation : getReservations())
        if(reservation.conflictsWith(newReservation)) 
            throw new InvalidReservationException("Reservation conflicts with another reservation", newReservation);
    addReservation(newReservation);
}

public boolean isOfSameType(Resource resource)
{
    return isOfType(resource.getType());
}

public class ResourceValue
{
    private final String name;
    private final ResourceType type;

    private ResourceValue(Resource resource)
    {
        this.name = resource.getName();
        this.type = resource.getType();
    }
    /**
     * @return the name
     */
    public String getName() { return name; }
    /**
     * @return the type
     */
    public ResourceType getType() { return type; }
}

public void deleteReservation(Reservation reservation)
{
    getReservations().remove(reservation);
}
}

I've copied the whole class, it looks a bit messy but try to look at the bottom of the class, there you can find the value class. I picked this class because it's the smallest one. In this example the value class doesn't copy all the fields but just the ones that are needed for the view.

My question is: "Is there any simpler way of keeping encapsulation between the view and the controller?"

Aucun commentaire:

Enregistrer un commentaire