views:

270

answers:

5

In virtually every project I've ever worked on, there will be one or two classes with the following properties:

  • Extremely large with many many members and methods.
  • Many other classes inheriting from this class.
  • Many other classes otherwise depending on this class.

Bad design, you might say. But in all cases, this wasn't the case at design time. The classes just grew organically over time and became the proverbial 'God class'. This has been such an invariant of my experience with large software projects that I have to ask:

  • Is it possible to foresee likely dependency magnets and design software in such a way that the chance of such classes manifesting themselves is less likely? If so, specifically, how?
  • Or, does it simply require ruthless refactoring as time goes by?
  • Or, is there some technical solution or design pattern which can mitigate the problems caused by such classes?
  • Or, a combination of all three?

Tips, experience and ideas welcomed!

+1  A: 

My experience is that the first design you come up isn't usually right and over time you need to rethink the design. When a class starts to get too large it is usually because it starts to violate SRP.

Some of Fowler's refactoring patterns are useful. The Extract Class refactoring can help, as can Extract Subclass and Extract Superclass. I often find myself changing inheritance hierarchies, perhaps using replace inheritance with delegation.

RichardOD
"When a class starts to get too large it is usually because it starts to violate SRP." -- I don't know. For example I have an 'Editor' class whose responsibility is to edit a DOM. Over time, there are more and more kinds of edits (i.e. number of public methods), and as the DOM that's being edited becomes more complicated then each edit becomes more complicated (i.e. the private methods which implement each edit become more tangled). The Editor class never violates SRP necessarily, it just grows until it's better to repackage some of its subroutines as classes or layers in their own right.
ChrisW
ChrisW, noticed I said usually and not always. Sometimes there are large classes that would be counterproductive to split up.
RichardOD
+5  A: 

Continually refactoring will help prevent this.

In addition, this is one place where forcing some amount of static code analysis can be very beneficial. You can often find these classes and flag them to refactor automatically very easily by looking at code metrics.

My biggest suggestion, though, is to keep a attitude that designs need to change, and things need to be broken apart. Often, in my experience, these classes form because people are unwilling to consider changing a broken or suboptimal design. Staying flexible makes it easier to prevent this.

Reed Copsey
A: 

Ruthless refactoring for me too, especially for small (i.e. not highly-designed, waterfall) projects which last a long time (e.g. years).

I start small: a little bit of architecture, say 2 to 5 components. I implement functionality by adding code to these components ... and when a component gets too big (which is subjective) then I divide it into more than one component. [By 'component' I mean 'package' or 'DLL' or something.]

"Dependency magnets" is a slightly different problem: to address that, I'm keen on layering software, without cyclic dependencies between layers, and sometimes with a facade which insulates high-level from low-level components.

ChrisW
+5  A: 

A lot of this, I think, tends to stem from subsequent design laziness. The philosophy of "aggressive refactoring" is a very good antidote to this sort of design problem. The issue is that while the initial design for the 'dependency magnet' classes is good, subsequent time-stressed engineers tend to simply attach other functionality to that class for the simple reason that it's available where they need it (usually).

Largely, the problem has to do with the fact that a design that can handle the needs of a mature software product is an overdesign for a less-mature software product. All of the design that ends up being in a mature iteration of a software product is massive overkill for an earlier iteration of that product; fundamentally, the design must always be fiddled with as development continues, therefore. And this is where the refactoring is necessary; as new features and functionality begin to be implemented, design problems will show their heads; as they do, it's critically important to take on the task of redesigning the way in which the classes interact, and to refactor the code to take advantage of it. Only in that way can you maintain a design maturity that is in any type of similarity with the project maturity.

Effectively, there is a level of design maturity that increases along with the project complexity; a massively complex design for a simple project is inappropriate; in the same way that a massively simple design for a complex project is inappropriate. But because that mismatch exists, the design must evolve along with the project. Refactoring as a process is simply a recognition of that necessity.

McWafflestix
+2  A: 

This one:

  • Many other classes inheriting from this class

can be overcome by preferring composition over inheritance.

Carl Manaster