A common suggestion is to put all the logic into each Pdf
object. But this requires that you extend the interface of Pdf
and amend the implementation of each Pdf
object each time want to support a new operation. It also requires that each Pdf
object has to know everything about how to operate on it, rather than just knowing how to store its own representation. The result is that you end up with giant Pdf
objects that basically have to do everything.
You probably want to use the Visitor Pattern to accomplish this.
Standard Visitor Pattern
Add a new method to your Pdf
base class:
class Pdf {
...
public abstract void AcceptVisitor(IPdfVisitor visitor);
}
The IPdfVisitor interface is fairly simple:
interface IPdfVisitor {
void Visit(FirstPdf pdf);
void Visit(SecondPdf pdf);
void Visit(ThirdPdf pdf);
}
Now, each Pdf class object you implement defines the AcceptVisitor function in the same way (but you need to write the implementation in each class).
class FirstPdf : Pdf {
...
public override void AcceptVisitor(IPdfVisitor visitor) {
visitor.Visit(this);
}
}
So, why do you have to have the implementation in each class? Because the compiler knows the static type of each class. So when you call visitor.Visit(this)
from an object of type FirstPdf
, it will call the Visit function that accepts an argument of type FirstPdf
. Now, you implement visitors for each "verb" you want to implement. Maybe you have a SavePdfVisitor
, a PrintPdfVisitor
, and a DisplayPdfVisitor
, for example. They would look like this:
class SavePdfVisitor : IPdfVisitor {
public void Visit(FirstPdf pdf) {
// FirstPdf specific save logic
}
public void Visit(SecondPdf pdf) {
// SecondPdf specific save logic
}
public void Visit(ThirdPdf pdf) {
// ThirdPdf specific save logic
}
}
Now, your generic save method for Pdfs looks like this:
public void Save(Pdf pdf) {
IPdfVisitor saveVisitor = new SavePdfVisitor();
pdf.AcceptVisitor(saveVisitor);
}
The pdf
object calls Visit
and passes itself to the save visitor, without needing to know what type of operation is being performed. And the correct method of SavePdfVisitor gets called. At a minimum, this lets you put all Save logic into a single place and use private methods for shared functionality.
One advantage of this is that if you define a new type of Pdf
, when you implement its AcceptVisitor
method, you will get a compiler error unless you add that type to the IPdfVisitor
interface and then to each of the visitor objects. This guarantees that when you define a new type of Pdf
, you don't forget to implement the logic for it for all the operations you perform on Pdf
's.
Better Visitor Pattern (In My Opinion)
On the other hand, if you have a lot of shared logic -- for example, the save operation is the same for most Pdf
types but is custom for just a few -- you can define your visitor objects using an abstract base class instead of an interface like this.
abstract class PdfVisitor {
public abstract void Visit(Pdf pdf);
public virtual void Visit(FirstPdf pdf) {
Visit((Pdf)pdf);
}
public virtual void Visit(SecondPdf pdf) {
Visit((Pdf)pdf);
}
public virtual void Visit(ThirdPdf pdf) {
Visit((Pdf)pdf);
}
}
class Pdf {
...
public abstract void AcceptVisitor(PdfVisitor visitor);
}
class FirstPdf : Pdf {
...
public override void AcceptVisitor(PdfVisitor visitor) {
visitor.Visit(this);
}
}
// Do the same for other Pdf classes
Notice that PdfVisitor
does NOT define the implementation of Visit(Pdf pdf)
. It does, however, provide a default implementation of Visit
for all the concrete implementation types. That default implementation just calls Visit(Pdf pdf)
. When you implement Visit(Pdf pdf)
in each of your concrete visitor objects, that defines the default behavior for that visitor or action. For example, suppose that your DisplayPdfVisitor
uses the same logic for every type of Pdf
. You just implement it this way:
class DisplayPdfVisitor : PdfVisitor {
public override void Visit(Pdf pdf) {
// Display logic here
}
}
On the other hand, if SavePdfVisitor
does the same thing for all Pdf
objects, except that SecondPdf
needs a special implementation, do this:
class SavePdfVisitor : PdfVisitor {
public override void Visit(Pdf pdf) {
// default save implementation
}
public override void Visit(SecondPdf pdf) {
// custom save implementation for SecondPdf type
}
}
Now if you call pdf.AcceptVisitor(saveVisitor)
on a Pdf
object of type SecondPdf
and it calls visitor.Visit(this)
, it will call the method that accepts an object of type SecondPdf
. However, the same code path for any other type of Pdf
will just call the default implementation.
The advantage of this approach is that you can define a new Pdf
operation by creating a visitor that just implements a single method and it will apply to all object types. You can also define a new type of Pdf
object and it will get default behavior on all your visitors without having to make any changes. If you create a new Pdf
class, say FourthPdf
, and you realize that you need custom logic for it in your DisplayPdfVisitor
then you just modify the base class:
abstract class PdfVisitor {
// previous stuff
public virtual void Visit(FourthPdf pdf) {
Visit((Pdf)pdf);
}
}
And then in DisplayPdfVisitor
:
class DisplayPdfVisitor : PdfVisitor {
// other implementation
public override void Visit(FourthPdf pdf) {
// custom FourthPdf display logic
}
}
You didn't have to change SavePdfVisitor
or PrintPdfVisitor
or anything else. They gladly apply the default save and print behavior to FourthPdf
objects, but DisplayPdfVisitor
handles them differently.