Simple Factory

December 21, 2021

你想寫個 Java 程式,透過 Gmail 寄送郵件。

最初的設計

如果直接使用 Java Mail 的話,你會透過一個固定的 Gmail 帳號來發送郵件,因此大概會是長這樣的程式片段:

var props = new Properties();
props.put("mail.smtp.host", "smtp.gmail.com");
props.put("mail.smtp.auth", "true");
props.put("mail.smtp.starttls.enable", "true");
props.put("mail.smtp.port", 587);
var session = Session.getInstance(props, 
     new Authenticator() {
         protected PasswordAuthentication getPasswordAuthentication() {
             return new PasswordAuthentication(username, password);
         }
     });

var message = new MimeMessage(session);
message.setFrom(new InternetAddress(from));
message.setRecipient(Message.RecipientType.TO, new InternetAddress(to));
message.setSubject(subject);
message.setSentDate(new Date());
message.setText(text);

Transport.send(message);

建立連線 Session 當然是必要的,只不過因為你是透過固定的 Gmail 帳號來發送郵件,如果在應用程式中,每當想寄送郵件,就得寫那些建立連線 Session 的程式碼,就會形成一種重複。

關心的事?

另一方面,你真正關心的,並不是如何建立連線,撰寫程式時實際上關心的是郵件址址、內容等,既然如此,將不關心的部份,從視野中移除,就會是必要的,想這麼做的話,可以考慮如下建立 mimeMessage 方法:

public class Mail {
    public static Message mimeMessage() {
        var props = new Properties();
        props.put("mail.smtp.host", "smtp.gmail.com");
        props.put("mail.smtp.auth", "true");
        props.put("mail.smtp.starttls.enable", "true");
        props.put("mail.smtp.port", 587);
        var session = Session.getInstance(props, 
            new Authenticator() {
                protected PasswordAuthentication getPasswordAuthentication() {
                    return new PasswordAuthentication(username, password);
                }
            });
        return new MimeMessage(session);
    }
}

在這邊有個 Mail 類別,而 mimeMessage 設計為 static,這只是因為 Java 沒辦法直接定義函式,如果是在其他可以直接定義函式的語言中,或許直接定義為函式也能解決需求,在上面的片段中 Mail 只是作為一個名稱空間罷了,如果是在 Python,或許在 mail 模組中定義個 mime 函式,也可以滿足需求。

總之,如果有了以上的 Mail.mimeMessage,你就可以這麼寫程式了:

var message = Mail.mimeMessage();
message.setFrom(new InternetAddress(from));
message.setRecipient(Message.RecipientType.TO, new InternetAddress(to));
message.setSubject(subject);
message.setSentDate(new Date());
message.setText(text);

Transport.send(message);

如果你需要一個物件,建立該物件的過程繁瑣,而且也不是你關心的事情,最簡單的方式,就是如上將建立物件的過程封裝起來,最簡單的方式就是建立一個方法(函式),把不關心的事塞進去,該方法(函式)就像一個工廠,直接給你想要的物件。

這種方式很簡單,簡單到基本上每個人都會,看多了就會發現不少地方都會這麼做,形成一種模式,若要溝通方便,就將這個方式取為 Simple Factory 好了。

關注分離

簡單到這種程度,有必要特別談這個模式嗎?其實就因為簡單,也就能簡單地表現出關注分離(separation of concerns)的精神,在寫下一段程式碼時,你關心什麼?不關心什麼?是一個很好的思考方式,這個思考方式可以引導你重構,而不是想著要實現某種模式,如果重構過後的程式碼,有點像是某種模式,那也只是剛好而已!

模式中有 factory 之名的,基本上都有個目的,將物件的建立與物件的使用分離,因為物件的建立是一個值得關心的地方,然而物件的使用往往又是另一個關注的範疇,若不想要建立與使用兩種關注的邏輯被混在一起,思考的方向,往往會是 factory 的相關模式。

Simple Factory 的實際例子很多,例如 Java 的 Integer.valueOf,它可以傳回 Integer 實例,而且在一定整數範圍內,傳回的實例會是同一個,也就是說 Simple Factory 因為關注分離,可以將不關心的事隱藏起來,從而實作上就可以有很多可能性,像是 Integer.valueOf 快取一定整數範圍內的 Integer 實例,只是其中一例。

因為概念上很簡單,視語言特性或實作方式而定,Simple Factory 可以有更多變化,例如 Java 可以定義 static 方法,Simple Factory 也就常被稱為 Static Factory,Java 可以有 private 建構式,可以搭配 static 方法,達到隱藏建構式,透過 static 方法生成實例的作用,進一步地達到實現單例(singleton)、原型(prototype)、依需求傳回子類實例等效果。