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

عرض عملي: إعادة تنظيم الحسابات المصرفية في حزم حسب الميزة

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

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

Lesson Overview

This is a hands-on lesson. You have learned four ideas in this module: classes live in packages that match folders, imports connect packages, package-private hides helpers, and feature packages make hiding practical. Now we apply all four to a small Bank Accounts project, step by step, and you can follow along on your own machine.

The starting point is the project most beginners produce: every class in one folder, no package line, everything public. The finish line is three feature packages (account, customer, notification) plus an app package for the entry point, with only the intended classes public. The behavior will not change by one character; only the structure does. That is what refactoring means: improving the structure without changing what the program does.

This lesson prepares the lab "Bank Accounts v5", where you do the same on your own version of the project. The classes here are a small stand-in for your earlier work, so use your own classes in the lab.

Everything below was compiled and run with JDK 21. Terminal commands are shown for a plain terminal; if you use an IDE, it has "move class" and "new package" features whose menus differ by product and version, so check its help. An IDE's move refactoring also fixes package lines and imports for you, which is a good reason to prefer it on big projects.

Learning Objectives

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

  • Plan a package structure for an existing project before touching any file.
  • Move classes into packages and correct each package line and folder.
  • Add the imports that the new structure requires.
  • Decide, class by class, what must stay public and what can become package-private.
  • Offer a public way to obtain an object whose class is hidden (a small public factory).
  • Use the compiler's errors as a checklist during a refactoring.
  • Verify that the program's output did not change.

Written Explanation

Step 0: the starting point

The project is eight small files in one folder, all in the default package. Here is its shape:

text
before/
  Account.java            public: balance, deposit, interest
  Customer.java           public: a name
  Notifier.java           public interface: send(to, message)
  EmailNotifier.java      public: prints "email to ..."
  InterestCalculator.java public: the interest formula
  Main.java               public: builds an account and uses it

Account holds a Customer owner and a Notifier; deposit sends a notification; addMonthlyInterest uses InterestCalculator. We compile and run it to record the behavior we must preserve:

text
javac -d bout before/*.java
java -cp bout Main
text
email to Sara: deposited 100.0
1302.1666666666667

Always capture the "before" output first. It is your test for the whole refactoring.

Step 1: plan the packages

Ask three questions for each class: which feature does it serve, who outside that feature needs it, and what does it depend on?

ClassFeatureNeeded by other features?
Accountaccountyes: Main creates it
InterestCalculatoraccountno, only Account uses it
Customercustomeryes: Account holds one, Main creates it
Notifiernotificationyes: Account is given one
EmailNotifiernotificationno, only needs to be constructed
Mainthe entry pointnone

Main is a special case. It builds objects from every feature, so it sits in its own package bank.app. Our packages all start with bank; in a real project you would use a reversed-domain prefix such as com.example.bank, as in Lesson 1. We use the short bank to keep the lines short.

Step 2 and 3: create packages, move classes, add imports

Create the folders bank/account, bank/customer, bank/notification and bank/app. Move each file and add a package line. Then fix the imports. The only cross-package uses are in Account (it uses Customer and Notifier) and Main (it uses Account, Customer and EmailNotifier).

Step 4: make the surface small

Remove public wherever no other package needs the class. This is where the compiler becomes your guide: if you remove public from something that is used from another package, it tells you with "is not public ... cannot be accessed from outside package". Then ask the question from Lesson 3: should that code really be using this class?

Two decisions deserve discussion:

  • InterestCalculator is used only by Account, in the same package. It becomes package-private, along with its method.
  • EmailNotifier is only constructed in Main. If it becomes package-private, Main cannot write new EmailNotifier(). We offer a small public class Notifiers with a static method email() that returns a Notifier. Main then depends on the public interface and the factory, and the concrete class stays hidden. The email class can later be replaced by a different channel without touching Main. The constructor of Notifiers is private because nobody should create one.

Step 5: the planted mistake

Before you finish, test the boundary. In bank/app, write a class that uses EmailNotifier directly. The compiler says:

text
error: EmailNotifier is not public in bank.notification; cannot be accessed from outside package
import bank.notification.EmailNotifier;
                        ^

The wrong fix is to make EmailNotifier public again. The right fix is to use the public surface: Notifiers.email().

Step 6: compile, run, compare

Compile from the after folder and run with the full name of Main. The output must match the "before" output exactly. If it does, the refactoring is done.

Examples

Example 1: the moved and trimmed classes (simple)

The smaller classes. bank/customer/Customer.java keeps public because Account and Main use it from other packages:

java
package bank.customer;

public class Customer {
    private final String name;

    public Customer(String name) { this.name = name; }

    public String name() { return name; }
}

bank/notification/Notifier.java stays a public interface, because other packages program against it:

java
package bank.notification;

public interface Notifier {
    void send(String to, String message);
}

bank/notification/EmailNotifier.java loses public. Interface methods are implicitly public, so send stays public in the class too (an implementation of an interface method must be public):

java
package bank.notification;

class EmailNotifier implements Notifier {
    public void send(String to, String message) {
        System.out.println("email to " + to + ": " + message);
    }
}

bank/notification/Notifiers.java is the public way to get one:

java
package bank.notification;

public final class Notifiers {
    private Notifiers() { }

    public static Notifier email() { return new EmailNotifier(); }
}

bank/account/InterestCalculator.java, now hidden:

java
package bank.account;

class InterestCalculator {
    double monthlyInterest(double balance) { return balance * 0.02 / 12; }
}

Example 2: Account and Main with their imports (practical)

Account is the public front desk of its feature. Its only changes are the package line and two imports:

java
package bank.account;

import bank.customer.Customer;
import bank.notification.Notifier;

public class Account {
    private final Customer owner;
    private final Notifier notifier;
    private double balance;
    private final InterestCalculator calculator = new InterestCalculator();

    public Account(Customer owner, double balance, Notifier notifier) {
        this.owner = owner;
        this.balance = balance;
        this.notifier = notifier;
    }

    public void deposit(double amount) {
        if (amount > 0) {
            balance += amount;
            notifier.send(owner.name(), "deposited " + amount);
        }
    }

    public void addMonthlyInterest() {
        balance += calculator.monthlyInterest(balance);
    }

    public double balance() { return balance; }
}

InterestCalculator needs no import: same package. bank/app/Main.java:

java
package bank.app;

import bank.account.Account;
import bank.customer.Customer;
import bank.notification.Notifiers;

public class Main {
    public static void main(String[] args) {
        Account a = new Account(new Customer("Sara"), 1200, Notifiers.email());
        a.deposit(100);
        a.addMonthlyInterest();
        System.out.println(a.balance());
    }
}

Example 3: compile, compare and review the result (realistic)

text
cd after
javac -d out -sourcepath . bank/app/Main.java
java -cp out bank.app.Main
text
email to Sara: deposited 100.0
1302.1666666666667

The output is identical to the "before" run. The compiler produced this class tree, one folder per package:

text
out/bank/account/Account.class
out/bank/account/InterestCalculator.class
out/bank/app/Main.class
out/bank/customer/Customer.class
out/bank/notification/EmailNotifier.class
out/bank/notification/Notifier.class
out/bank/notification/Notifiers.class

Now review the public surface of each package:

PackagePublicHidden (package-private)
bank.accountAccountInterestCalculator
bank.customerCustomernone
bank.notificationNotifier, NotifiersEmailNotifier
bank.appMainnone

Of seven classes, five are public instead of seven, and every one of them is public for a reason you can state. Imports also tell the dependency story: account depends on customer and notification; neither depends back on account. That one-way pattern is what we asked for in Lesson 4.

Backend Insight

Refactoring a project into packages is routine work. Real codebases begin as a few classes and grow; someone must eventually say "this is now three features". The process in this lesson is how it is done safely: record behavior, move in small steps, let the compiler list what broke, and re-run to compare.

The small public factory (Notifiers.email()) is a pattern you will meet again. It lets a package decide how an object is built while callers see only an interface. Later, when the notification feature gains SMS and push channels, only bank.notification changes. In a modular monolith, as later modules in the roadmap will show, packages like these become the boundaries between modules, and the public classes become each module's API.

Common Mistakes

Mistake 1: moving everything at once

Move one package, compile, then the next. A pile of errors from twenty files at once is hard to read, and the compiler's first errors are often the root cause of the rest.

Mistake 2: forgetting to update package lines after moving files

A moved file whose package line still says the old name produces "package does not exist" errors elsewhere. Folder and package line change together.

Mistake 3: reaching for public to silence errors

Each time you do, record why the class must be public. If you cannot give a reason, find another solution.

Mistake 4: changing behavior while refactoring

If you fix a bug or rename a method at the same time, you cannot tell whether a changed output came from the move. Refactor first, compare, then make other changes in a separate step.

Mistake 5: leaving Main and helpers in the default package

Code in the default package can import classes from named packages, but named-package classes cannot import anything from the default package. A leftover Main there is therefore a dead end for any other class that wants to use it. Keep Main in a named package so the whole project is consistent and can be launched with a full class name.

Exercises

Try each exercise yourself before opening the solutions.

Level A: Foundation

A1. For class Customer in package bank.customer, give the file path under the source root and its package line.

أظهر الحل A1

A1. bank/customer/Customer.java with package bank.customer;.

A2. After the refactoring, which class does Main use to obtain a Notifier, and why can it not write new EmailNotifier()?

أظهر الحل A2

A2. It uses Notifiers.email(), which returns the public Notifier interface. EmailNotifier is package-private in bank.notification, so code in bank.app cannot name or construct it.

A3. Name the two cross-package imports in Account.

أظهر الحل A3

A3. import bank.customer.Customer; and import bank.notification.Notifier;.

Level B: Application

B1. Add a ReportPrinter class to the project that prints an account's balance. Put it in the package where it fits best, decide its modifier, and say which imports it needs. Assume only Main will call it.

أظهر الحل B1

B1. It fits bank.app if it is only a printing helper for Main: class ReportPrinter with no modifier (package-private, since only Main in the same package calls it), importing bank.account.Account. If other features will print reports later, move it to its own report feature package and make the entry class public.

B2. You decide InterestCalculator should also be used by a new bank.report package. Describe two ways to handle that, and which you prefer.

أظهر الحل B2

B2. Option 1: make InterestCalculator public and import it, which widens the public surface. Option 2: give Account a public method (for example projectedInterest()) that the report package calls, keeping the calculator hidden. Prefer option 2: the formula stays free to change.

B3. The refactored project compiles, but the output now says 1302.17 instead of 1302.1666666666667. List two steps to find out whether the move caused this.

أظهر الحل B3

B3. Re-run the "before" build and compare outputs, which shows whether the difference exists before the move. Then check whether the formatting code changed between versions (a stray edit to a print statement, for instance). If the numbers differ in the before run as well, the cause is not the move.

Level C: Challenge

C1. Add an SmsNotifier in bank.notification and a Notifiers.sms() factory. List every file you change, and say whether Account changes.

أظهر الحل C1

C1. Add SmsNotifier.java (package-private, implements Notifier) and edit Notifiers.java to add public static Notifier sms(). Account does not change, because it depends only on the Notifier interface. Main changes only if you want it to use SMS.

C2. Write the two-sentence justification the lab asks for ("why feature over layer packaging") using the Bank Accounts project as your example.

أظهر الحل C2

C2. For example: "Feature packages keep everything about accounts, customers or notifications together, so a change touches one folder. They let helpers like InterestCalculator and EmailNotifier be package-private, so only the classes other features really need are public."

Mini Challenge

Reorganize your own Bank Accounts project (or the eight-file project above) from scratch without looking at the solution code.

Check your work: the program prints the same output as before; every class sits in a folder that matches its package line; no class remains in the default package; at least one helper is package-private; and you can state, for every public class, who outside its package needs it. Write your public-surface table like the one in Example 3.

Summary & Next Lesson

Key takeaways

  • Refactoring changes structure, not behavior: record the output first, compare at the end.
  • Plan by asking, for each class: which feature, who needs it, what does it use?
  • Move files and package lines together, then add imports for cross-package uses.
  • Make a class public only when another package needs it. Use a small public factory to hide a concrete class behind an interface.
  • Compiler errors guide the work. A "not public" error is a prompt to think, not an instruction to add public.
  • The result is small public surfaces and one-way dependencies between features.

Next lesson

This is the last lesson of the module on packages and organization. Apply it in the lab "Bank Accounts v5", then check yourself with the module quiz.

Self-check

  1. What does refactoring promise about the program's behavior?
  2. Why is InterestCalculator package-private?
  3. How can Main get an email notifier without seeing EmailNotifier?
  4. What should you do before you start moving files?
  5. What is the compiler's complaint when code in another package uses a hidden class?
أظهر الإجابات
  1. That it does not change: the same input gives the same output.
  2. Only Account, in the same package, uses it.
  3. Through the public factory Notifiers.email(), which returns the Notifier interface.
  4. Compile and run the project, and record its output to compare later.
  5. "is not public in <package>; cannot be accessed from outside package".

References

التطبيق

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

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

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

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