模板方法模式

模板方法模式在超类中定义了一个算法的框架,允许子类在不修改结构的情况下重写算法的特定步骤。

比如我有一个爬取网页的需求,看起来的代码如下:

public class ExtractTextStrategy {

    public ExtractTextVO extractText(String url) {
        ExtractTextVO extractTextVO = new ExtractTextVO();
        extractTextVO.setUrl(url);

        // 发送 http 请求获取网页的 DOM 树
        Document document = Jsoup.parse(HttpUtils.get(url, initHeaders(url)));
        if (document == null) {
            return extractTextVO;
        }

        // 获取网页的有效内容
        extractTextVO.setContent(document.body().text());

        return extractTextVO;
    }
}

有一天,你发现,有些网页比较特殊。比如,使用 http 请求无法获取网页的 DOM 树、网页的 DOM 树结构比较复杂,获取网页的有效内容需要使用不同的算法等等。然后开始修改代码:

public class ExtractTextStrategy {

    public ExtractTextVO extractText(String url) {
        ExtractTextVO extractTextVO = new ExtractTextVO();
        extractTextVO.setUrl(url);

        // 发送 http 请求获取网页的 DOM 树
        Document document = null;
        if (url.contains("xxx")) { 
            // 使用 selenium 获取网页的 DOM 树
            document = Jsoup.parse(getHtmlBySelenium(url));
        } else {
            document = Jsoup.parse(getHtmlByHttp(url));
        }
        if (document == null) {
            return extractTextVO;
        }

        // 获取网页的有效内容
        String text = null;
        if (url.contains("aaa")) { 
            text = getTextWithGNE(document);
        } else if (url.contains("bbb")) {
            text = getTextWithGoose(document);
        } else if (url.contains("ccc")) { 
            text = getTextWithReadability(document);
        } else if (url.contains("ddd")) { 
            text = getTextWithBoilerpipe(document);
        } else if (url.contains("eee")) { 
            text = getTextWithDiffbot(document);
        } else if (url.contains("xxx")) { 
            text = getTextWithLLM(document);
        } else {
            text = document.body().text();
        }
        extractTextVO.setContent(text);

        return extractTextVO;
    }
}

随着需求不断增加,条件分支越来越多,代码会越来越长,可读性会变得很差。这时,你可以使用模板方法模式,将获取网页有效内容的算法封装成模板方法或抽象方法,让子类来实现或重写。重构后的代码如下:

  • ExtractTextStrategy.java
interface ExtractTextStrategy {
    boolean support(String url);

    ExtractTextVO extractText(String url);
}
  • AbstractExtractTextStrategy.java
public abstract class AbstractExtractTextStrategy implements ExtractTextStrategy { 

    @Override
    public ExtractTextVO extractText(String url) { 
         ExtractTextVO extractTextVO = new ExtractTextVO();
        extractTextVO.setUrl(url);

        // 获取网页的 DOM 树
        Document document = this.getDocument(url);
        if (document == null) {
            return extractTextVO;
        }

        // 获取网页的有效内容
        extractTextVO.setContent(this.extractContent(document, url));

        return extractTextVO;
    }

    protected Document getDocument(String url) {
        return this.getDocument(url, false);
    }

    protected Document getDocument(String url, boolean isSelenium) {
        Document document = null;
        if (isSelenium) { 
            document = Jsoup.parse(getHtmlBySelenium(url));
        } else { 
            document = Jsoup.parse(getHtmlByHttp(url));
        }
        return document;
    }

    protected String extractContent(Document document, String url) {
        return document.body().text();
    }
}
  • XxxExtractTextStrategy.java
@Component
public class XxxExtractTextStrategy extends AbstractExtractTextStrategy { 

    @Override
    public boolean support(String url) { 
        return url.contains("xxx");
    }

    @Override
    protected Document getDocument(String url) { 
        return super.getDocument(url, true);
    }

    @Override
    protected String extractContent(Document document, String url) { 
        return getTextWithLLM(document);
    }
}
  • AaaExtractTextStrategy.java
@Component
public class AaaExtractTextStrategy extends AbstractExtractTextStrategy { 

    @Override
    public boolean support(String url) { 
        return url.contains("aaa");
    }

    @Override
    protected String extractContent(Document document, String url) { 
        return getTextWithGNE(document);
    }
}
  • ExtractTextService.java
@Service
public class ExtractTextService { 

    @Autowired
    private List<ExtractTextStrategy> extractTextStrategies;

    public ExtractTextVO extractText(String url) { 
        for (ExtractTextStrategy extractTextStrategy : extractTextStrategies) { 
            if (extractTextStrategy.support(url)) { 
                return extractTextStrategy.extractText(url);
            }
        }
    }
}

可以发现,模板方法模式通常会结合策略模式一起使用。