Friday, February 21, 2014

Custom Types in Object-Oriented Languages

C++ inherited a preprocesser command from C: typedef. It is a form of abstraction. Think about it: if I can assign a new name to an existing type with this, then I'm encapsulating that type behind an abstraction. If I need to change the definition I don't need to recode all the clients that use it. And in C++ it works with templates!

#typedef CustomerList vector<Customer&>;

The reason C gave us typedef is to simplify the type names used. Simplifying things reduces the mistakes that are made.

Fast forward to Java and C#: they don't have typedef! No, they don't, but I can still make this work with class inheritance. In fact, you should choose inheritance over typedef in C++ too, we'll see why in a moment. Here is a Java class that does nothing more than create a new "CustomerList type" from an ArrayList:

class CustomerList extends ArrayList<Customer> {

So why make this class? Well, one reason is that I'm basically lazy and I just don't want to type ArrayList<Customer> all the time. So the immediate effect is that the clients using this will not have to keep repeating a generic type. It's harder to make dumb mistakes if I just have to type CustomerList.

When I teach any object-oriented programming I always stress design to the abstraction. Sometimes I'll call it "code to the interface." Let's say that we have a class with a method that returns an ArrayList<Customer>. When I store that value I'll save it in a variable of type List<Customer>, because I only need the methods the List interface provides. The method could change and return something other than an ArrayList. But as long as whatever it returns still implements List<Customer> I'll be able to use it without modifying my code. Here's a brief example of what that might work like:

class Customers {

@tab;private ArrayList customers;

@tab;public Customers() { customers = new ArrayList(); }

@tab;public ArrayList getCustomers() { return customers; }


Customers customerManager = new Customers();
List customerList = customerManager.getCustomers();

I can't stress enough that the Customers shouldn't reveal anything more than what the clients need. So if the class uses an ArrayList<Customer> internally, then the getCustomers method really should return List<Customer>. "What you can hide you can change," so if the class hides that it is really an ArrayList it can be changed without affecting any clients. Let's make that change to line seven:

public List<Customer> getCustomers() { return customers; }

But it's still not complete. Whenever a class uses a collection internally a choice has to be made: do we give clients access to the list and the elements or not? If we want to protect the list, then we have another question to answer: where do we put the code to protect the list?

If I put the code in the Customers class then I'm sure to violate the Single Responsibility Principle (Martin). The Customers class already has the responsibility of managing the collection of customers, so the code to protect the list is a second responsibility. We don't want to mix responsibilities in a class if we can avoid it, that breaks cohesion in the class.

So when I created the abstraction CustomerList that happens to be a class that doesn't have any responsibilities. It's the perfect place to put the code to protect the list! We'll simply make Customers return a new CutomerList based on the private ArrayList. To do that it will have a constructor that can accept the ArrayList and duplicate it. Fortunately there is already a constructor in the superclass that implements it for us.

But the new CustomerList still has references to all of the original Customers in the ArrayList. To prevent the original Customer instances from being modified we'll lazily make a clone and return that when a customer is retrieved:

class CustomerList extends ArrayList {

@tab;public CustomerList(Collection c) { super(c); }
@tab;public void add(Customer c) { throw new CustomerListException(); }
@tab;public Customer get(int index) { return super.get(index).clone(); }

class Customers {
@tab;private ArrayList customers;
@tab;public Customers() { customers = new ArrayList(); }
@tab;public CustomerList getCustomers() { return new CustomerList(customers); }

So making the extra class is a balancing act, and here's where I draw the line:
  1. When I'm using the Java API or a third party library I simply use the highest abstraction I can for what the library gives me. I don't create new types to wrap what they use.
  2. When I'm working with my own classes and what I'm returning to a client is getting large (to type) or confusing then I'll create a class as an abstraction between that code and the client. If I create one of these classes then I create the abstractions in every instance to keep my project code symmetrical.
We started out in C++ and finished up in Java, but the concepts that I talked about apply to all class-based object-oriented languages. Some folks will use these ideas, and some won't. It isn't right or wrong to use them or not use them. Whether you do is up to you :)

No comments:

Post a Comment