Loading learning content...
A class is more than a bag of loosely related methods. In well-designed systems, methods work together harmoniously—each method has a clear purpose, methods support each other, and together they fully express what the class can do. This quality is called cohesion.
Cohesive methods are like musicians in an orchestra: each plays their part, and together they create something greater than the sum of individual contributions. Incohesive methods are like a random group playing different songs—technically functional but chaotic and hard to follow.
By the end of this page, you will understand the principles of method cohesion, know how to design methods that are focused and purposeful, recognize the signs of poorly designed methods, and create classes whose methods work together as a unified whole.
Cohesion measures how strongly related the elements within a module (class, method, or function) are to each other. High cohesion means elements belong together—they work toward a common purpose. Low cohesion means elements are loosely related or unrelated—they happen to be in the same container but don't form a coherent unit.
At the method level, cohesion asks:
At the class level, cohesion asks:
| Cohesion Type | Description | Quality |
|---|---|---|
| Functional | All elements contribute to a single, well-defined task | Best ✓ |
| Sequential | Output of one element feeds into the next | Good |
| Communicational | Elements operate on the same data | Acceptable |
| Procedural | Elements are grouped because they happen in sequence | Weak |
| Temporal | Elements are grouped because they execute at the same time | Weak |
| Logical | Elements are grouped by category but do different things | Poor |
| Coincidental | Elements have no meaningful relationship | Worst ✗ |
Can you describe what a method does in a single sentence without using 'and' or 'or'? If not, the method may be doing too many things. 'This method validates the order AND sends a confirmation email' suggests two responsibilities.
The Single Responsibility Principle (SRP) applies not just to classes but to methods. Each method should have one reason to change—one focused purpose.
Signs a Method Has Multiple Responsibilities:
// Step 1: Validate, // Step 2: Process, // Step 3: Save123456789101112131415161718192021222324252627282930313233343536373839
// BEFORE: Low cohesion - multiple responsibilities in one methodpublic void processOrder(Order order) { // Responsibility 1: Validation if (order.getItems().isEmpty()) { throw new ValidationException("Order must have items"); } if (order.getCustomer() == null) { throw new ValidationException("Order must have a customer"); } for (Item item : order.getItems()) { if (item.getQuantity() <= 0) { throw new ValidationException("Invalid quantity"); } } // Responsibility 2: Pricing calculations double subtotal = 0; for (Item item : order.getItems()) { subtotal += item.getPrice() * item.getQuantity(); } double tax = subtotal * 0.08; double shipping = calculateShipping(order); double total = subtotal + tax + shipping; order.setTotal(total); // Responsibility 3: Inventory update for (Item item : order.getItems()) { inventoryService.decrementStock(item.getProductId(), item.getQuantity()); } // Responsibility 4: Persistence orderRepository.save(order); // Responsibility 5: Notification emailService.sendOrderConfirmation(order); // Responsibility 6: Analytics analyticsService.trackOrderPlaced(order);}123456789101112131415161718192021222324252627282930
// AFTER: High cohesion - each method has one purposepublic void processOrder(Order order) { validate(order); calculatePricing(order); reserveInventory(order); save(order); notifyAndTrack(order);} private void validate(Order order) { orderValidator.validate(order); // Dedicated validator} private void calculatePricing(Order order) { PricingResult pricing = pricingService.calculate(order); order.applyPricing(pricing);} private void reserveInventory(Order order) { inventoryService.reserve(order.getItems());} private void save(Order order) { orderRepository.save(order);} private void notifyAndTrack(Order order) { eventPublisher.publish(new OrderPlacedEvent(order)); // Listeners handle email, analytics, etc.}Notice how the refactored code reads like prose: validate, calculate, reserve, save, notify. Each line is at the same level of abstraction. The details are pushed into helper methods or dedicated services. This is the 'Newspaper' metaphor: headlines at the top, details below.
While there's no magic number, method length and complexity are strong signals of cohesion.
Guidelines for Method Length:
Cyclomatic Complexity:
Cyclomatic complexity counts the number of independent paths through a method. Each if, else, for, while, case, catch, and &&/|| adds a path.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152
// BEFORE: High complexity - many nesting levelspublic double calculateDiscount(Customer customer, Order order) { double discount = 0; if (customer != null) { if (customer.getMembershipLevel() != null) { if (customer.getMembershipLevel() == MembershipLevel.GOLD) { discount = 0.15; } else if (customer.getMembershipLevel() == MembershipLevel.SILVER) { discount = 0.10; } else if (customer.getMembershipLevel() == MembershipLevel.BRONZE) { discount = 0.05; } } if (order.getTotal() > 100) { discount += 0.02; } if (isHolidaySeason()) { discount += 0.05; } } return discount;} // AFTER: Reduced complexity - guard clauses, extracted methodspublic double calculateDiscount(Customer customer, Order order) { if (customer == null) return 0; // Guard clause double discount = getMembershipDiscount(customer); discount += getLargeOrderDiscount(order); discount += getSeasonalDiscount(); return discount;} private double getMembershipDiscount(Customer customer) { MembershipLevel level = customer.getMembershipLevel(); if (level == null) return 0; return switch (level) { case GOLD -> 0.15; case SILVER -> 0.10; case BRONZE -> 0.05; };} private double getLargeOrderDiscount(Order order) { return order.getTotal() > 100 ? 0.02 : 0;} private double getSeasonalDiscount() { return isHolidaySeason() ? 0.05 : 0;}Method names are the primary documentation for what a method does. A well-named method doesn't need comments—its name explains its purpose.
Naming Principles:
calculateTotal, sendEmail)sendOrderConfirmationEmail over sendsortByDate over quickSortByDateFieldisValid, hasItems, canProceedfetch for one retrieval, use it everywheregetX if it modifies state| Pattern | Convention | Examples |
|---|---|---|
| Getters | getPropertyName() | getBalance(), getName(), getItems() |
| Setters | setPropertyName(value) | setBalance(amount), setStatus(status) |
| Boolean queries | isCondition(), hasProperty() | isActive(), isEmpty(), hasPermission() |
| Actions | verbNoun() | sendEmail(), createOrder(), deleteUser() |
| Conversions | toFormat(), asType() | toString(), toJson(), asList() |
| Computations | calculateX(), computeX() | calculateTotal(), computeHash() |
| Factory methods | createX(), of(), from() | createDefault(), of(value), fromString() |
| Finders | findByX(), getByX() | findByEmail(), getById() |
The worst methods are those with misleading names. A 'getUser' that creates users on cache miss. A 'validate' that also saves. A 'toString' that has side effects. If the name doesn't match the behavior, either rename the method or fix the behavior.
The Intention-Revealing Name:
A method's name should reveal its intention—why you would call it, not just what it does.
Compare:
process(data) — Process how? Why would I call this?calculateMonthlyPayment(loan) — Clear purpose, clear inputCompare:
handleEvent(event) — What does 'handle' mean?updateInventoryOnPurchase(event) — Exactly what happens and whenMethod parameters significantly impact cohesion. Too many parameters often indicate a method doing too much.
Parameter Count Guidelines:
Why Many Parameters Hurt Cohesion:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950
// BEFORE: Too many parameterspublic void scheduleDelivery( String customerId, String address, String city, String zipCode, LocalDate preferredDate, TimeSlot preferredTime, boolean requiresSignature, boolean isFragile, String specialInstructions) { // Implementation} // AFTER: Parameter object encapsulates related datapublic void scheduleDelivery(DeliveryRequest request) { // Implementation} public class DeliveryRequest { private final String customerId; private final Address address; // Groups address components private final DeliveryWindow window; // Groups date/time private final DeliveryOptions options; // Groups flags // Builder pattern for flexible construction public static Builder builder(String customerId) { return new Builder(customerId); } public static class Builder { private String customerId; private Address address; private DeliveryWindow window; private DeliveryOptions options = DeliveryOptions.defaults(); public Builder address(Address address) { ... } public Builder window(DeliveryWindow window) { ... } public Builder options(DeliveryOptions options) { ... } public DeliveryRequest build() { ... } }} // Usage is now self-documenting:scheduleDelivery(DeliveryRequest.builder("CUST123") .address(new Address("123 Main St", "Springfield", "62701")) .window(DeliveryWindow.afternoon(LocalDate.of(2024, 1, 15))) .options(DeliveryOptions.fragile().requiresSignature()) .build());Boolean parameters are particularly problematic: scheduleDelivery(..., true, false) tells readers nothing. Either use a parameter object with named fields, or split into multiple methods: scheduleFragileDelivery(), scheduleStandardDelivery().
Cohesive methods don't work in isolation—they collaborate to express the class's full capabilities. A well-designed class has methods that complement each other.
Patterns of Method Collaboration:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172
public class ShoppingCart { private List<CartItem> items = new ArrayList<>(); private DiscountStrategy discountStrategy; // PUBLIC API - High-level operations public void addProduct(Product product, int quantity) { validateQuantity(quantity); CartItem item = findExistingItem(product) .map(existing -> existing.addQuantity(quantity)) .orElseGet(() -> createNewItem(product, quantity)); if (!items.contains(item)) { items.add(item); } recalculateTotals(); // Maintain invariant } public OrderSummary checkout() { validateForCheckout(); OrderSummary summary = buildOrderSummary(); clear(); // Reset state return summary; } // QUERY METHODS - Used by commands public boolean isEmpty() { return items.isEmpty(); } public int getTotalItemCount() { return items.stream().mapToInt(CartItem::getQuantity).sum(); } // PRIVATE HELPERS - Support public methods private Optional<CartItem> findExistingItem(Product product) { return items.stream() .filter(item -> item.getProduct().equals(product)) .findFirst(); } private CartItem createNewItem(Product product, int quantity) { return new CartItem(product, quantity); } private void validateQuantity(int quantity) { if (quantity <= 0) { throw new IllegalArgumentException("Quantity must be positive"); } } private void validateForCheckout() { if (isEmpty()) { throw new IllegalStateException("Cannot checkout empty cart"); } } private void recalculateTotals() { // Called after any change to maintain consistency } private OrderSummary buildOrderSummary() { double subtotal = calculateSubtotal(); double discount = discountStrategy.calculate(this); return new OrderSummary(items, subtotal, discount); } private double calculateSubtotal() { return items.stream() .mapToDouble(CartItem::getSubtotal) .sum(); }}Notice how the methods form an ecosystem: public methods for external use, private helpers for internal composition, query methods for state inspection, and validation methods for constraint enforcement. Each has its role, and together they express what a ShoppingCart can do.
Learning to spot incohesive methods is a critical skill. Here are common patterns and smells:
1234567891011121314151617181920212223242526272829303132333435363738394041
// SMELL: Flag-driven behaviorpublic void save(Entity entity, boolean validate, boolean notify) { if (validate) { // 20 lines of validation } // Save logic if (notify) { // 15 lines of notification }}// FIX: Separate methods, compose as neededpublic void save(Entity entity) { /* save only */ }public void saveWithValidation(Entity entity) { validate(entity); save(entity); }public void saveAndNotify(Entity entity) { save(entity); notify(entity); } // SMELL: Mixed abstraction levels public void processOrder(Order order) { // High level validateOrder(order); // Suddenly low level String sql = "INSERT INTO orders (id, customer_id) VALUES (?, ?)"; PreparedStatement stmt = connection.prepareStatement(sql); stmt.setString(1, order.getId()); // ... more JDBC code // Back to high level sendConfirmation(order);}// FIX: Keep abstraction consistentpublic void processOrder(Order order) { validateOrder(order); orderRepository.save(order); // Repository handles JDBC sendConfirmation(order);}We've explored the principles and practices of method cohesion. Let's consolidate:
Module Complete:
You've now completed the Attributes and Methods module. You understand:
With these foundations, you're ready to explore more advanced topics: constructors and object lifecycle, access modifiers, static vs instance members, and ultimately, how to design clean classes that embody these principles.
Congratulations! You've mastered the fundamental building blocks of classes: attributes and methods. You can now design classes with meaningful attributes and cohesive methods that create maintainable, understandable object-oriented systems.