Struktuurmustrid

Fassaad (Facade pattern)

Fassaadmustri mõte on peita keerulisi implementatsioonidetaile fassaadi taha, millega on lihtsam ümber käia.

Fassaad on tõenäoliselt üldiseim muster, mida see leht käsitleb. Suures osas ongi juba programmide kirjutamine olemuselt väikeste, konkreetsete ja detailsete tükkide kokku pakkimine suuremateks, abstraktsemateks, kergemini mõistetavateks ning probleemi olemusele lähedasemateks komponentideks.

Seetõttu võib mõnes mõttes öelda, et loomulik koodi kirjutamine ongi pidev fassaadimustri rakendamine.

Näide

Ütleme, et tahame interneti kaudu veebilehtede sisu pärida. Java standardteek võimaldab meil suhelda teiste arvutitega kasutades HTTP protokolli:

public class App {
    public static void main(String[] args) throws IOException {
        var url = new URL("https://www.example.com");
        var connection = (HttpURLConnection) url.openConnection();
        connection.setRequestMethod("GET");

        var in = new BufferedReader(new InputStreamReader(connection.getInputStream()));
        String inputLine;
        StringBuilder response = new StringBuilder();
        while ((inputLine = in.readLine()) != null)
            response.append(inputLine);
        in.close();

        System.out.println(response);
    }
}

Warning

Tõenäoliselt on reaalsuses parem mõte kasutada mõnda kasutajasõbralikumat teeki, kui programmis on sageli vaja HTTP päringuid sooritada

Asi töötab:

<!doctype html><html><head>    <title>Example Domain</title>    <meta charset="utf-8" />    <meta http-equiv="Content-type" content="text/html; charset=utf-8" />    <meta name="viewport" content="width=device-width, initial-scale=1" />    <style type="text/css">    body {        background-color: #f0f0f2;        margin: 0;        padding: 0;        font-family: -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;            }    div {        width: 600px;        margin: 5em auto;        padding: 2em;        background-color: #fdfdff;        border-radius: 0.5em;        box-shadow: 2px 3px 7px 2px rgba(0,0,0,0.02);    }    a:link, a:visited {        color: #38488f;        text-decoration: none;    }    @media (max-width: 700px) {        div {            margin: 0 auto;            width: auto;        }    }    </style>    </head><body><div>    <h1>Example Domain</h1>    <p>This domain is for use in illustrative examples in documents. You may use this    domain in literature without prior coordination or asking for permission.</p>    <p><a href="https://www.iana.org/domains/example">More information...</a></p></div></body></html>

Ent mis siis, kui tahame kahte lehte pärida?

public class App {
    public static void main(String[] args) throws IOException {
        var url = new URL("https://www.example.com");
        var connection = (HttpURLConnection) url.openConnection();
        connection.setRequestMethod("GET");

        var in = new BufferedReader(new InputStreamReader(connection.getInputStream()));
        String inputLine;
        StringBuilder response = new StringBuilder();
        while ((inputLine = in.readLine()) != null)
            response.append(inputLine);
        in.close();

        System.out.println(response);

        var url2 = new URL("https://www.example.com");
        var connection2 = (HttpURLConnection) url2.openConnection();
        connection2.setRequestMethod("GET");

        var in2 = new BufferedReader(new InputStreamReader(connection2.getInputStream()));
        String inputLine2;
        StringBuilder response2 = new StringBuilder();
        while ((inputLine2 = in2.readLine()) != null)
            response2.append(inputLine2);
        in.close();

        System.out.println(response2);
    }
}

Kood töötab, ent ilmselgelt see pole hästi kirjutatud:

  • Kood on sisuliselt loetamatu, ning ei kirjelda kuidagi soovitud tulemusi

  • Kui midagi peaks päringu juures muutma, peaks seda muutma mitmes kohas

  • Koodi on sisuliselt võimatu elegantselt taaskasutada

Anname parem päringu tegemisele meetodi kaudu fassaadi:

public class App {
    public static void main(String[] args) throws IOException {
        var exampleDotComContent = getBody("https://www.example.com");
        var exampleDotNetContent = getBody("https://www.example.net");
        System.out.println(exampleDotComContent);
        System.out.println(exampleDotNetContent);
    }

    private static String getBody(String urlString) throws IOException {
        var url = new URL(urlString);
        var connection = (HttpURLConnection) url.openConnection();
        connection.setRequestMethod("GET");

        var in = new BufferedReader(new InputStreamReader(connection.getInputStream()));
        String inputLine;
        StringBuilder response = new StringBuilder();
        while ((inputLine = in.readLine()) != null)
            response.append(inputLine);
        in.close();

        return response.toString();
    }
}

Selline lähenemine leevendab kõik eelmainitud mured. Antud juhul on getBody meetod fassaad keerulisemale HTTP päringu sooritamise allsüsteemile.

Fassaadiga kaetud allsüsteem võib olla kuitahes keeruline ning võib koosneda kas klassidest või funktsioonidest. Fassaadmuster pole ainuüksi objektorienteeritud koodis esinev muster.

Siin on veel paar näidet fassaadidest, mis loodetavasti aitavad võimalikke kasutusjuhte ette kujutada:

var videoConverter = new VideoConverter();
videoConverter.convert("./clip.mp4").to("mov");
var image = new Image("./mountain.png");
image.resize(0.8);
image.flip();
image.redChannel().amplify(0.2);
image.convertTo(ImageFormat.JPG);
var canvas = new Canvas(1200, 800);
canvas.draw(new Circle(new Point(0, 0), 24, Color.RED));
canvas.draw(new Square(new Point(400, 400), 18, Color.BLUE));
canvas.draw(new Line(new Point(200, 200), new Point(300, 300), Color.BLACK));
canvas.display();

Adapter

Kui rakenduse osad ei sobi omavahel kokku, võib olla kasu adapteri disainimustrist. Adapteri kaudu konverteeritakse komponendi algne liides teiseks liideseks läbi vahepealse Adapter objekti.

Note

Adapterid aitavad järgida põhimõtet, et kood peaks üldjuhul sõltuma liidetest, mitte konkreetsetest implementatsioonidest (levinud ingl. k. käsklause program to interfaces).

Näide

Note

Näidet saad lähemalt vaadata ja käima panna siin salves. Näide asub adapter-factory moodulis.

Ütleme, et meil on programm, mis tagastab kasutaja soovil kas koera- või kassipilte. Selle teostamiseks kasutame kahte API-t:

Note

Võid neid URL-e oma veebilehitsejas külastada, et nende vastuseid näha

Programm võiks töötada niiviisi:

Would you like a dog or cat image? dog
https://images.dog.ceo/breeds/poodle-medium/WhatsApp_Image_2022-08-06_at_4.48.38_PM.jpg
Would you like a dog or cat image? cat
https://cdn2.thecatapi.com/images/5kr.jpg
Would you like a dog or cat image? poleolemas
No image provider configured for type poleolemas
Would you like a dog or cat image?

Ütleme, et nende piltide pärimiseks on meil juba olemas eraldi klassid:

public class CatImageProvider {
    private final Retrofit retrofit = new Retrofit.Builder()
            .baseUrl("https://api.thecatapi.com/v1/")
            .addConverterFactory(JacksonConverterFactory.create())
            .build();
    private final CatImageService catImageService = retrofit.create(CatImageService.class);

    public CatImage getCatImage() throws IOException {
        return catImageService.getRandomCatImages(1).execute().body().get(0);
    }

    public List<CatImage> getCatImages(int count) throws IOException {
        return catImageService.getRandomCatImages(count).execute().body();
    }

    private interface CatImageService {
        @GET("images/search")
        Call<List<CatImage>> getRandomCatImages(@Query("limit") int count);
    }
}
public class CatImage {
    @JsonProperty("id")
    public String id;
    @JsonProperty("url")
    public URL url;
    @JsonProperty("width")
    public int width;
    @JsonProperty("height")
    public int height;
}
public class DogImageProvider {
    private final Retrofit retrofit = new Retrofit.Builder()
            .baseUrl("https://dog.ceo/api/")
            .addConverterFactory(JacksonConverterFactory.create())
            .build();
    private final DogImageService dogImageService = retrofit.create(DogImageService.class);

    public DogImage getDogImage() throws IOException {
        return dogImageService.getRandomDogImage().execute().body();
    }

    private interface DogImageService {
        @GET("breeds/image/random")
        Call<DogImage> getRandomDogImage();
    }
}
public class DogImage {
    @JsonProperty("status")
    public String status;
    @JsonProperty("message")
    public URL imageUrl;
}

Pärimise detailid pole olulised - oluline asi mida tähele panna on see, et kuna kasutame koerte ja kasside jaoks erinevaid allikaid, on ka päringute vastused pisut erinevad (vt. klassid DogImage ja CatImage).

Kuidas võiks programmi põhiloogika välja näha? Me tahame, et programmi kulg oleks midagi sellist:

  • Küsi kasutajalt looma tüüp

  • Tee vastav päring

  • Kuva päringu tulemusest pildi URL

Siin on esimene naiivne lahendus:

public class App {
    public static void main(String[] args) {
        var scanner = new Scanner(System.in);
        while (true) {
            System.out.print("Would you like a dog or cat image? ");
            var animalType = scanner.nextLine().toLowerCase();
            try {
                switch (animalType) {
                    case "dog" -> System.out.println(new DogImageProvider().getDogImage().imageUrl);
                    case "cat" -> System.out.println(new CatImageProvider().getCatImage().url);
                    default -> System.out.printf("No image provider configured for type %s%n", animalType);
                }
            } catch (IOException e) {
                System.out.println("Something went wrong when fetching your image.");
            }
        }
    }
}

Ent see koodijupp teeb liiga paljut. Pane järgmisi asju tähele:

  • vastusest URLi leidmiseks peame erinevaid välju (imageUrl ja url) kasutama. See on vihje sellele, et me ei programmeeri liidestele.

  • koodijupp peab õige loomapildi pärimiseks valima õige AnimalImageProvider klassi.

Sisuliselt on programmi töö praegu selline:

  • Küsi kasutajalt looma tüüp

  • Vali päringu tegemiseks õige klass (üleliigsus nr 1)

  • Tee vastav päring

  • Kaeva välja päringu tulemusest pildi URL (üleliigsus nr 2)

  • Kuva päringu tulemusest pildi URL

Ent me tahame, et kasutajalt loomaliigi küsimise ning URL vastuse andmise loogika oleks muust programmist võimalikult eraldiseisev. Praegu me rikume single responsibility printsiipi, kuna programmi kasutamise loogikat koormab päringute tegemise loogika. Kui peaksime lisama kalapiltide funktsionaalsuse, rikuksime ka open-closed printsiipi.

Esimest viga parandame tehase meetoditega. Teist viga saame parandada kasutades adapteri mustrit. Loome liidese, mis on probleemile vastav. Me tahame ju alati päringust URLi saada:

public interface ImageProvider {
    URL getImageURL() throws IOException;
}

Nüüd saame CatImageProvider ja DogImageProvider klassid viia üle sellele liidesele kasutades adaptereid:

public class DogImageProviderAdapter implements ImageProvider {
    private final DogImageProvider dogImageProvider;

    public DogImageProviderAdapter(DogImageProvider dogImageProvider) {
        this.dogImageProvider = dogImageProvider;
    }

    @Override
    public URL getImageURL() throws IOException {
        return dogImageProvider.getDogImage().imageUrl;
    }
}
public class CatImageProviderAdapter implements ImageProvider {
    private final CatImageProvider catImageProvider;

    public CatImageProviderAdapter(CatImageProvider catImageProvider) {
        this.catImageProvider = catImageProvider;
    }

    @Override
    public URL getImageURL() throws IOException {
        return catImageProvider.getCatImage().url;
    }
}

Nüüd ei pea programmi põhivoos me enam URLi taga otsima, vaid saame alati selle ühtsest liidesest kätte:

// ...
switch (animalType) {
    case "dog" -> System.out.println(new DogImageProviderAdapter(new DogImageProvider()).getImageURL());
    case "cat" -> System.out.println(new CatImageProviderAdapter(new CatImageProvider()).getImageURL());
    default -> System.out.printf("No image provider configured for type %s%n", animalType);
}
// ...

Ükskõik kui palju loomatüüpe me ka ei lisaks, ning ükskõik kui mitmel erineval viisil nende piltide pärimine ka ei toimuks, on siin koodi osas liides nende saamiseks alati getImageURL tänu adapteritele.