神刀安全网

L is for the Liskov Substitution Principle

This is part 2 of the SOLID Principles for Android Developers series. If you missed part 1 or aren’t familiar with what the SOLID principles are, check outPart 1, where we intro SOLID and discuss the Single Responsibility Principle, andPart 2 where we talk about the Open/Closed principle.

The Liskov Substitution Principle

The third letter in the SOLID mnemonic acronym is L, which is for the Liskov Substitution Principle (LSP). The Liskov Substitution Principle was introduced by Barbara Liskov in 1987 in a keynote at a conference. The Liskov Substitution Principle states the following:

Objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program.

So…what does that really mean?

I’m sure you’re like me and you probably read it many times trying to figure out what that means only to find that you’re probably confusing yourself further and further by doing so. Unfortunately (or fortunately – depends on how you look at it), if you dig deeper into the Wikipedia entry on the Liskov Substitution Principle you’ll find that the article dives fairly deeply into some deep computer science, with links that branch out all over the place. Yikes.

The thing is, this principle is very easy to understand and you use it all the time without knowing it. You probably have even written code that adheres to the LSP.

An Example of Replacing Object Instance with Subtypes

Java is a statically typed language. The compiler is very good at catching type errors and notifying us via the errors that it reports. We’ve all done this many times. You try to assign a String to a Long or vice versa, and the compiler tells you you’ve made a mistake. The compiler is also very good at allowing us to write code that adheres to the Liskov Substitution Principle.

Let’s assume that you’re writing some Android code that allows you to work with the List type in Java. I’m sure you’ve written some code like this in your application at one time or another:

// Get the ids somehow (loop, lookup, etc) ArrayList<Integer> ids = getCustomerIds();  List<Customer> customers = customerRepository.getCustomersWithIds(ids);

At this point, you just care that the customerRepository returns a List<Customers> . Perhaps you’ve even written the CustomerRepository , but because your backend isn’t done yet, you’ve decided to separate your interface from the implementation. (Hmm, ‘Interface’… Maybe that could be related to the ‘I’ in SOLID? :wink:)

Assume the code looks like this:

public interface CustomerRepository {    List<Customer> getCustomersWithIds(List<Integer> ids);  }   public class CustomerRepositoryImpl implements CustomerRepository {    @Override    public List<Customer> getCustomersWithIds(List<Integer> ids) {         // Go to API, DB, etc and get the customers.          ArrayList<Customer> customers = api.getWholeLottaCustomers(ids);          return customers;    } }

In the code sample above, the customer repository needs a list of customer IDs so that it can obtain those customers. The customer repository only requires that that list of customer IDs be of type List<Integer> . When we call the repository we provide an ArrayList<Integer> like so:

// Get the ids somehow (loop, lookup, etc) ArrayList<Integer> ids = getCustomerIds();  List<Customer> customers = customerRepository.getCustomersWithIds(ids);

Wait a second…the customer repository needs a List<Integer> , not a ArrayList<Integer> . How can that still work?

This is the Liskov Substitution Principle at work. Since ArrayList<Integer> is a subtype of List<Integer> , the program will not falter: We’re replacing the instance of the requested type ( List<Integer> ) with an instance of its subtype ( ArrayList<Integer> ).

In other words, in the code above, we’ve depended upon an abstraction ( List<Integer> ), and because of that we can supply a subtype ( ArrayList<Integer> ) and the program will still run without an issue. Why is that so?

The reason is that the customer repository is depending upon the contract provided by the List interface. The ArrayList is an implementation of the List interface, therefore, when the program runs, the customer repository will not see that the type is of ArrayList , but as an instance of List . The Principle section of the LSP Wikipedia article explains this very well, so I’m going to quote it here:

Liskov’s notion of a behavioral subtype defines a notion of substitutability for mutable […] objects; that is, if S is a subtype of T, then objects of type T in a program may be replaced with objects of type S without altering any of the desirable properties of that program (e.g., correctness).

In short, we can replace anything that extends List and we will not break the program (but I’d still write a test to be sure).

I’m sure you’ve seen code like this written all over the place in your app, in sample code, etc. This is very common. If you’ve written code like this, you’ve been following the Liskov Substitution Principle. It’s easy, right? :+1:

We can take this even further in our example from above too! Since the CustomerRepositoryImpl implements the CustomerRepository interface contract, we know that anyone who relies on the CustomerRepository interface will benefit from the Liskov Substitution Principle.

ButHow?

Very simple. Assume that while under test we use Dagger to inject a MockCustomerRepository , which is just a class that implements the CustomerRepository interface. The calling code just knows that it’s working with an instance of CustomerRepository , not a mock version of it. Since the subtype ( MockCustomerRepository ) follows the same contract as the parent type ( CustomerRepository ), the correctness of the program will not be affected. This provides a ton of flexibility in testing and mocking, all because we followed the Liskov Substitution Principle.

What if a specific type is required?

Let’s flip this idea on its head. If you’re familiar with the Java type system, you probably know that List<E> actually implements Collection<E> .

Would the following compile? ( Hint: :no_good: )

// Get the ids somehow (loop, lookup, etc) Collection<Integer> ids = getCustomerIds();  List<Customer> customers = customerRepository.getCustomersWithIds(ids);

Why not?

The getCustomersWithIds only accepts List<Integer> . List does implement Collection , but Collection does not implement List . So while a List IS A Collection , a Collection IS NOT neccessarily a List . In this example, the compiler can’t prove that the Collection<Integer> is for sure a List<Integer> . When presented in this manner, these types are not compatible.

Return Types, Parameters, and More

The Liskov Substitution Principle is not limited to parameter arguments either. For example, the original code stated that the customer repository returned an ArrayList<Customer> :

public interface CustomerRepository {    List<Customer> getCustomersWithIds(List<Integer> ids);  }   public class CustomerRepositoryImpl implements CustomerRepository {    @Override    public List<Customer> getCustomersWithids(List<Integer> ids) {         // Go to API, DB, etc and get the customers.          ArrayList<Customer> customers = api.getWholeLottaCustomers(ids);          return customers;    } }  // Somewhere else in the program List<Customer> customers = customerRepository.getCustomersWithIds(...);

The customer repository is returning an ArrayList<Customer> . The caller has no idea, the only thing it knows is that it’s getting back a List<Customer> . This very same thing applies with Realm too. For example, the RealmResults class implements List<E> as well.

So what does that mean? Well, put simply I could return a RealmResults<Customer> from my repository and the caller only knows that they’re receiving a List<E> . This provides an insane amount of flexibility in your code. I could create my own customized list that extends from LinkedList and return that instead. Since LinkedList returns a List the application will continue to function, ensuring that we did not alter the correctness of the program.

You can rely on an abstraction without worrying about your app breaking. This happens all over in the Android framework.

Conclusion

The Liskov Substitution Principle is so simple, you probably didn’t even know there was a name for it. You use it every day and it provides vast benefits to you as a developer. You can implement this yourself not just with List but with your own interfaces.

Stay tuned for part 4 of the series where we talk about more interface goodness.

转载本站任何文章请注明:转载至神刀安全网,谢谢神刀安全网 » L is for the Liskov Substitution Principle

分享到:更多 ()

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址