اكتشاف الفئات الضخمة (god classes) وتقسيمها
في هذا الدرس 11 موضوعات
هذا الدرس متاح حاليًا باللغة الإنجليزية فقط، ولم تُنشر نسخته العربية بعد.
Lesson Overview
In the last lesson you gave each behavior to the class that has its information. What happens when you skip that and give every job to one class? You get a god class: one class that knows too much and does too much, and that every other part of the program depends on.
God classes are rarely designed on purpose. They grow. A project starts with a LibraryManager that lends books; then somebody needs to send an e-mail, so a method is added there; then a receipt; then saving to a file. Each addition is the easy choice for that day. This lesson teaches you to see the pattern, to name its signs, and to split a god class into focused classes without breaking the program.
In the staffing a small shop picture, the god class is the owner who also cooks, serves, does the accounts, and cleans. The shop works until the owner is ill, or until anyone wants to change the menu without breaking the accounts.
Everything here was compiled and run with JDK 21. This lesson builds on lesson 2, on interfaces (BEG-024), and on classes and encapsulation (BEG-020, BEG-021).
Learning Objectives
By the end of this lesson you will be able to:
- Define a god class and list its common signs.
- Apply the "and test" to a class description.
- Explain why a god class makes a program hard to change and test.
- Split a god class into focused classes in small steps that keep the program working.
- Recognize the opposite problem, the anemic class, and avoid over-splitting.
- Decide when a large class should not be split.
Written Explanation
What a god class is
A god class (also called a god object) is a class that references a large number of distinct types, has too many unrelated methods, or both. Wikipedia describes it as an anti-pattern: one "all-knowing" object holds most of the data and methods, and other code depends on it for nearly everything. The same article quotes a heuristic: be very suspicious of an abstraction whose name contains Driver, Manager, System, or Subsystem. The reason is practical, not stylistic: a change made for one purpose can ripple into unrelated ones.
Refactoring guides list the common signs of a large class as many fields, many methods, and many lines. They also note the cause: classes start small, and developers add new features to an existing class because it is easier than creating a new one. The god class is the sum of many easy choices.
Signs
| Sign | What to look for |
|---|---|
| The "and" test | Describing the class needs "and": "it lends books and prices fines and sends e-mails and saves files" |
| Unrelated fields | Some methods use only fields A and B, others only C and D |
| Many reasons to change | A change to the e-mail text, the fine rate, or the file format all edit this one class |
| Many dependencies | Imports from unrelated areas: files, e-mail, formatting |
| A vague name | Manager, Util, Helper, Processor, System |
| Everyone depends on it | Every feature touches it, so merging changes is painful |
Size alone does not prove it. A class with several hundred lines but one clear job can be fine. A class with sixty lines and four jobs is a problem.
Why it hurts
- Change ripples. A bug fix for fines can break the receipt, because both live in the same class and share fields.
- Testing is hard. To test the fine you must create a whole
LibraryManager, with its list, its notifier, and its file. - Teams collide. Everybody edits the same file.
- Reuse is impossible. You cannot reuse the fine rule in another program without dragging the e-mail code along.
How to split one, safely
Do not rewrite. Split in small steps, and after every step the program must compile and give the same output as before. This is refactoring: improving structure without changing behavior.
- Record the behavior. Run the program and save the output. It is your safety net.
- List the responsibilities. Use the "and" test: pricing, notification, storage, lending.
- Group the data. Which fields belong with which responsibility?
- Extract one responsibility at a time, into a class with a name that says its job:
FinePolicy,LoanStore. - Hide the old details behind an interface where the job may vary. A notifier (BEG-024) is a good example, since e-mail today may be SMS tomorrow.
- Leave a thin coordinator. The original class keeps only the sequence of steps and delegates the work.
- Compare the output after each step.
The opposite mistake: anemic and over-split
The reverse of the god class is the anemic design from the last lesson: data-only classes and all logic elsewhere. A related trap is splitting too enthusiastically: FineCalculatorFactory, FineCalculatorImpl, and FineCalculatorHelper for what was one method. Each class should have a reason to exist: its own rule, its own data, or a variation you really have. If a "class" has one method that reads one other object's field, it probably belongs back in the owner.
Rule of thumb: split by reason to change, not by number of lines.
A trade-off to notice
In the last lesson, the fine was a method of Loan. In the split below it becomes a separate FinePolicy. Both are defensible. The policy version suits a library where fines may differ by member type or book category, and change for business reasons independently of what a loan is. The Loan version suits a library that will never change its single rate. A design is a series of such choices, and lesson 4 shows how to explain them.
Examples
Example 1: the "and" test on three classes (simple)
| Description | Verdict |
|---|---|
"Loan records who has which book and when it is due." | One job. Fine as is. |
"ReportUtil formats dates, parses CSV files, and calculates tax." | Three unrelated jobs and a vague name. Split. |
"Member stores a name and the loans and knows if the limit is reached." | The jobs belong together (membership). Fine. |
Example 2: a library god class (practical)
This LibraryManager lends books, calculates fines, "sends" e-mail, prints receipts, and "saves to a file" (a StringBuilder stands in for the file here to keep the example small).
class LibraryManager {
private final List<String> titles = new ArrayList<>();
private final List<String> borrowers = new ArrayList<>();
private final List<Integer> dueDays = new ArrayList<>();
private final StringBuilder file = new StringBuilder();
void lend(String title, String borrower, int today) {
titles.add(title);
borrowers.add(borrower);
dueDays.add(today + 14);
System.out.println("email to " + borrower + ": " + title + " is due on day " + (today + 14));
file.append(title).append(',').append(borrower).append('\n');
}
int fineFor(String title, int today) {
int i = titles.indexOf(title);
int late = today - dueDays.get(i);
return late > 0 ? late * 25 : 0;
}
void printReceipt(String title, int today) {
int i = titles.indexOf(title);
System.out.println("RECEIPT " + borrowers.get(i) + " " + title + " fine=" + fineFor(title, today));
}
String saved() { return file.toString(); }
}Running m.lend("Dune", "Lina", 1); m.printReceipt("Dune", 18); System.out.print(m.saved()); gives the behavior we must preserve:
email to Lina: Dune is due on day 15
RECEIPT Lina Dune fine=75
Dune,LinaSigns: three parallel lists that must always stay in step (a loan scattered across three fields, with no Loan class), the "and" test fails (lending, pricing, e-mail, receipts, saving), a vague name, and four reasons to change. The String parameters also hide types: nothing stops you passing the borrower where the title is expected.
Example 3: the split (realistic)
Extract in this order. Each step compiled and gave the same three lines.
- A
Loanclass replaces the parallel lists: it holds title, borrower, and due day. FinePolicygets the fine rule.Notifier, an interface, withConsoleNotifierimplementing it, replaces the print of the e-mail.LoanStoretakes the file writing.Libraryis what is left: it keeps the loans and calls the others.
class FinePolicy {
private static final int PER_DAY = 25;
int fine(int dueDay, int today) {
int late = today - dueDay;
return late > 0 ? late * PER_DAY : 0;
}
}
interface Notifier { void send(String to, String message); }
class ConsoleNotifier implements Notifier {
public void send(String to, String message) {
System.out.println("email to " + to + ": " + message);
}
}
class LoanStore {
private final StringBuilder file = new StringBuilder();
void save(String title, String borrower) {
file.append(title).append(',').append(borrower).append('\n');
}
String contents() { return file.toString(); }
}
class Library {
private final List<Loan> loans = new ArrayList<>();
private final FinePolicy fines;
private final Notifier notifier;
private final LoanStore store;
Library(FinePolicy fines, Notifier notifier, LoanStore store) {
this.fines = fines; this.notifier = notifier; this.store = store;
}
void lend(String title, String borrower, int today) {
Loan loan = new Loan(title, borrower, today + 14);
loans.add(loan);
notifier.send(borrower, title + " is due on day " + loan.dueDay());
store.save(title, borrower);
}
String receipt(String title, int today) {
for (Loan loan : loans) {
if (loan.title().equals(title)) {
return "RECEIPT " + loan.borrower() + " " + title
+ " fine=" + fines.fine(loan.dueDay(), today);
}
}
throw new IllegalArgumentException("no loan for " + title);
}
}(Loan is a plain holder of three fields and three getters: it keeps the data together for now, and the next refactoring, as in lesson 2, would move behavior into it and replace the strings with Book and Member.) The three-line output is identical after the split.
What did we gain? A change to the fine rate edits FinePolicy only. To send SMS instead of e-mail, write a new Notifier: Library does not change, which is the "program to an interface" idea from BEG-024. A test for FinePolicy needs no library at all. Library still builds the receipt text, and that is acceptable for now. If receipts grow, they are the next candidate for extraction: you will know because the "and" test will say so.
Backend Insight
In backend projects the god class is often called OrderService, UserManager, or Utils, with hundreds of lines and a dozen injected dependencies. Teams usually find it when a change in one feature breaks another, or when its test needs a long setup. The split in this lesson is what experienced engineers do routinely: pull out pricing, notifications, and persistence into separate classes behind interfaces, so a database or mail provider can be replaced. Splitting is also what makes code reviewable: a reviewer can understand a thirty-line FinePolicy at a glance.
Common Mistakes
Mistake 1: Splitting by size instead of by responsibility
- Incorrect: cutting a 400-line class into
Manager1,Manager2, andManager3at line 133. - Correct: separate it by jobs: pricing, notification, storage.
Mistake 2: Rewriting everything at once
A big-bang rewrite cannot be checked. Extract one class, compile, compare output, continue.
Mistake 3: Moving the mess instead of removing it
Moving all code to LibraryHelper and calling it from LibraryManager gives you two god classes. Each new class must have a clear, single job.
Mistake 4: Over-splitting into tiny classes
A class per method creates indirection and files without value. Split when a class has a different reason to change.
Mistake 5: Making fields public to ease the split
Making fields public or adding getters for everything so the new classes can reach them rebuilds the same coupling. Pass what is needed, or move the behavior with its data.
Exercises
Try each exercise yourself before opening the solutions.
Level A: Foundation
A1. What is a god class? Name two of its signs.
أظهر الحل A1
A1. A god class is one that knows or does too much: many unrelated responsibilities, fields, or dependencies. Signs: a vague name such as Manager; it needs "and" to describe; unrelated groups of fields; many reasons to change.
A2. Apply the "and" test: "SchoolManager stores students, computes grades, prints report cards, and sends e-mails to parents." How many responsibilities do you find?
أظهر الحل A2
A2. Four: student data, grading, printing report cards, and e-mailing parents.
A3. True or false: a class with 800 lines is always a god class. Explain.
أظهر الحل A3
A3. False. Size is a hint, not proof. A long class with one cohesive job may be fine. What counts is the number of unrelated responsibilities.
Level B: Application
B1. Propose a split for the SchoolManager in A2. Give each new class a name and one sentence about its job, and say which class remains as coordinator.
أظهر الحل B1
B1. For example: Student (name and data), GradeBook or GradeCalculator (computes grades), ReportCardPrinter (builds the text), ParentNotifier (an interface for e-mail), and School remains as a thin coordinator. Other names are fine if each has one job.
B2. Why is Notifier better as an interface than a concrete EmailSender field in the coordinator?
أظهر الحل B2
B2. With an interface, the coordinator depends only on send(to, message). You can swap e-mail for SMS or a test double without editing the coordinator, and the coordinator does not need e-mail details.
B3. List the steps you take, in order, before and during extracting FinePolicy from LibraryManager.
أظهر الحل B3
B3. (1) Run the program and save its output. (2) List the responsibilities. (3) Find the fine arithmetic and the data it uses. (4) Create FinePolicy and move the arithmetic. (5) Call it from the old class. (6) Compile and compare the output. (7) Remove the now-unused code.
Level C: Challenge
C1. After splitting, a teammate created FineCalculatorFactory, FineCalculatorImpl, and FineCalculatorHelper, each with one short method. Evaluate this and suggest what to do.
أظهر الحل C1
C1. It is over-splitting. Three classes for one short rule add indirection and no new reason to change. Merge them into FinePolicy (or back into Loan) until a real variation appears.
C2. A 300-line PriceCalculator has one job (pricing) but several long methods for tax, discounts, and rounding. Should you split it into a god-class fix? Argue both sides briefly.
أظهر الحل C2
C2. For splitting: tax and discount rules change for different reasons and could be separate. Against: all share one job, and splitting only for size adds indirection. A sensible answer is to keep one class, order its methods, and extract only when two rules start changing independently or one needs its own tests.
Mini Challenge
Write a short god class on paper for a pizza shop with methods for taking an order, computing the price, printing the kitchen ticket, and saving the daily sales. Then:
- Apply the "and" test and list the responsibilities.
- Name the new classes and say what each owns.
- Decide which one should be an interface because it may vary, and say why.
- Write the order of your refactoring steps.
Check your work: no new class has a vague name, and the coordinator contains no pricing arithmetic.
Summary & Next Lesson
Key takeaways
- A god class has many unrelated responsibilities and everyone depends on it. It usually grows by many easy additions.
- Signs: the "and" test, unrelated fields, many reasons to change, a vague name.
- Split in small steps, keeping the output unchanged, and leave a thin coordinator.
- Use interfaces where the job may vary.
- Avoid the opposite: anemic data classes and tiny classes with no reason to exist.
Next lesson
Next is "Explaining a design". You have made decisions (the fine as a policy, the notifier as an interface). Now you learn to present them to a reviewer: the problem, the classes, the trade-offs, and the alternatives you rejected.
Self-check
- What is the "and" test?
- Name two problems a god class causes.
- Why must the output stay the same after each extraction step?
- What remains in the original class after the split?
- What is over-splitting?
أظهر الإجابات
- Describe the class in a sentence. If it needs several "and"s, it has several responsibilities.
- Any two: changes ripple, testing is hard, teams collide, reuse is impossible.
- It shows that you changed only the structure, not the behavior, so a mistake is caught at once.
- A thin coordinator that calls the extracted classes in sequence.
- Creating classes with no reason to exist, such as a class per method, which adds indirection without value.
References
- Wikipedia, God object. Supports the definition (many distinct types or unrelated methods), the anti-pattern status, the caution about names such as Manager, and the fix of dividing responsibilities into smaller classes with a coordinator.
- Refactoring Guru, Large Class. Supports the signs (many fields, methods, lines), the cause (adding to an existing class is easier), and Extract Class as treatment.
- Martin Fowler, Anemic Domain Model. Supports the description of the opposite problem, data-only classes with logic in services.
- Wikipedia, Single-responsibility principle. Supports splitting by reason to change.
التطبيق
طبّق هذا الدرس بمختبر الوحدة وتمارينها واختبارها.
تعليم الدرس كمكتمل اختياري ويمكنك التراجع عنه في أي وقت. يبقى تقدمك محفوظًا في هذا المتصفح.