First some context - these named refactorings (from Refactoring by Martin Fowler) aren't necessarily meant to tell you what's better design-wise, they are just things that you can do to transform your code in a stepwise fashion (without breaking it) on your way to the design you are after. Many refactorings in this catalogue have a dual refactoring that does the exact opposite (though not this particular one, I believe). All of them have pros and cons. When the book was written, you'd do these transformations manually; today, you have tools that can do many popular refactorings with one click. You may apply various refactorings to arrive to a design you already know you want, but their non-destructive nature also allows you to try out a bunch of different things in hopes of sort of stumbling into a better design, assuming the current one is not working for you.
That said, you generally want to minimize the number of places where you are repeating code that should be kept in sync. Assuming you can confine any computation that actually uses name
, age
and other student fields to showStudentInfo(Student student)
or similar methods, then such methods will likely be called in more places compared to the number of direct accesses to name
and age
(which would happen inside these methods). So if you need to modify the Student
class, there are only a few places that need to be updated, compared to what you'd have to do if you had to update every call site.
Another advantage is that you're more closely modeling what the actual function is supposed to represent ("show info for this student"). Also, the design is kind of constrained in a way that makes the function easy to use correctly and hard to use incorrectly. While showStudentInfo(Student student)
only accepts student objects, showStudentInfo(String name, int age)
is a more open-ended method that you can call with arbitrary parameters: showStudentInfo("Porsche 911", 8)
. That's fun (unless you do it by accident), but such lower-level methods belong somewhere deeper in the code, where they'd perhaps serve a more generic purpose, with the appropriate change in the name showBasicInfo(String name, int age)
, or something like that.
Having showStudentInfo(Student student)
helps insure correctness to some extent, and in a compiled language, the compiler will prevent you from doing silly things. So this particular refactoring makes sense in the higher level code, but for the lower level, more generic code, you might decide against it.
Now, your Student
class is really just a data structure, but in OOP, the object that you preserve as a whole to pass in as a parameter might have behaviors associated with it, so you wouldn't necessarily be accessing any fields on it at all (it might not even provide that option), you'd be delegating work to it by calling some of its methods. So there are designs that use this technique that are potentially very different than what you were envisioning.
Also, going down this path is not the only option. There could be cases where you don't want to pass individual low-level parameters, cases where you still want the correctness-friendly kind of design discussed above, but you also don't want to pass the entire Student object (maybe it doesn't make conceptual sense, maybe you don't want to allow access to all features of the class, etc). So you introduce a new concept - maybe an independent new type that you can create from a student instance, or maybe you introduce a base class that Student
will derive from, so that you can make use of polymorphism.
Or I misunderstood the term "hidden dependency" (which I think "hiddendependency" of a method means classes that don't appear directly inparameter list of a method that the method requires)?
Software design is about battling complexity, about minimizing the number of things that you have to keep in mind while looking at a piece of code within a particular context. Passing in a whole object is more about "I don't have to think about the details of that function while I'm looking at the code that calls it". As in, you know what that function does at a high level (the same way you know what some library function does, without knowing how it does it), you know where all the inputs are coming from, and you know what sorts of output you can expect. showStudentInfo(Student student)
takes in a student and shows the student info; no surprises there1, even though you don't know what it does with the student object internally.
A hidden global dependency is more like "something unexpected happened when I made the call; suddenly this other 'unrelated' thing over there doesn't work, and now I have to fire up the debugger and hope I'm lucky enough for the bug to happen again, so I can figure out what on Earth is going on". Or maybe these other parts of the system appear to be fine, but the function that you called sometimes doesn't work correctly, apparently without rhyme or reason.
In other words, a function accessing a global is the same as it having an additional non-obvious input value that you yourself didn't supply at the call site, that can change in unpredictable ways, affecting the behavior of the callee, and possibly of other parts of the system that are accessing (and maybe modifying) the same global.
1"no surprises there" - well, almost. The "show" in the name implies the call will produce some output to the screen, probably via the standard output (like Console.WriteLine
in C# or System.out.println
in Java), and these are globals, with a global state. This is often of no concern, but it can make it difficult get the desired behavior in the terminal if you're trying to do something more complex than sequential printouts.