Class Names That Reveal Responsibility Scope
Class Names That Reveal Responsibility Scope
The Smell
The logistics platform has a class named ShipmentManager. It has 3,400 lines, 47 public methods, and 23 injected dependencies. What does it manage? Everything. Rate calculation, carrier assignment, status tracking, label generation, billing event creation, customer notification, and audit logging. The name Manager is a blank check. It permits any responsibility because “managing” can mean anything.
The platform also has DataHelper, ShipmentUtils, CommonService, and OrderProcessor. Each name signals the same problem: the class has no bounded responsibility because the name does not constrain it.
A search across the logistics codebase finds:
| Suffix | Count | Average method count |
|---|---|---|
| Manager | 8 | 31 |
| Helper | 12 | 18 |
| Utils | 6 | 22 |
| Processor | 9 | 14 |
| Handler | 14 | 11 |
Classes with Manager in the name average 31 public methods. Classes with domain-specific names like RateCalculator, CarrierGateway, or TrackingEventPublisher average 6 public methods. The suffix predicts the responsibility sprawl.
The Cognitive Cost
When a developer sees ShipmentManager in an import statement, they learn nothing about why the importing class depends on it. The import could be for rate calculation, tracking, labeling, or any of the other 44 public methods. The developer must open ShipmentManager and search for the specific method being called. They then mentally bookmark: “okay, this class uses ShipmentManager for label generation specifically.”
When a developer sees ShipmentLabelGenerator in an import statement, they know immediately: this class generates shipment labels. No bookmark needed. No file opened. One import, one concept, zero working memory consumed.
The difference compounds. A class with six imports from vaguely named classes requires six mental bookmarks. A class with six imports from precisely named classes requires zero.
The Before
// HARD TO READ: The name "ShipmentManager" permits any responsibility.
// A reader cannot predict what any method does from the class name alone.
public class ShipmentManager {
// Dependencies from 5 different subdomains
private final ShipmentRepository shipmentRepo;
private final CarrierRepository carrierRepo;
private final RateRepository rateRepo;
private final WarehouseRepository warehouseRepo;
private final CustomerRepository customerRepo;
private final BillingService billingService;
private final NotificationService notificationService;
private final LabelService labelService;
private final AuditService auditService;
private final TrackingService trackingService;
// ... 13 more dependencies
public ShipmentRate calculateRate(Shipment s, Carrier c) { /* 60 lines */ }
public void assignCarrier(Long shipmentId, Long carrierId) { /* 45 lines */ }
public byte[] generateLabel(Long shipmentId, LabelFormat fmt) { /* 80 lines */ }
public void updateStatus(Long id, ShipmentStatus status) { /* 90 lines */ }
public void processTrackingEvent(TrackingEvent event) { /* 70 lines */ }
public Invoice generateInvoice(Long shipmentId) { /* 55 lines */ }
public void notifyCustomer(Long shipmentId, String template) { /* 40 lines */ }
public List<Shipment> findByDateRange(LocalDate from, LocalDate to) { /* 20 lines */ }
public ShipmentReport generateReport(ReportCriteria c) { /* 100 lines */ }
// ... 38 more public methods
}
The Fix
// READABLE: Each class name describes exactly what it does.
// A reader seeing any of these in an import statement knows the dependency reason.
public class ShipmentRateCalculator {
private final CarrierRateRepository carrierRates;
private final SurchargePolicy surchargePolicy;
public ShipmentRate calculate(Shipment shipment, Carrier carrier) { /* ... */ }
public ShipmentRate recalculate(Shipment shipment) { /* ... */ }
}
public class CarrierAssigner {
private final CarrierRepository carriers;
private final CarrierSelectionPolicy selectionPolicy;
public CarrierAssignment assign(Shipment shipment) { /* ... */ }
public CarrierAssignment reassign(Shipment shipment, String reason) { /* ... */ }
}
public class ShipmentLabelGenerator {
private final LabelTemplateRepository templates;
private final BarcodeEncoder barcodeEncoder;
public ShipmentLabel generate(Shipment shipment, LabelFormat format) { /* ... */ }
}
public class ShipmentStatusTracker {
private final ShipmentRepository shipments;
private final TrackingEventPublisher events;
public StatusTransition updateStatus(Shipment shipment,
ShipmentStatus newStatus) { /* ... */ }
public void processCarrierEvent(TrackingEvent event) { /* ... */ }
}
Each class has two to four public methods. Each class has two to three dependencies. Each class name communicates its entire responsibility scope. When another class imports ShipmentRateCalculator, the reader knows the dependency exists for rate calculation. No file needs to be opened. No method needs to be searched.
The names also create natural boundaries. A developer asked to add a fuel surcharge knows it belongs in ShipmentRateCalculator. A developer asked to support a new label format knows it belongs in ShipmentLabelGenerator. The name guides the developer to the right file without needing to search the codebase.
Contrast this with ShipmentManager. Where does the fuel surcharge go? Probably in ShipmentManager, next to the other 46 methods. Where does the new label format go? Also ShipmentManager. The name provides no guidance. Every new feature makes the class larger because no name boundary exists to suggest a different location.
The Rule
If a class name ends in Manager, Helper, Utils, or Processor, it is a design smell, not a name. Rename the class to describe the specific thing it does, not the category of thing it is. A class that calculates rates is a RateCalculator. A class that generates labels is a LabelGenerator. A class that tracks shipment status is a StatusTracker. If the class does too many things to fit in a specific name, the class needs to be split, not renamed. The name is the diagnostic. Apply this test: if someone adds a new method to this class, can they use the class name to decide whether the method belongs here? If the name permits any method, the name is wrong.