First, lets understand what was the issue:
The Staple job knows about many more methods which it doesn't use today, (e.g., the print
method) but which are available for it to use, should it want to.
However, the Job class "thinks" that the Staple class will be "a good citizen" and never use the print method at all.
There are many potential big issues here -
For some reason, the Staple job may start using the print method - by accident or intentionally.
Then down the road, either any changes to the print method may go untested,
OR, any changes to the print method will trigger a regression test in the Staple job also,
AND, any impact analysis for changes to the print job will necessarily involve impact analysis of the staple job too.
This is just the issue of Staple knowing about the print functions. Then there's the case of the Print job knowing all about stapling functions. Same problems.
Very soon, this system would reach a point where any change will require a full blown impact analysis of each module and a full blown regression test.
Another problem is that today, all jobs which can be printed can be stapled, and vice versa on this particular printer.
However, tomorrow, there could be a need to install the same firmware on a device that only prints or only staples. What then? The code already assumes that all Jobs are printable and stapleable. So any further granular breakdown / simplification of responsibilities is impossible.
In more recent terms, imagine a class called "AppleDevice" which has functions for MakePhoneCall as well as PlayMusic. Now your problem is while you can easily use this on an iPhone, you cannot use it for an iPod since the iPod cannot make phone calls.
So, the issue is not that the Job class is all-powerful. In fact, that's how it should be, so that it can act as a common link in the entire "workflow" where someone may scan a job, then print it, then staple it etc.
The problem is that the usage of all its methods is not restricted. Anyone and everyone could use and abuse any method whenever they want to, thus making the maintenance difficult.
Hence, the Dependency Injection approach of only telling users "exactly what they need to know, and nothing more" ensures that calling modules only use the code that they are meant to.
A sample implementation would look like:
interface IStapleableJob { void stapleYourself(); }
interface IPrintableJob { void printYourself(); }
class Job implements IStapleableJob, IPrintableJob {
....
}
class Staple {
public static void stapleAllJobs(ArrayList<IStapleableJob> jobs) {
for(IStapleableJob job : jobs) job.stapleYourself();
}
}
class Print {
public static void stapleAllJobs(ArrayList<IPrintableJob> jobs) {
for(IPrintableJob job : jobs) job.printYourself();
}
}
Here, even if you pass a Job object to the Staple and Print methods, they dont know that its a Job, so they cannot use any methods that they are not supposed to. Thus, when you make any changes to a module, your scope of impact analysis and regression testing is restricted. That's the problem that ISP solves.
I had to deal with this a couple times. The first time I used option 2 (events) and as you said it became really complicated. If you go that route, I highly suggest you need very thorough unit tests to make sure the events are done correctly and you're not leaving dangling references, otherwise it's a really big pain to debug.
The second time, I just implemented the parent property as a function of the children, so keep a Dirty
property on each animal, and let Animal.IsDirty
return this.Animals.Any(x => x.IsDirty)
. That was in the model. Above the model there was a Controller, and the controller's job was to know that after I changed the model (all actions on the model were passed through the controller so it knew that something had changed), then it knew it had to call certain re-evaluation functions, like triggering the ZooMaintenance
department to check if the Zoo
was dirty again. Alternatively I could just push the ZooMaintenance
checks off until some scheduled later time (every 100 ms, 1 second, 2 minutes, 24 hours, whatever was necessary).
I found the latter has been much simpler to maintain, and my fears of performance problems never materialized.
Edit
Another way of dealing with this is a Message Bus pattern. Rather than using a Controller
like in my example, you inject every object with an IMessageBus
service. The Animal
class can then publish a message, like "Mess Made" and your Zoo
class can subscribe to the "Mess Made" message. The message bus service will take care of notifying the Zoo
when any animal publishes one of those messages, and it can re-evaluate its IsDirty
property.
This has the advantage that Animals
no longer need a Zoo
reference, and the Zoo
doesn't need to worry about subscribing and unsubscribing from events from every single Animal
. The penalty is that all Zoo
classes subscribing to that message will have to re-evaluate their properties, even if it wasn't one of its animals. That may or may not be a big deal. If there's only one or two Zoo
instances, it's probably fine.
Edit 2
Don't discount the simplicity of option 1. Anyone revisiting the code won't have much problem understanding it. It'll be obvious to someone looking at the Animal
class that when MakeMess
is called that it propagates the message up to the Zoo
and it'll be obvious to the Zoo
class where it gets its messages from. Remember that in object-oriented programming, a method call used to be called a "message". In fact, the only time it makes much sense to break from option 1 is if more than just the Zoo
has to be notified if the Animal
makes a mess. If there were more objects that needed to be notified, then I would probably move to a message bus or a controller.
Best Answer
It is recursive, since the call stack needs to grow. A new call frame has to be pushed (to the same function).
If the
go
method called anotherfoo
method (even on an unrelated class) which itself calls againgo
on the same class, you would have a mutual recursion ...Another way to look at that is noticing that method invocation is just a dispatch phase (computing which function should really be called depending upon the receiver), followed by a function call (whose first argument is the reciever).
However, read about tail calls (or tail recursion). It enables to replace the current call frame with the newly called one (without growing the call stack).
So good compilers implement a tail call as a "jump with arguments"; read also about continuation passing style