OOPS Interview Questions · Question 24

A Bird base class defines fly(), but some birds cannot fly. What is wrong with the design, and how would you improve it?

Interview preparation resource from Gate Smashers.

Interview-ready answer

The problem is that flight is modeled as a universal property of Bird, although it is only a capability of some birds. A Penguin is still a valid Bird but cannot honor a fly() contract. This creates an incorrect abstraction and can violate the Liskov Substitution Principle if clients expect every Bird to fly. Remove fly() from the Bird base class. Model flight as an opt-in capability: for example, let flying birds implement a Flyable interface, and make code that requires flight depend on Flyable rather than Bird. If flight behavior must be shared or changed at runtime, use composition with a FlightBehavior strategy, but expose that behavior only on bird types that can fly. Do not use exceptions or no-op implementations to pretend that non-flying birds support flight.

Java examples: capability interface and strategy compositionjava
public final class BirdDesignExample {

    // Approach 1: model flight as an opt-in capability.
    interface Flyable {
        void fly();
    }

    abstract static class Bird {
        private final String name;

        protected Bird(String name) {
            this.name = name;
        }

        public String getName() {
            return name;
        }

        public void eat() {
            // Common bird behavior.
        }
    }

    static final class Sparrow extends Bird implements Flyable {
        Sparrow(String name) {
            super(name);
        }

        @Override
        public void fly() {
            System.out.println(getName() + " flies.");
        }
    }

    static final class Penguin extends Bird {
        Penguin(String name) {
            super(name);
        }
        // Penguin is a Bird, but it is not Flyable.
    }

    static void letItFly(Flyable bird) {
        bird.fly();
    }

    // Approach 2: compose a flight behavior for bird types that can fly.
    interface FlightBehavior {
        void fly(String birdName);
    }

    static final class WingFlight implements FlightBehavior {
        @Override
        public void fly(String birdName) {
            System.out.println(birdName + " flies using its wings.");
        }
    }

    static final class FlyingBird extends Bird implements Flyable {
        private FlightBehavior flightBehavior;

        FlyingBird(String name, FlightBehavior flightBehavior) {
            super(name);
            this.flightBehavior = flightBehavior;
        }

        @Override
        public void fly() {
            flightBehavior.fly(getName());
        }

        public void setFlightBehavior(FlightBehavior flightBehavior) {
            this.flightBehavior = flightBehavior;
        }
    }
}
Understand it clearly

Why the base design is incorrect

A base class should contain behavior that all of its valid subtypes can support. Declaring fly() on Bird says that every Bird can fly, which is false for birds such as penguins.

A non-flying subclass is then forced into a misleading implementation: it may throw an exception, do nothing, or require callers to check its concrete type before calling fly(). Each outcome weakens the class contract.

  • Incorrect abstraction: Flight is a selective capability, not an essential property of every bird.
  • Substitution problem: A Bird reference cannot safely be treated as flyable when some Bird subtypes cannot fulfill fly().

Relevant design principle

This is primarily an Liskov Substitution Principle issue. If Bird promises fly(), every subtype must preserve that promise. A non-flying bird cannot do so meaningfully.

It can also indicate an overly broad interface: clients that only need common bird behavior should not be coupled to flight. Single Responsibility Principle may support separating flight behavior, but it is not the central problem; the key issue is the invalid base-class contract.

Improved design

Keep Bird focused on behavior shared by all birds, such as eating or sleeping. Represent flight separately, so only birds that can fly expose a flight operation.

A Flyable capability interface is usually the simplest solution. Client code that needs flight should accept Flyable, not Bird. Alternatively, composition with a FlightBehavior strategy is useful when multiple flight implementations are shared or when the behavior must be changed at runtime.

  • Capability interface: Flying bird classes implement Flyable; non-flying bird classes do not.
  • Composition: A flying bird delegates to a FlightBehavior object. Non-flying birds should not expose a fly operation merely to delegate to a failure behavior.
Quick comparison
BasisCapability interfaceFlightBehavior composition
Best useUse when flight is simply an opt-in capability and callers need a compile-time contract.Use when flight implementations are shared, configurable, or changeable at runtime.
Client dependencyClients that need flight depend on Flyable.Clients interact with a flying bird or another object that owns a FlightBehavior.
Non-flying birdsThey do not implement Flyable.They should not be given a public flight operation solely to report that they cannot fly.