Design principles

  1. Identify the parts of your application that are changing, strip them out, and don’t couple them with the rest.
  2. Program for interfaces, not implementations
  3. Use composition more than inheritance

Let’s say we want to design a game of ducklings, and these ducklings can fly, they can quack, they can swim, let’s define it this way.

public abstract class Duck {

    public abstract void display(a);

    public void swim(a) {
        System.out.println("i can swimming!");
    }

    public void quack(a) {
        System.out.println("quack quack.");
    }

    public void fly(a) {
        System.out.println("i can fly!"); }}Copy the code

Through the abstract class definition, we can implement different ducks with common characteristics, such as the red duck.

public class RedDuck extends Duck {
    @Override
    public void display(a) {
        System.out.println("i'm a red duck!"); }}Copy the code

So the problem is, not all ducks can fly, for example, toy ducks can’t fly, so what to do? The simplest thing you can think of is to override the fly method in a subclass so that it doesn’t fly.

public class ToyDuck extends Duck {
    @Override
    public void display(a) {
        System.out.println("i'm a toy duck!");
    }

    @Override
    public void fly(a) {
        System.out.println("i can't fly!"); }}Copy the code

This class alone is fine, but if there are 100 types of ducks that can’t fly, we can’t replicate the fly method inherited from 100 subclasses. That would be stupid. The root cause of the problem is: although flying is a common characteristic of ducks, the flight movements have been separated into two specific manifestations, can fly or can’t fly.

Since inheritance doesn’t satisfy our needs, how do we satisfy our needs? Let’s go over our needs first. First of all, flight is a trait that all ducks have and need to have. Secondly, flight is divided into flying and non-flying. We want the ducks that can fly to have a way to fly, and the ducks that can’t fly to have a way to not fly, but we don’t want to manually implement it in each subclass. Because flying can be done once, and then associated with all flying ducks. The flightless action is implemented once, and then associated with all flightless ducks.

Design principle 1: Keep moving parts of the program separate and don't couple them with other parts

According to design principle 1, the feature of flight is the place of change. We separate it from Duck and define it as an interface. There are two subclasses to implement it, one can fly and one can’t fly.

public interface Flyable {
    void performanceFly(a);
}
Copy the code
public class CanFly implements Flyable {
    @Override
    public void performanceFly(a) {
        System.out.println("i can fly!"); }}Copy the code
public class FlyNoWay implements Flyable {
    @Override
    public void performanceFly(a) {
        System.out.println("i can't fly!"); }}Copy the code

Then add the flying property to the Duck class as a property of the Duck class, and define a flying method for the Duck class.

public abstract class Duck {

    public Flyable flyable;

    public abstract void display(a);

    public void swim(a) {
        System.out.println("i can swimming!");
    }

    public void quack(a) {
        System.out.println("quack quack.");
    }

    public void fly(a) { flyable.performanceFly(); }}Copy the code

The advantage of this approach is that all successor ducks will still be able to fly, but the availability is subject to the implementation of Flyable, which can be tailored to the individual ducks. At the same time, the advantage of placing the Flyable interface in the Duck class is that the parent class defines flight options, which are “delegated” to child classes. That is, programming against the interface, not the implementation.

public class ToyDuck extends Duck {
    public ToyDuck(a) {
        flyable = new FlyNoWay();
    }
    @Override
    public void display(a) {
        System.out.println("i'm a toy duck!"); }}Copy the code

The test code is as follows:

public class TestToyDuck {
    public static void main(String[] args) {
        Duck duck = newToyDuck(); duck.fly(); }}Copy the code

I can’t fly!

The above implementation has the disadvantages of: when instantiating the subclass duck, the ability to fly is written dead, and cannot be dynamically changed.

public abstract class Duck {

    public Flyable flyable;

    public abstract void display(a);

    public void fly(a) {
        flyable.performanceFly();
    }

    public Flyable getFlyable(a) {
        return flyable;
    }

    public void setFlyable(Flyable flyable) {
        this.flyable = flyable; }}Copy the code

Set the getter/setter method in Duck Duck and change it dynamically after instantiating the subclass.

public class TestToyDuck {
    public static void main(String[] args) {
        Duck duck = new ToyDuck();
        duck.fly();
        duck.setFlyable(newCanFly()); duck.fly(); }}Copy the code

Print result:

i can’t fly! i can fly!

In this way, we can be very flexible in implementing our requirements. We can create a flying duck again by setting a flying implementation class after instantiating the duck.