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

عرض عملي: رسم UML ثم برمجة Customer وAccount

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

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

Lesson Overview

This is a follow-along demo that puts the whole module together. You will draw the diagram for a small bank first, then write the code from the diagram, in the same order a real designer would. On the way you will make one deliberate mistake that every beginner makes with two-way links, watch it fail, and fix it.

In the family of contacts picture, you are now writing down who knows whom, who owns whom, and checking that what you wrote in the family tree matches what the family actually does.

Why this matters: in real projects the diagram and the code drift apart unless someone checks. The habit you practise here, "draw, code, compare", prepares you for the lab, Bank Accounts v4, and for the entity models you will meet when you work with databases.

This lesson builds on the four earlier lessons of the module and on the Account classes from earlier modules. List is still a preview here, as in lesson 2.

Learning Objectives

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

  • Draw a diagram for a small domain before writing code.
  • Code immutable Transaction, composite Account, and Customer with openAccount, from a diagram.
  • Implement a two-way link between Customer and Account so that both sides stay in sync.
  • Recognise and fix the "back link never set" bug.
  • Name each relationship in a note and confirm that diagram and code match.
  • Decide when to drop a back link.

Written Explanation

The task and the plan

The bank rules, in plain English:

  • A customer can open many accounts. Each account belongs to exactly one customer.
  • An account keeps its history of many transactions. A transaction has a type, an amount, and a date, and it never changes after it is created.

The plan has six steps:

  1. Sketch the diagram.
  2. Code Transaction as a small immutable class.
  3. Code Account with a private list of transactions.
  4. Code Customer with a private list of accounts and openAccount.
  5. Deliberately introduce the back-link mistake, watch it fail, and fix it.
  6. Compare diagram and code.

Step 1: the diagram

text
Customer 1 -------- * Account         (association: owner / accounts)
Account  1 ◆------- * Transaction     (composition: history)

Note that we draw no fields. The lines say everything. Customer to account is an association: we assume an account object is not simply part of a customer's body, and that it has its own life in the bank's rules. Account to transaction is composition: the account creates its transactions and no one else touches them.

Step 2: Transaction, immutable

A transaction is a fact about the past. If it could change, the history would not be trustworthy. So it gets final fields, no setters, and no list inside it. Three strings and a number are immutable already, so no defensive copies are needed.

Step 3: Account, the composite

Account creates its Transaction objects inside deposit and withdraw, keeps the list private, and returns a copy. It also holds a Customer field: the other half of the two-way link.

Step 4: Customer, the one-to-many holder

Customer keeps a List<Account>. openAccount creates the account and adds it to the list. It does not return the list itself.

Step 5: the planned mistake

We make Account hold its Customer, and then forget to set it. This happens often with bidirectional links: two fields must always describe one fact. If they are filled in two separate places, one will eventually be missing. The cure is to fill both in one method.

Step 6: compare

Count the lines in the diagram and the link fields in the code. They must match one for one.

Examples

Example 1: the first, broken version (simple)

java
class Account {
    private final String number;
    private Customer owner;                 // back link, easy to forget
    private final List<Transaction> transactions = new ArrayList<>();
    private double balance;

    Account(String number) { this.number = number; }

    void deposit(double amount, String date) {
        balance += amount;
        transactions.add(new Transaction("DEPOSIT", amount, date));
    }
    String summary() { return number + " (" + owner.getName() + ") balance " + balance; }
}

class Customer {
    private final String name;
    private final List<Account> accounts = new ArrayList<>();
    Customer(String name) { this.name = name; }
    String getName() { return name; }

    Account openAccount(String number) {
        Account account = new Account(number);
        accounts.add(account);              // forgot: tell the account who its owner is
        return account;
    }
}

Running this in main:

java
Customer amal = new Customer("Amal");
Account acc = amal.openAccount("A-1");
acc.deposit(100, "2026-03-01");
System.out.println(acc.summary());

fails (JDK 21):

text
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "Customer.getName()" because "this.owner" is null
	at Account.summary(Main.java:28)
	at Main.main(Main.java:49)

The customer knows its account, but the account does not know its customer: the two sides are out of sync, and nobody ever set owner.

Example 2: the fix, both sides set in one method (practical)

The account requires its owner at construction, and openAccount passes this:

java
class Account {
    private final String number;
    private final Customer owner;
    private final List<Transaction> transactions = new ArrayList<>();
    private double balance;

    Account(String number, Customer owner) {
        if (owner == null) {
            throw new IllegalArgumentException("an account needs an owner");
        }
        this.number = number;
        this.owner = owner;
    }
    // deposit, withdraw, summary, transactions() as before
}

class Customer {
    // ...
    Account openAccount(String number) {
        Account account = new Account(number, this);   // both sides set in one place
        accounts.add(account);
        return account;
    }
    List<Account> accounts() { return List.copyOf(accounts); }
}

The Transaction class is as sketched in step 2:

java
class Transaction {
    private final String type;
    private final double amount;
    private final String date;
    Transaction(String type, double amount, String date) {
        this.type = type;
        this.amount = amount;
        this.date = date;
    }
    String describe() { return date + " " + type + " " + amount; }
}

The date is a plain String for now, to avoid new topics. Running a small main (two accounts, a deposit, a withdrawal, then printing each account and its transactions) gave:

text
A-1 (Amal) balance 70.0
  2026-03-01 DEPOSIT 100.0
  2026-03-02 WITHDRAW 30.0
A-2 (Amal) balance 50.0
  2026-03-03 DEPOSIT 50.0

An account cannot exist without an owner, and the only ordinary path to a new account sets both sides.

Example 3: what the fix does not prevent (realistic)

Nothing stops other code from calling the constructor directly:

java
Account stray = new Account("A-9", amal);      // bypasses openAccount
System.out.println(stray.summary() + " | customer lists " + amal.accounts().size() + " accounts");
text
A-9 (Amal) balance 0.0 | customer lists 2 accounts

The account says its owner is Amal, but Amal's list does not contain it: out of sync again, in the other direction. Two honest options exist at this level:

  • Document the rule that accounts are created only through openAccount, and review code for violations.
  • Drop the back link if nobody needs account.getOwner(). One-directional links cannot disagree. In the diagram, that means a line with an arrow from Customer to Account.

A later module on packages shows how to make the constructor invisible outside the package, which closes the gap. Here we keep the first option and the diagram unchanged.

Finally, the comparison step. The diagram has two lines. The code has a List<Account> in Customer (and the Customer field in Account for the other end of the first line), and List<Transaction> in Account. Each line has matching fields, and Transaction has no link fields at all. Diagram and code match.

Backend Insight

The two-way links in this lesson are exactly what object-relational frameworks have to manage when they map objects to tables: a customer row, many account rows each pointing back with a customer id. When both sides exist in memory, someone has to keep them consistent, and the safest design is the one you just wrote: one method creates the child and sets both ends.

A second lesson for backend design: prefer one direction unless a use case needs both. Many production bugs come from a back link that was added "just in case" and never kept in sync.

Common Mistakes

Mistake 1: Setting the back link in a separate step

account.setOwner(this) called by someone else after construction is easy to forget. Set the link in the constructor, from the same method that adds to the list.

Mistake 2: Adding the account to the list but not the owner (or the reverse)

Both directions describe one fact. If you update one, update the other in the same method.

Mistake 3: Returning the live list from accounts()

Outsiders could add an account behind the customer's back and skip openAccount. Return List.copyOf(accounts).

Mistake 4: Letting the diagram drift

If you add a field link in code, add the line in the diagram, and the reverse. Compare before you call a task finished.

Mistake 5: Making Transaction mutable

A setter on Transaction lets history be rewritten. Keep it immutable.

Exercises

Try each exercise yourself before opening the solutions.

Level A: Foundation

A1. Put the demo steps in order: code Customer; sketch the diagram; compare; code Transaction; code Account.

أظهر الحل A1

A1. Sketch the diagram; code Transaction; code Account; code Customer; compare.

A2. Why is Transaction immutable?

أظهر الحل A2

A2. It is a record of something that happened. If it could be changed, the account's history could no longer be trusted.

A3. Which relationship does Account 1 ◆---- * Transaction show, and what does the diamond tell you about who creates the transactions?

أظهر الحل A3

A3. Composition. The diamond sits on the Account end, so the account is the whole: it creates and owns its transactions.

Level B: Application

B1. Write Customer.accounts() so that callers cannot change the customer's list. What does List.copyOf do when the caller tries add?

أظهر الحل B1

B1. List<Account> accounts() { return List.copyOf(accounts); }. An attempt to call add on the returned list throws UnsupportedOperationException, as in lesson 2.

B2. A Member has a list of Loan objects, and each Loan has a Member field. Show how Member.borrow(String title) creates the loan so both sides are set.

أظهر الحل B2

B2. (We compiled and ran this.)

java
Loan borrow(String title) {
    Loan loan = new Loan(this, title);   // the loan gets its member here
    loans.add(loan);                      // the member gets its loan here
    return loan;
}

With new Member("Lina"), m.borrow("Java").describe() gives Lina has Java and loanCount() is 1.

B3. In Example 3, why does stray not appear in amal.accounts()? Name two ways to avoid the problem.

أظهر الحل B3

B3. It was created by calling the constructor directly, so no code added it to the customer's list. Avoid the problem by creating accounts only through openAccount (a rule, enforced by review), or by removing the back link so there is nothing to disagree. Making the constructor inaccessible outside the package is a technique for a later module.

Level C: Challenge

C1. Add Account findAccount(String number) to Customer, returning the account or null when none matches. Also state what the caller must do with the result.

أظهر الحل C1

C1. (Compiled and run.)

java
Account findAccount(String number) {
    for (Account account : accounts) {
        if (account.getNumber().equals(number)) {
            return account;
        }
    }
    return null;
}

The method assumes Account has a getNumber() getter. For an unknown number it returns null, and calling findAccount("X") on a customer without that account printed null in our test. The caller must check for null before using the result.

C2. A teammate wants a getOwner() on Account "just in case". Make the argument for and against keeping the back link, then pick one.

أظهر الحل C2

C2. For: account.getOwner() makes statements and messages easy (summary() uses it). Against: a second field to keep in sync, and a bug source if someone creates accounts outside openAccount. A fair pick: keep it only if some code needs it, and only with the constructor-sets-it design from Example 2. If nobody calls getOwner(), drop the back link.

Mini Challenge

Start from an empty file and build the lab, Bank Accounts v4, in the order of this demo, with one change: make withdraw refuse any amount larger than the balance (as in the code we ran for Example 2). Before writing code, write one note that names each relationship type (Customer-Account, Account-Transaction). After coding, write a three-line list: lines in the diagram, link fields in the code, and whether they match.

Check your work: the account cannot exist without its owner, the transaction list is private and copied on the way out, and the note says "association" and "composition" in the right places.

Summary & Next Lesson

Key takeaways

  • Draw first, then code, then compare. Every diagram line should have matching fields.
  • Transaction is immutable; Account creates and owns its transactions; Customer holds its accounts.
  • A two-way link must be set on both sides in one method, ideally the constructor path.
  • If nobody needs the back link, drop it: one direction cannot get out of sync.
  • Hand out copies of lists, not the lists themselves.

Next lesson

This is the last lesson of the module. Finish the lab, Bank Accounts v4, and then check the quiz. The course outline lists "Java object model" as module BEG-026, whose first lesson is "The Object class". It studies what every Java class inherits, including how to compare objects for equality.

Self-check

  1. In what order do you do the steps of this demo?
  2. Why did the first version throw NullPointerException?
  3. How does openAccount keep both sides in sync?
  4. When should you drop a back link?
  5. How do you hand out the customer's accounts safely?
أظهر الإجابات
  1. Draw the diagram, code Transaction, Account, and Customer, then compare diagram and code.
  2. Account had an owner field that nobody set, so owner.getName() was called on null.
  3. It creates the account with this as the owner and adds it to the list in the same method.
  4. When no code needs to go from an account back to its customer.
  5. Return List.copyOf(accounts).

References

  • Oracle, List (Java SE 21 API). Supports that List.copyOf returns an unmodifiable list, so mutators such as add throw UnsupportedOperationException, and that later changes to the source are not reflected in the copy.
  • Oracle, ArrayList (Java SE 21 API). Supports ArrayList as a List implementation with an empty no-argument constructor.
  • Wikipedia, Class diagram. Supports the diamond and multiplicity notation used in the diagram.
  • All program output and the exception text in this lesson come from compiling and running the code with JDK 21.

التطبيق

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

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

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

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