[SalesForce] Abstract, Interface, Classes OH MY – Question on Approach

So I believe that I have a real world use case for an object model that uses abstract classes and interface. I do however have a few questions about the best way to go about it:

Interface

public interface iLove{
   String pet();
}

Abstract

public abstract class Animal{
   public string animalName {get;set;}
   public boolean happy {get;set;}

   public virtual boolean isHappy();
}

Class

public class cat extends Animal implements iLove{

  public cat(String nm){
     animalName = nm;
  }

  public string pet(){
    system.debug(animalName);        
    happy = true;
    return 'prrrrrrr';
  }

  public override boolean isHappy(){
    return happy == null ? false : happy;
  }

}

So, if I instantiate like this:

Animal a = New Cat('Fred');

I do not have access to the method pet() since Animal does not implement pet

Doing this:

iLove i = (iLove)a;

I can now can execute pet()

However, if I instantiate like so:

iLove i = New cat('Fred');

Executing pet() debugs the animalName and there is no access to isHappy() or animalName directly

Now, the goal is to have many Animals that have a common definition (Animal) that may have different interfaces. for this example each one implements their own pet method, Dog – Bark, Lion – Roar, etc.

Is there a better way to handle all of this where I would not have to Instantiate Animal and then cast to iLove

Goal would be one line for instantiation where I would have access to the properties in Animal as well as the methods of the interface without having to do

Cat c = New Cat('Fred');

As obviously that would work but then it cuts out the whole point of the interface….

Is the casting of the SuperClass to the Interface the way to go about it?

Should I just define the abstract methods in the abstract class. If so what is the point of the Interface. I would suspect in this case it may not be needed but could be used if other classes which to interact with the cat without having to worry about the Animal class.

Thoughts?

Putting the interface on the Animal class has similar effects…

Best Answer

tl;dr

We write code using interfaces and abstract classes so that utility methods that deal with one or the other can work with whatever data we feed it so long as it conforms to our specifications, even when that data type may not already exist or even be planned at the time we write the utility methods. It's not a means to just abstractly hide data types as in your example. While the design you've pondered (parent abstract class and child class with interface) is actually practical (and I've used such designs before), it's not about using some parent type to access everything, but a way to write utility methods that can accept various data types, allowing us to reuse algorithms.


Yes, you have to cast when going to a sub-class, or to an interface that only exists on a sub-class. However, you're missing the point entirely: both super-classes and interfaces let us pretend we're working with that type of thing automatically. Let's take your code as a base, and add some functions:

public static String petLovable(iLove lovable) {
    return lovable.pet();
}
public static Boolean checkHappiness(Animal anAnimal) {
    return anAnimal.isHappy();
}

Notice how we use the interface and super-class directly as the data type for each parameter. Now, we can do this:

Cat c = new Cat('Fred');
System.debug(petLovable(c));
System.assert(checkHappiness(c));

No casts are required, because Fred knows he's both lovable (iLove) and an Animal. But wait, there's more:

public Boolean isLovable(Object o) {
    return o instanceOf iLove;
}

That's right. We can immediately tell if something conforms to a specific interface, even if it's stripped down to a mere Object reference. Likewise, Fred will also identify as an Animal and a Cat:

System.assert(c instanceOf Animal);
System.assert(c instanceOf Cat);

This works even if we stuff Fred into an Object reference, or perhaps even some other foreign object:

public Boolean isAnimal(iLove lovable) {
    return lovable instanceOf Animal;
}

Generally speaking, when we write code with polymorphic intentions, our goal is to have a set of functions that don't care if an object is a particular subtype, perhaps even a type that was not in existence when the code was originally written, so long as it conforms to a particular contract.

In real Apex Code, we have a few examples of this happening. For example, let's look at the function signature for Database.executeBatch:

public static Id executeBatch(Object obj);

(Note: the documentation should state Database.Batchable<T>, but, well, what are we going to do?)

If you try to give it just any old object, it'll complain that there's no method for executeBatch that expects a parameter of type T (where T simply means any type). This is a compile-time error. If you cast to Database.Batchable, and T implements it, it'll run okay, but if not, you will get a runtime type conversion error.

Salesforce allows us to write batches for things they haven't possibly even thought of yet. The interface forces a contract that makes sure that we can't feed it bogus code. Every object that goes into Database.executeBatch must be something that can legitimately run as a batch (ignoring a way that you can force a batch crash with execute anonymous classes...).

We see the same thing with the Comparable class, which allows us to sort our objects that are in a list, even though the Apex Code runtime has no way of knowing in advance how we're going to choose to sort our data.

The entire point of working with super classes and interfaces isn't so that some line of code can have access to a bunch of different things, really, it's so that we can write reusable code that can work with a variety of different things, even though that code may not not know what data type we're giving it.

Finally, I'll show you a real life example. We'll write our own sorting algorithm, known as Gnome Sort.

public static void gnomeSort(System.Comparable[] someList) {
    Integer index = 0, size = someList.size() - 1;
    while(index < size) {
        if(someList[index].compareTo(someList[index+1]) > 0) {
            Comparable temp = someList[index+1];
            someList[index+1] = someList[index];
            someList[index] = temp;
            index = Math.max(0, index-1);
        } else {
            index++;
        }
    }
}

This isn't a particularly elegant sorting algorithm, but it's short enough to see exactly what's going on without too many trappings. Using this method, we can sort any Comparable list of elements. Thanks to another bug I found while writing this, Integer (and apparently other system classes) do not implement Comparable, but it should work for any class you write.

First, we'll add an Age to our Cats:

public class Cat extends Animal implements Comparable {
    public string name;
    public Integer age;
    public Cat(String n, Integer a) {
        name = n;
        age = a;
    }
    public Integer compareTo(Object o) {
        return age - ((Cat)o).age;
    }
    // Previously explored methods omitted for brevity
}

We can then sort them by age:

Cat[] cats = new Cat[] { new Cat('Fred', 2), new Cat('Alice', 4), new Cat('Bob', 3), new Cat('Mouser',1)};
Utils.gnomeSort(cats);

In fact, we can go one step further, and plug in another interface:

public interface CompareTwoObjects {
    Integer compare(Object a, Object b);
}

And create some classes that use this:

public class CatNameCompare implements CompareTwoObjects {
    public Integer compare(Object a, Object b) {
        Cat cat1 = (Cat)a, cat2 = (Cat)b;
        return cat1.name.compareTo(cat2.name);
    }
}

We can then customize our gnomeSort to use the custom compare function:

public static void gnomeSort(Object[] someList, CompareTwoObjects compare) {
    Integer index = 0;
    while(index < someList.size() - 1) {
        if(compare.compare(someList[index], someList[index+1]) > 0) {
            Object temp = someList[index+1];
            someList[index+1] = someList[index];
            someList[index] = temp;
            index = Math.max(0, index-1);
        } else {
            index++;
        }
    }
}

We now have a plug-and-play gnomeSort method that accepts any list of objects, and as long as our CompareTwoObjects class knows how to compare two given objects, the sort will work correctly:

Cat[] cats = new Cat[] { new Cat('Fred', 2), new Cat('Alice', 4), new Cat('Bob', 3), new Cat('Mouser',1)};
Utils.gnomeSort(cats, new CatNameCompare());

Of course, implementing Comparable and using the standard List.sort function would be faster, but hopefully this answer shows why we care about Interfaces-- we can write code that implements a specific algorithm, and then allow it to plug into any data type that we want to support, even those types that were not in existence when we wrote the code.

Related Topic