简体   繁体   中英

How to map objects to helper classes using polymorphism?

I want to replace a switch statement with polymorphism. Let's take the example of a PostOffice . This post office sends Letter 's and Package 's, which are both subclasses of Mail . There are specific ways to send different types of Mail , so there is a LetterService and PackageService , both of which are MailService 's

public class PostOffice {

    @Inject
    private LetterSender letterSender;

    @Inject
    private PackageSender packageSender;

    public void send( Mail mail ) {
        if ( mail instanceof Letter ) {
            letterSender.send( (Letter) mail );
        } else if ( mail instanceof Package ) {
            packageSender.send( (Package) mail );
        }
    }

}

How can I avoid the conditional and instanceof? I've been told that you can remove these using polymorphism, but I still don't understand how to "route" the correct type of Mail to the correct MailSender .

According to the actual logic, LetterSender and PackageSender does probably have two distinct methods with each one a distinct parameter. For the first one :

 public void send(Letter letter);

And for the second one :

 public void send(Package letter);

To benefit from the polymorphism you should define a common method defined in an interface that these two classes implement. For example :

public interface MailSender{
   void send(Mail mail);
}

But in Java, parameters are not covariant for overriding . So you could not implement the interface by subtyping the Mail parameter.
So it means that you have to implement void send(Mail mail) in the two Sender classes such as :

public class LetterSender implements MailSender{
   @Override
   public void send(Mail mail){
      // ...
   }
}  

public class PackageSender implements MailSender{
   @Override
   public void send(Mail mail){
      // ...
   }
}  

To achieve it you should define Mail from a high level point of view where you define behaviors/methods required for any Mail subclasses.
Each Mail subclass would define the implementation for them.
And so the two sender implementations could process send(Mail mail) without needing to downcast the parameter.

This sort of problem can be solved in a few different ways. The simplest version is a Chain of Responsibility:

interface Sender {
    boolean canSend(Mail mail);
    void send(Mail mail);
}
...
List<Sender> senders;
...
senders.stream()
    .filter(s -> s.canSend(mail))
    .findAny()
    .ifPresentOrElseThrow(
        s -> s.send(mail),
        () -> new SomethingException()
    );

You could use the Visitor pattern for this.

Define an interface MailVisitor as:

public interface MailVisitor {

    void visitLetter(Letter letter);
    void visitPackage(Package package);
}

Implement this interface in MailSender:

public class MailSender implement MailVisitor {
     @Override
     public void visitLetter(Letter letter) {//letter sending goes here}
     @Override
     public void visitPackage(Package package) {//package sending goes here}
}

Now in the PostOffice class you let the MailSender take a visit to the mail package that just arrived:

public class PostOffice {

    @Inject
    private MailSender mailSender;

    public void send(Mail mail) {
        mail.visit(mailSender);
    }
}

The visit method is implemented as following:

public abstract class Mail {

    public abstract void visit(MailVisitor visitor);
}

public class Letter extends Mail {

    public void visit(MailVisitor visitor) {
        visitor.visitLetter(this);
    }
}

public class Package extends Mail {

    public void visit(MailVisitor visitor) {
        visitor.visitPackage(this);
    }
}

It took me a while to fully grasp how this worked the first time I encountered it. But it's a very powerful design pattern, which allows you to eliminate every instanceof + cast operation.

The biggest advantage of this is that when you define a new Mail subclass, let's say AirMail. The compiler will force you to implement the visit(MailVisitor visitor) method. Which will automatically make you define a new method on the MailVisitor. And this will in turn obligate you to implement that new method in the MailSender class. So your code will not compile until you have defined logic that is able to handle your newly created subtype. Whereas if you had used an if statement, you could have simply forgotten to add a new branch for AirMail, which would leave your application unable to send any Mail in need of airplane transportation :)

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM