Type Design Checklist, General

2.1.1. Ensure each type has a single, well-defined purpose

Details coming soon…

2.1.2. Ensure types represent domain concepts, not design abstractions

Details coming soon…

2.1.3. Limit the number of types

Rationale:

Simplicity. Each additional type increases the size of APIs, making them harder to learn and remember. The conceptual complexity of use cases and scenarios also increases when more types are used. The overall quality of APIs suffers, unless the concrete, measurable benefits added by the type compensate for the increased size and complexity. Everything else being equal, fewer types are better.

Do this:

package com.company.product.api;

//This class requires complex configuration and initialization
public class ApiClass { 

    //Default constructor
    public ApiClass() {}

    //Setters and getters
    public void setOption1(Object option1) {}
    public void setOption2(Object option2) {}

    //Initialize after correctly configured
    public void initialize() throws Exception {}
}

Don’t do this:

package com.company.product.api;

//This class requires complex configuration and initialization
public class ApiClass {}
public class ApiClassBuilder {}
public class ApiClassBuilderFactory {}

Don’t do this:

package com.company.product.api;

public class ValueHolder {
    public Object getValue() {}
    public void setValue(Object value) {}
}

public class ApiClass {

    /**
        Does stuff.
        @param result Contains the result of the operation
        @return True if success, False otherwise
    */
    public boolean doStuff(ValueHolder result) {}
}

Don’t do this:

package com.company.product.api;

public class Id {

     public Id(String id) {}
}

public class ApiClass {

    public Object lookupById(Id id) {}
}

2.1.4. Limit the size of types

Details coming soon…

2.1.5. Follow consistent design patterns when designing related types

Details coming soon…

2.1.6. Favor multiple (private) implementations over multiple public types

Details coming soon…

2.1.7. Favor interfaces over class inheritance for expressing simple commonality in behavior

Rationale:

Simplicity and Evolution. You can significantly simplify APIs by capturing common behavior to allow for uniform processing across different types. Commonalities are expressed in Java using either interfaces or class inheritance. Interfaces and class inheritance have different advantages and disadvantages and it is very important to know when to use which.

Abstract superclasses provide more powerful support for expressing commonalities and we talk about these in item 2.1.8. Unfortunately, Java limits you a single superclass per type, which prevents you from capturing several commonalities relating to different aspects in a complex API. Even if you don’t need to capture multiple aspects of common behavior in the first relese of the API, such requirements may appear in successive API releases. Adding new interfaces to existing classes is relatively easy. Modifying the class inheritance hierarchy without breaking backward compatibility can be quite difficult. It is this potential need to express several new comonalities in the future which makes interfaces a better choice for expressing simple commonalities.

2.1.8. Favor abstract classes over interfaces for decoupling API from implementation

Rationale:

Safety and Evolution. Abstract classes can be used everywhere Java interfaces can with the limitation that a class can only extend a single (abstract) superclass while it can implement multiple interfaces. In most APIs Java’s lack of support for multiple class inheritance is not an issue because the interfaces are not intended to be implemented by clients. In these cases abstract classes are the better design choice.

In Java you cannot prevent clients from implementing public interfaces. When you use an interface for a method parameter type, the compiler only guarantees that it implements the correct method signatures; you can never be sure it also implements the correct behavior. You are left with only two choices: either you perform extensive runtime checks or blindly trust the implementation, risking serious consequences, such as data corruption. Unlike interfaces, abstract classes can have code to enforce constraints on behavior. For example, you can prevent clients from extending abstract classes, check preconditions, verify postconditions, and guarantee invariants as illustrated by the examples bellow. You cannot do any of these with interfaces.

Do this:

public abstract class ApiClass {

    //Package scope constructor prevents clients
    //from extending this class
    ApiClass() {}
}

Do this:

public abstract class ApiClass {

    //This public method guarantees preconditions and postconditions
    //for any abstract method implementation
    public final Collection find(String query) {
        if(query == null) {
            throw new NullPointerException("Null query parameter");
        }
        if(query.isEmpty()) {
            throw new IllegalArgumentException("Empty query parameter");
        }
        Collection result = findImplementation(query);
        if(result == null) {
            result = new ArrayList();
        }
        return result;
    }

    protected abstract Collection findImplementation(String query);
}

Do this:

public abstract class ApiClass {

    /*
    * Garanteed invariant:
    *   value != null &&
    *   value != this.lastModified &&
    *   value.before(new Date()) &&
    *   oldValue.before(newValue) &&
    *   value unchanged => class unchanged
    */
    public final Date getLastModified() {return (Date)lastModified.clone();}

    public final void anyMutatorMethod(Param param) {
        anyMutatorMethodImplementation(param);
        updateLastModified();
    } 

    protected final void updateLastModified() {
        Date newValue = new Date();
        if(lastModified.before(newValue)) {
            lastModified = newValue;
        }
    }

    protected abstract void anyMutatorMethodImplementation(Param param); 

    private Date lastModified = new Date();
}

Exceptions:

You should use abstract classes only when they are not intended for client extension. You may provide abstract classes for client extension, but you should not force clients to extend them, since Java does not support multiple inheritance. Use Java interfaces instead and provide abstract classes which implement them for convenience. You can see this approach used in the Java collections library, which contains pairs like Collection and AbstractCollection, List and AbstractList, and so on. Clients are encouraged to extend the abstract classes and even when they decide to implement the interfaces directly, they can use the provided abstract classes as guidance for a correct implementation.

For simple interfaces, such as callbacks, there is no need to provide corresponding abstract classes.

Of course, whenever possible, concrete classes are preferable to abstract classes.

2.1.9. Favor enumeration types over constants

Details coming soon…

2.1.10. Consider generic types

Rationale:

Simplicity and Safety. Generic types are safer and easier to use than types which require casts in client code. In addition to increased complexity, casts may throw ClassCastException at runtime, introducing an additional failure mode. When you design new API types, try to make sure that they can be used without such casts. When the main difference between API types is the type of data they operate on, it may be possible to replace them with a single, generic type. For safety, consider restricting the permissible value of the type parameter of the generic parameter.

Exceptions:

Because generics are implemented by type erasure in Java, there are some restrictions on what you can do with a type parameter within a generic type. For example, it is not possible to create a new instance of the type indicated by the generic type parameter because this type is unknown to the Java virtual machine at runtime:

public class NaiveGenericFactory<T> {

    public T createInstance() {
        return new T(); //Compiler error: Type parameter T cannot be instantiated directly
   }
}

As a rule, only methods known at compile time (methods implemented by all possible type parameter values) can be called from within a generic type. This is one more reason why restricting the generic type parameter is useful. If the type parameter is not restricted, only the methods defined for all reference types can be used.

2.1.11. Consider placing constraints on the generic type parameter

Details coming soon…

2.1.12. Consider using interfaces to achieve similar effect to multiple inheritance

Details coming soon…

2.1.13. Avoid designing for client extension

Details coming soon…

2.1.14. Avoid deep inheritance hierarchies

Details coming soon…

2.1.15. Do not use public nested types

Details coming soon…

2.1.16. Do not declare public or protected fields

Details coming soon…

2.1.17. Do not expose implementation inheritance to the client

Details coming soon…

Creative Commons Licence
This work is licensed under a Creative Commons Attribution-ShareAlike 2.5 Canada License.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: