انتقل إلى المحتوى
أمين أبوزيد
منشورBEG-022-L07

عرض عملي: إعادة هيكلة وراثة سيئة إلى تركيب

  • الدرس 7 من 7
  • 11 دقيقة قراءة
  • مبتدئ
في هذا الدرس 11 موضوعات

هذا الدرس متاح حاليًا باللغة الإنجليزية فقط، ولم تُنشر نسخته العربية بعد.

Lesson Overview

In the last lesson inheritance was the right tool. Here it is the wrong one, and you will fix it. This is a guided refactoring: you start from a small hierarchy that compiles, passes a quick test, and quietly lets users break its rules, then you replace extends with a field.

The family tree picture helps one last time. A stack of plates is not a child of a general shelf where you can slide a plate out of the middle. A stack uses a list to hold its items, but it does not become a list.

Why this matters: refactoring a wrong "is-a" into "has-a" is a real task on real code bases. The same four steps apply every time: name the broken rule, hold the old parent in a private field, forward only what should stay, and close any leak.

This lesson builds on "Composition over inheritance" and uses encapsulation from the previous module. Where steps depend on your IDE or operating system, they say what to create rather than where to click.

Learning Objectives

By the end of this lesson you will be able to:

  • Recognise a hierarchy where a child inherits behavior that breaks its own rules.
  • Name the broken rule in one sentence.
  • Replace extends with a private field and forwarding methods.
  • Spot and remove a leak that gives callers back the internal object.
  • Show that the old misuse no longer compiles.
  • Write a short, clear explanation of the refactor.

Written Explanation

Step 1: the provided hierarchy

The library class you will start from is a small list of ints, written for this lesson. It supports adding at the end, adding at an index, removing by index, reading, and asking its size:

java
public class IntList {
    private int[] items = new int[10];
    private int size = 0;

    public void add(int value) {
        items[size] = value;
        size++;
    }

    public void add(int index, int value) {
        for (int i = size; i > index; i--) {
            items[i] = items[i - 1];
        }
        items[index] = value;
        size++;
    }

    public int remove(int index) {
        int removed = items[index];
        for (int i = index; i < size - 1; i++) {
            items[i] = items[i + 1];
        }
        size--;
        return removed;
    }

    public int get(int index) { return items[index]; }
    public int size() { return size; }
    public boolean isEmpty() { return size == 0; }
}

(The array has room for ten values only, to keep the code short. A real list grows; the standard library's lists do. It does not matter for this lesson.)

A teammate wanted a stack and, because a stack stores items in order, reused it with extends:

java
public class BadStack extends IntList {
    public void push(int value) { add(value); }
    public int pop() { return remove(size() - 1); }
    public int peek() { return get(size() - 1); }
}

It looks tidy: three one-line methods, no repeated code. The is-a sentence, though, sounds forced: "a stack is an int list". A stack is something you use only from one end.

Step 2: show the break and name the rule

A stack has one rule: last in, first out. The only way in is push, the only way out is pop. But BadStack inherited every public method of IntList. Try this:

java
BadStack s = new BadStack();
s.push(1);
s.push(2);
s.push(3);
s.add(0, 99);      // inherited: inserts at the bottom
s.remove(1);       // inherited: removes from the middle
System.out.println(s.pop());
System.out.println(s.pop());
System.out.println(s.pop());

It compiles and prints:

text
3
2
99

A real last-in-first-out stack holding 1, 2, 3 would give 3, 2, 1. Here the 1 is gone and a 99 appeared at the bottom, because callers used the inherited add(index, value) and remove(index).

Name the broken rule in one sentence: "BadStack inherits add(index, value) and remove(index), which let any caller break last-in-first-out." This sentence is also the first half of the explanation the lab asks for.

The same problem exists with a standard library class: the Java documentation for java.util.Stack shows that it extends Vector, and it recommends using Deque implementations instead for stack operations. It is a historical example of the trap. Lists and Deque come in a later module; here you only need to see that even the library has the problem.

Step 3: hold the list in a private field

Create a class with the same job but has-a instead of is-a:

java
public class Stack {
    private final IntList items = new IntList();
    ...
}

The field is private so nobody outside can touch it, and final so the stack always uses the same list. This is the whole structural change: extends IntList becomes private final IntList items.

Step 4: forward only what should stay

Expose exactly the operations a stack promises: push, pop, peek, isEmpty. Each one forwards to the list:

java
public class Stack {
    private final IntList items = new IntList();

    public void push(int value) { items.add(value); }

    public int pop() {
        if (items.isEmpty()) {
            System.out.println("Stack is empty");
            return -1;
        }
        return items.remove(items.size() - 1);
    }

    public int peek() {
        if (items.isEmpty()) {
            System.out.println("Stack is empty");
            return -1;
        }
        return items.get(items.size() - 1);
    }

    public boolean isEmpty() { return items.isEmpty(); }
}

(Returning -1 for an empty stack is a placeholder for the lab; a later module shows a better way to report errors.)

Use it:

java
Stack s = new Stack();
s.push(1);
s.push(2);
s.push(3);
System.out.println(s.pop());
System.out.println(s.peek());
text
3
2

Now try the old abuse, s.add(0, 99);. It no longer compiles:

text
Main.java:9: error: cannot find symbol
        s.add(0, 99);
         ^
  symbol:   method add(int,int)
  location: variable s of type Stack

The compiler now enforces the rule. The unwanted methods did not become "discouraged"; they are simply absent.

Step 5: the planned mistake, a leaking getter

A common slip while refactoring: someone adds a convenient getItems() that returns the internal list:

java
public IntList getItems() { return items; }

The class compiles, and the old attack works again, now through the back door:

java
s.getItems().add(0, 99);
s.getItems().remove(1);

Popping three times prints 3, 2, 99, the same broken order as before. The leak gives callers the inner object, with all its methods, so encapsulation is lost. Two fixes: remove the getter, which is best when nobody needs it, or return a copy or a read-only view of the data. If callers only need the count, add a size() that forwards to items.size() and nothing else.

This is the same lesson as returning internal mutable arrays from the previous module: returning an internal mutable object hands over control.

Step 6: explain the refactor in two sentences

The lab requires a short explanation. A good one is:

Stack extends IntList was wrong because a stack is not a kind of list: it inherited methods that insert or remove in the middle and break last-in-first-out. Now Stack holds an IntList in a private field and forwards only push, pop, peek, and isEmpty, so the rule can no longer be broken from outside, and the list can be replaced later without changing callers.

Notice the structure: (1) what was wrong and which rule broke, (2) what the new design does and what it gains.

Examples

Example 1: the whole refactor in two lines (simple)

Before: public class BadStack extends IntList { ... }. After: public class Stack { private final IntList items = new IntList(); ... }. Every other change follows from these two lines: the compiler tells you which inherited calls now need items. in front.

Example 2: a Queue by composition (practical)

A queue is first in, first out: items enter at the end and leave from the front. Starting fresh with composition:

java
public class Queue {
    private final IntList items = new IntList();

    public void enqueue(int value) { items.add(value); }

    public int dequeue() { return items.remove(0); }

    public int size() { return items.size(); }
}

Enqueue 10, 20, 30, dequeue twice, then print size():

text
10
20
1

The same IntList serves two different classes, each offering a different small window. With inheritance both would have inherited the whole list interface. (An empty queue would fail on remove(0); guarding that is the same idea as in pop, left out here for length.)

Example 3: choosing the fix for a leak (realistic)

A teammate argues: "The report page needs to show the stack's contents, so getItems() is necessary." Evaluate it:

  1. Does the page need to change the list? No, only to read it.
  2. So return a copy of the values, or provide size() and a peek()-like read method. Never the internal list.
  3. Even better, ask what the page really needs: perhaps only the top item and the count. Give exactly that.

Handing out less always keeps more freedom for later changes: tomorrow items can become a different structure, and only the inside of Stack changes.

Backend Insight

In a backend, the same refactor appears as "don't extend the framework's collection or utility class to get its helper methods; hold it". A UserCache that extends a map class exposes every map method, so any code anywhere can clear or overwrite the cache, and you can never change how it stores things without breaking callers. A UserCache that holds a map and offers find(id) and store(user) gives you a small contract. When the storage later changes from memory to a remote service, only the inside of the class changes.

This is also how reviewers check a design: they ask, "which methods does this class offer that it should not?" If the answer is "everything the parent had", inheritance was the wrong choice.

Common Mistakes

Mistake 1: Keeping extends and hiding methods

You cannot hide an inherited public method. A child may not make it less accessible, because of the rule you saw in Lesson 3. Trying to override it with a narrower modifier is a compile error.

Mistake 2: Forwarding everything

Copying all the held class's methods defeats the point. Forward only what the new class promises.

Mistake 3: Leaking the internal object

A getter that returns the field gives callers all its methods. Return a copy, a read-only view, or nothing.

Mistake 4: Calling the field public to "make it easy"

A public field is a leak with extra steps. Keep it private.

Mistake 5: Writing a vague explanation

"It is better" does not satisfy a reviewer. Name the broken rule, the new structure, and the gain.

Exercises

Try each exercise yourself before opening the solutions.

Level A: Foundation

A1. Which two lines change when BadStack becomes Stack (the class line and one other)?

أظهر الحل A1

A1. The class line, extends IntList removed, and the new field line private final IntList items = new IntList();. The methods then use items. in front of the list calls.

A2. Name two inherited methods that let callers break a stack's rule.

أظهر الحل A2

A2. add(int index, int value) and remove(int index) (also get(int index) lets callers read the middle).

A3. Why is the items field private and final?

أظهر الحل A3

A3. private so no outside code can use the list directly; final so the stack always works on the same list object and cannot be pointed at another one by mistake.

Level B: Application

B1. Write Queue using composition as in Example 2, and add isEmpty(). What does dequeue() do on an empty list in the version shown?

أظهر الحل B1

B1. isEmpty() is public boolean isEmpty() { return items.isEmpty(); }. In the version shown, dequeue() on an empty list calls items.remove(0) and reads an element that does not exist (the array slot still holds 0) and then lowers size below zero, which corrupts the queue. A guard like the one in pop is the fix: check isEmpty() first.

B2. This Stack has a bug in design. Fix it without breaking the code that prints the stack's size:

java
public IntList getItems() { return items; }
أظهر الحل B2

B2. Remove it, since nothing needs to change the list. For the size use case, replace it with public int size() { return items.size(); }.

B3. Write the two-sentence refactor explanation for Queue extends IntList becoming a composed Queue.

أظهر الحل B3

B3. Queue extends IntList was wrong because a queue is not a kind of list: it inherited methods like add(index, value) and remove(index) that insert and remove anywhere and break first-in-first-out. Now Queue holds an IntList privately and offers only enqueue, dequeue, and size, so callers cannot break the order and the storage can change later.

Level C: Challenge

C1. Stack also needs size(). What is the minimal, safe way to add it? Show the method and say what it must not return.

أظهر الحل C1

C1.

java
public int size() { return items.size(); }

It must not return the list itself or any object that lets callers change it; an int count cannot leak anything.

C2. The team wants a stack and a queue to share their list handling. Argue for or against a new base class ListHolder that both extend. Use the signs from the previous lesson.

أظهر الحل C2

C2. Against: the new base class would be inheritance only for code reuse, the is-a sentence "a stack is a ListHolder" sounds forced, and children would inherit every protected helper and depend on its internals. Better: a small separate class that both hold, or just let each hold an IntList, which is already shared code. For: none of the warning signs is present if ListHolder is a documented extension point, but that is rarely the case here.

Mini Challenge

Write a History class for a browser-style "back" list that holds an IntList of page ids. It offers only visit(int id), back() (removes and returns the latest id, or prints No history and returns -1), and count(). Try to call add(0, 5) on a History and confirm that it does not compile. Finally, write the two-sentence explanation of why History should not extend IntList.

Check your work: only the three methods are visible from outside, and your explanation names the broken rule and the gain.

Summary & Next Lesson

Key takeaways

  • A child inherits every accessible parent method, including ones that break its own rules.
  • The refactor: remove extends, add a private final field of the old parent type, forward only the operations the class should offer.
  • The compiler then enforces the rule: unwanted methods are missing.
  • Never return the internal object; return a copy, a narrow value, or nothing.
  • A good explanation names the broken rule, the new structure, and the gain.

Next lesson

This was the last lesson of the inheritance module. The next module starts with the lesson "One call, many behaviors" (BEG-023-L01), which builds on extends and @Override to show how one call can run different code depending on the real object.

Self-check

  1. What rule does Stack extends IntList break?
  2. What replaces extends IntList in the refactor?
  3. Why is getItems() returning the field a problem?
  4. How do you show that the old abuse is gone?
  5. Name one sign that inheritance was the wrong choice here.
أظهر الإجابات
  1. Last-in-first-out, because inherited methods can insert or remove anywhere.
  2. A private final IntList field with forwarding methods.
  3. Callers get the whole internal list and can bypass the stack's rules.
  4. The old calls, such as s.add(0, 99), no longer compile.
  5. The child inherits methods it should not offer, or the is-a sentence sounds forced.

References

  • Oracle, Java SE 21 API, java.util.Stack. Supports: Stack extends Vector, and the documentation recommends Deque implementations instead for stack operations.
  • Oracle, Java Tutorials, Overriding and Hiding Methods. Supports: an override cannot have narrower access, so an inherited public method cannot be hidden by a child.
  • The IntList, BadStack, Stack, and Queue code and all outputs and the compiler message were compiled and run with JDK 21. The IntList class and the refactoring steps are this course's own teaching material, in line with the lab description.

التطبيق

طبّق هذا الدرس بمختبر الوحدة وتمارينها واختبارها.

المختبر والتمارين الاختبار

تعليم الدرس كمكتمل اختياري ويمكنك التراجع عنه في أي وقت. يبقى تقدمك محفوظًا في هذا المتصفح.

اكتب للبحث في المستويات والمراحل والوحدات والدروس.