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

إسناد المسؤوليات

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

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

Lesson Overview

In the last lesson you decided which classes exist in the library: Book, Member, Loan, and maybe Library. Now every behavior in the requirements needs a home: who works out whether a loan is overdue? Who decides whether a member may borrow another book? Who marks a book as lent?

Put it in the wrong class and the design still runs, but each change touches several classes and one class ends up knowing everything. In the staffing a small shop picture, this lesson is about handing each job to the person who already has the tools and information for it, not to a manager who must keep running to the others to fetch what is needed.

The lesson has one main rule, one warning sign, and one limit of the rule. All code was compiled and run with JDK 21.

This lesson builds on BEG-021 (invariants, "tell, don't ask"), BEG-024 (interfaces), and BEG-025 (collaboration between objects), and on the first lesson of this module.

Learning Objectives

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

  • Use the rule "give the job to the class that has the information" to place a behavior.
  • Recognize an anemic design where a manager does all the thinking.
  • Move a behavior from a manager into the class that owns the data.
  • Tell the difference between computing something and coordinating several objects.
  • Notice the warning sign of lots of data passed between two classes.
  • Explain "one reason to change" for a class.

Written Explanation

The rule: the class with the information does the job

Ask of any behavior: which class already holds the data needed to do this? Give the job to that class. This is a well-known design guideline; in Craig Larman's GRASP set of principles it is called Information Expert, stated as: assign a responsibility to the class that has the information needed to fulfil it.

A responsibility is something a class is expected to know or to do. Loan knows its due day, so "am I overdue?" is naturally Loan's job. No other class has that information without asking Loan for it.

A useful way to find the owner is to ask the question in the first person, as an object. "Am I overdue?" sounds right from a loan. "Is this loan overdue?" asked by a manager sounds like the manager is going to look inside the loan, which is exactly what the rule wants to avoid.

The anemic alternative

Here is a design that works but puts the job in the wrong place. Loan is a data holder with a getter, and a LoanManager does the calculation.

java
class Loan {
    private final int dueDay;
    Loan(int dueDay) { this.dueDay = dueDay; }
    int getDueDay() { return dueDay; }
}

class LoanManager {
    int fine(Loan loan, int today) {
        int lateDays = today - loan.getDueDay();
        return lateDays > 0 ? lateDays * 25 : 0;
    }
}

With new LoanManager().fine(new Loan(14), 17) it prints 75. The result is right. The shape is the problem. Martin Fowler calls this style an anemic domain model: the classes have the names of the domain's nouns but almost no behavior, and the logic lives in separate service objects. He objects that this is procedural programming dressed as objects: the data and the behavior that works on it are separated, which is the opposite of what objects are for.

Notice the warning sign in the code: fine reaches into loan with a getter to pull out one value, then does arithmetic on it. When one class keeps asking another for its data to compute something, the computation probably belongs in the other class.

The responsible alternative

Move the arithmetic to the class that owns the due day. Loan now guards its own rule, and callers tell it what they need, as BEG-021 taught.

java
class Loan {
    static final int FINE_PER_DAY = 25; // cents
    private final Book book;
    private final int dueDay;
    Loan(Book book, int dueDay) { this.book = book; this.dueDay = dueDay; }
    Book book() { return book; }
    boolean isOverdue(int today) { return today > dueDay; }
    int fine(int today) { return isOverdue(today) ? (today - dueDay) * FINE_PER_DAY : 0; }
}

The getter getDueDay() has disappeared. Nobody outside needs the raw due day any more, so dueDay is not exposed. Nothing is shared except the questions a caller may ask. If the fine rule changes, one class changes.

Placing the rest of the library's behaviors

Apply the rule to each job.

JobWho has the informationOwner
Is this loan overdue? What is its fine?Due dayLoan
May this member borrow another book?The member's current loans and the limitMember
Is this book free? Mark it as lentWhether the book is on loanBook
Lend a book to a memberNeeds member, book, and a new loan togetherLibrary

The last row shows a second kind of job. Computing something from your own data goes to the owner of the data. Coordinating several objects (check the member, mark the book, create the loan, record it) belongs to a class whose job is to connect them. That is Library. It should call the others and let each make its own decision. It should not make their decisions for them.

The complete small design

java
import java.util.ArrayList;
import java.util.List;

class Book {
    private final String title;
    private boolean available = true;
    Book(String title) { this.title = title; }
    String title() { return title; }
    boolean isAvailable() { return available; }
    void markLent() {
        if (!available) throw new IllegalStateException(title + " is already lent");
        available = false;
    }
    void markReturned() { available = true; }
}

// Loan as above

class Member {
    static final int MAX_LOANS = 2;
    private final String name;
    private final List<Loan> loans = new ArrayList<>();
    Member(String name) { this.name = name; }
    String name() { return name; }
    boolean canBorrow() { return loans.size() < MAX_LOANS; }
    void addLoan(Loan loan) { loans.add(loan); }
}

class Library {
    Loan lend(Member member, Book book, int today) {
        if (!member.canBorrow()) {
            throw new IllegalStateException(member.name() + " has reached the loan limit");
        }
        book.markLent();
        Loan loan = new Loan(book, today + 14);
        member.addLoan(loan);
        return loan;
    }
}

Running a short main that lends Dune on day 1, asks for the fine on day 18, then lends two more books to the same member:

text
fine on day 18: 75
Lina has reached the loan limit

The due day is 15, three days late at 25 per day gives 75. The third lend is refused by Member, and Library simply passes on the failure.

One reason to change

Robert C. Martin states the single-responsibility principle as "a class should have only one reason to change". In practice: if the fine rate changes, one class should be affected. If the loan limit changes, one class should be affected. Loan changes when loan rules change, Member when membership rules change, Library when the steps of lending change. In the next lesson we will see what happens when one class has many reasons.

Where the rule has limits

  • Two classes hold parts of the information. "Can this member borrow this book?" depends on the member's loans and on whether the book is reference-only. Do not force it into one class: ask each (member.canBorrow(), book.isAvailable()) and let the coordinator combine the answers.
  • Not every job belongs in the domain classes. Printing, e-mail, and file saving are not about loans. They get their own classes. The next lesson covers that.
  • The rule does not say "put everything in the biggest class". If a class starts collecting jobs that use only a little of its own data, the rule has been stretched too far.

Examples

Example 1: order total (simple)

An order has lines; each line has an item, a unit price, and a quantity. Who computes the total?

  • A line knows its price and quantity, so OrderLine.subtotal() is unitPrice * quantity.
  • An order knows its lines, so Order.total() adds up the subtotals.
java
class OrderLine {
    private final String item;
    private final int unitPrice;
    private final int quantity;
    OrderLine(String item, int unitPrice, int quantity) {
        this.item = item; this.unitPrice = unitPrice; this.quantity = quantity;
    }
    int subtotal() { return unitPrice * quantity; }
}

class Order {
    private final List<OrderLine> lines = new ArrayList<>();
    void add(OrderLine line) { lines.add(line); }
    int total() {
        int sum = 0;
        for (OrderLine line : lines) sum += line.subtotal();
        return sum;
    }
}

For a pen (150 cents, four of them) and a pad (400 cents, one), total() prints 1000. No class reads another's fields.

Example 2: lending, step by step (practical)

Trace library.lend(lina, dune, 1) and see each object use its own information:

  1. Library asks lina.canBorrow(). Member counts its own loans and compares with its limit.
  2. Library calls dune.markLent(). Book checks and changes its own state, and refuses if it is already lent. This is an invariant living where the data lives.
  3. Library creates new Loan(dune, 1 + 14). The due day comes from the lending day.
  4. Library hands the loan to lina.addLoan(loan).

Library has no loan counts, no book flags, no due-day arithmetic. It only knows the order of the steps. When the loan limit changes from two to three, only Member.MAX_LOANS changes.

Example 3: a member's total fines (realistic)

The front desk wants the total fine a member owes today. Who computes it?

Option A: Library loops over member.getLoans(), calls loan.fine(today), and adds up. Option B: Member.totalFine(today) loops over its own list and sums loan.fine(today).

Option B is better. Member owns the list, so the loop belongs there, and Library does not need to know how the member stores loans. Notice the chain: Order.total() sums OrderLine.subtotal(), and Member.totalFine() sums Loan.fine(). Each level asks the level below, and nobody reaches two levels down. If the member's loans are later stored in a different structure, only Member changes.

Backend Insight

Backend services often grow a class called OrderService or AccountManager with methods that take an entity, call six getters, and compute something. Frameworks and persistence layers encourage this, because the entities look like plain data. But the rules (limits, fees, states) are still the heart of your system, and scattering them across services makes them hard to find and to test. A rule on the class that owns the data can be tested with a plain new Loan(...), no server and no database needed. Later modules and the DDD course (MID-060) take this idea further.

Common Mistakes

Mistake 1: A manager that does all the work

  • Incorrect: LoanManager.fine(loan, today) calling loan.getDueDay().
  • Correct: loan.fine(today).

Mistake 2: Getters for everything "just in case"

A getter for every field lets every other class compute with your data. Expose answers to questions, not the raw data, unless a real caller needs it.

Mistake 3: Putting the job in the class that calls it

Library.lend is the place where you notice that a member must have a limit. That does not make the limit Library's job. The information belongs to Member.

Mistake 4: Moving everything into the domain classes

Printing a receipt is not a Loan job. Responsibilities that need other information (screen, file, network) deserve their own class.

Mistake 5: Hiding rules in the coordinator

If Library.lend contains if (member.loanCount() >= 2), the number 2 now lives in two places.

Exercises

Try each exercise yourself before opening the solutions.

Level A: Foundation

A1. State the rule for assigning a responsibility in one sentence.

أظهر الحل A1

A1. Give the job to the class that has the information needed to do it.

A2. Book has a title and an available flag. Which class should own isAvailable(): Book, Library, or Main?

أظهر الحل A2

A2. Book, because it holds the flag. Library would have to read the book's state to answer.

A3. What is the warning sign when ClassA repeatedly calls three getters of ClassB before computing something?

أظهر الحل A3

A3. The computation is probably in the wrong class. It should move into ClassB (or the class that owns the data).

Level B: Application

B1. A class Report has String lateLine(Loan loan, int today) that builds text using loan.getDueDay() and today. Which part of it is the loan's own business? Describe how to change it.

أظهر الحل B1

B1. Deciding whether and how late a loan is belongs to Loan: add isOverdue(today) and lateDays(today) and remove the getter. Report still builds text but only from the answers: loan.fine(today).

B2. For each, name the owner among Loan, Member, Book, Library: (a) "How many days late am I?" (b) "Which of my loans are overdue?" (c) "Mark me as returned." (d) "Process a return: update the book and close the loan."

أظهر الحل B2

B2. (a) Loan. (b) Member, which owns the list, asking each loan isOverdue. (c) Book (markReturned). (d) Library, which coordinates book and loan.

B3. An Account has balance. A BankService has canWithdraw(Account a, int amount) that returns a.getBalance() >= amount. Rewrite so the account answers the question.

أظهر الحل B3

B3.

java
class Account {
    private int balance;
    boolean canWithdraw(int amount) { return balance >= amount; }
}

BankService calls account.canWithdraw(amount). The getter is no longer needed for this purpose.

Level C: Challenge

C1. The library adds "reference books cannot be borrowed". Who owns the rule? Describe the code changes and what Library.lend looks like afterwards.

أظهر الحل C1

C1. The information "this book is reference-only" belongs to the book, so Book gets a flag and isLendable(). Book.markLent refuses a reference book. Library.lend is unchanged, because it already asks the book to do the marking, and the rule is guarded in one place. If Library checked the flag itself, another caller could bypass it.

C2. A teammate says: "My LoanManager is fine; the code is all in one place, so it is easy to find." Give one fair point in its favour and two reasons the responsibility design is better.

أظهر الحل C2

C2. A fair point: it is easy to see all the rules in one file. Better: first, Loan is just data, so its rule is not protected and any other caller can compute fines differently. Second, the manager needs a getter for every value it uses, which widens what the rest of the program can see. Third, Loan cannot be tested on its own with its rule. Any two of these are a fine answer.

Mini Challenge

Take the library from this lesson and add a feature: a member may renew a loan once, which adds 14 days to its due date. In a short note:

  1. List who needs which information to decide this.
  2. Assign renew to a class and justify it.
  3. Name the invariant ("at most one renewal") and where it lives.
  4. Sketch the method signatures (not the code).

Check your work: no class reads another class's field to make a decision, and the coordinator only calls methods.

Summary & Next Lesson

Key takeaways

  • Give each behavior to the class that has the information it needs.
  • A class that keeps pulling data out of another with getters is a warning sign.
  • Computing belongs to the data owner; coordinating belongs to a class that connects owners.
  • A rule should live in one place, so a change touches one class.
  • Not every job belongs in the domain classes; sending e-mail and saving files get their own.

Next lesson

Next is "Recognizing and splitting god classes". We will see what happens when responsibilities all go to one class, how to recognize it, and how to split it safely.

Self-check

  1. Why does Loan.fine(today) belong in Loan?
  2. What is the difference between computing and coordinating?
  3. What problem does an anemic design cause?
  4. Where should the loan limit live: Library or Member? Why?
  5. What does "one reason to change" mean?
أظهر الإجابات
  1. Loan holds the due day, which is the information the fine needs.
  2. Computing uses the object's own data; coordinating calls several objects in order and lets each decide.
  3. The data and the rules on it are separate, so the rules scatter, and the data owner cannot guard them.
  4. Member, which owns the loans and knows the limit. Then a change touches one class.
  5. A class should be affected by one kind of change only, such as changes to fine rules.

References

  • Wikipedia, GRASP (object-oriented design)). Supports the Information Expert statement (assign responsibility to the class that has the information to fulfil it), and the low coupling and high cohesion guidelines.
  • Martin Fowler, Anemic Domain Model. Supports the description of domain classes with little behavior and the objection that it separates data from behavior.
  • Wikipedia, Single-responsibility principle. Supports the statement "a class should have only one reason to change" and its attribution to Robert C. Martin.

التطبيق

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

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

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

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