Classes
Everything earlier in this course was about writing a good line, then a good function. None of that matters if the classes those functions live in are a mess. This page is about the next level up: how a class should be organized, how big it should be, and why "small" means something different for a class than it did for a function.
Class organization
A class reads top to bottom like a short document. Constants first, then static variables, then instance variables, then the public methods, with each public method followed by the private helpers it calls. A reader who starts at the top and moves down should never have to scroll back up to understand what they just read.
Keep variables and helper methods private by default - a public variable is rare enough that one showing up is worth a second look. Loosen that only when something concrete needs it, like a test in the same package reaching for a helper, and treat that as a last resort rather than a habit.
Classes should be small
Functions are measured in lines. Classes are measured in responsibilities, and that number is almost always smaller than people expect.
A class with five methods can still be too big if those five methods do unrelated things. A class with seventy methods can, in principle, be fine if all seventy serve one purpose (though that's rare in practice). The line count is a distraction; the question is always "how many reasons does this class have to change?"
A good gut check: write one sentence describing what the class does, and ban the words "and",
"or", "but", and "if" from that sentence. If you can't avoid them, the class is doing more than
one job. Class names are a related tell. Manager, Processor, Super, and similar
vague nouns tend to show up on classes that accumulated responsibilities nobody assigned on
purpose.
Single responsibility, applied to classes
A class should have one reason to change. Reading and converting a temperature and printing a report are three different reasons: a new sensor protocol, a new unit system, and a new report layout each touch a different concern, so bundling them means any one of those changes risks breaking the other two.
The natural worry is that more classes means more to navigate. It doesn't add up that way in practice - a system built from many small, clearly labeled classes has the same amount of logic as one built from a few sprawling ones, but you only ever have to open the drawer you actually need. A toolbox with fifty labeled compartments beats one drawer you dump everything into.
Cohesion
A class is cohesive when its methods actually use its instance variables. A class where every
method touches every field is maximally cohesive; a stack with push, pop, and size
built around one elements list and one topOfStack counter is a good example, since almost
every method needs both fields.
Watch for the opposite: a handful of instance variables that only three of your fifteen methods ever touch. That's usually a second class trying to escape the first one. This shows up naturally when you break up a long function - if the piece you're extracting needs four local variables, promoting them to instance variables lets you extract a method with no parameters, but it also means the class picks up fields that only that one extracted method cares about. When you notice a subset of fields and methods only talking to each other, that subset wants to be its own class.
Organizing for change: the open/closed principle
The DailyReport class above will eventually need to support more than one output format.
Left alone, that means reopening render() and adding another if branch every time a new
format shows up, which puts a well-tested method at risk on every feature request.
Pulling the varying part behind a ReportFormatter interface means a new format is a new
class, and DailyReport itself never changes again. That's the open/closed principle: open to
new behavior through extension, closed to edits in code that already works.
None of this is a reason to build the interface before a second format actually shows up - the moment to split is when you're about to reopen a class that already works, not out of fear that you might have to someday.
Isolating from change: depend on abstractions
The same idea applies to dependencies, not just behavior. If DailyReport constructs a
concrete BluetoothSensor itself, every test of DailyReport now depends on real hardware
being reachable, and swapping in a different sensor means editing the report class.
DailyReport now depends on a TemperatureSensor abstraction it owns, and any concrete
sensor - Bluetooth, a lab rig, or a fake that returns a fixed value for tests - can be handed
in from outside. This is the dependency inversion principle: depend on the shape of the thing
you need, not on one specific implementation of it. It's what makes the class testable without
touching real hardware, and it's the same shape as the Shipping and Database examples from
the SOLID principles page, just applied at the level of a whole class's dependencies rather
than one method.