AEM開発者ブログ by YAMATO

アドビ社のデリバリーパートナー大和株式会社のAEM開発者ブログです。

大和公式サイトのヘッダ開発を通してコンポーネントのバックエンド部分の開発をやってみる

AEM Developerの皆様お疲れさまです。大和株式会社の狩野です。

Qiitaのアドベントカレンダーに便乗して始めた記事投稿イベント最終日です。
今日最終日の内容は、「大和公式サイトのヘッダ開発を通してコンポーネントのバックエンド部分の開発をやってみる」です。

12日目の記事→HTL vs JSP

今回は、実際にコンポーネント開発の流れを実際に踏襲してみます。
何をコンポーネントにするかというのは、大和公式サイトのヘッダー部分をAEMで開発してみるという手法を取ります。
実際はAEMで開発しているわけではない(Wordpress)ですが、機会があってAEMで同じものを開発したことがあったので、今回はその時にどういう過程を踏んだのかというのを紹介したいと思います。

注意しておいてほしいのですが、今回の私が行った実装が必ず絶対100%正しいとは言い切れません。
また、現状は正しかったとしても、時の流れや技術のトレンドの変化によって正しくなくなることもあります。

ヘッダーが何で構成されているか

大和公式サイトのヘッダーは以下の要素で構成されています。

f:id:yamato_tech:20201225162645p:plain
大和公式サイトヘッダー部分

  • ロゴ画像
  • ナビゲーション
    • 子供のパスをバルーンで出すか出さないか
    • 子供のページ群
    • ルートページ(ナビゲーションに直接貼られているページ)
    • ナビゲーションのラベル

厳密にはもう少し要素ごとに分解できますが、今回はこの程度の要素分けで実装します。

※右側の「採用情報」のボタン部分は別のコンポーネントを配置するのでここでは考慮しません

今回はこの「ナビゲーション部分」の実装について考えます。

各要素を実装する

メリット

上の各要素を1個1クラスで実装します。
1要素1クラス定義することによるメリットは以下です。

  • クラス名を見ただけでこのファイルが何を実装しているかがわかる
    • 要素の名前を適切に付ける必要はあります
  • 結合度が下がる
    • 1つ1つが単体で動くという実装のため、それぞれが結合している必要がなくなります
  • 凝集度が上がる
    • 凝集≒1つのクラスに関連するロジックが集まっているということ
  • 再利用が容易
    • 小さい単位でクラスを作成しているので再利用しやすい
  • 不正値の混入を防ぐ
    • 不正値が入った時点で例外を送出することができる
  • 単体テストが容易
    • 動作の最小単位でクラスを作っているので簡単

なお、この手法はドメイン駆動設計という設計手法で使われる「値オブジェクト(Value Object)」という概念を参考にしています。
私はドメイン駆動設計についてすごく勉強したという訳でもなく、ドメイン駆動設計で使われている他の要素を取り入れていないので、もしかしたらドメイン駆動設計の理念からすると反することをやっている可能性はありますが、今回はひとまずそこは目をつむらせてください。

実際のクラスと実装

クラス

  • ShowChild
    • 子供のパスを表示するかしないか
    • ダイアログでON/OFFを設定する
  • RootPath
    • ナビゲーションに表示するページのURL
    • バルーンを出す場合、ナビゲーションに表示しているパスの子供のページをバルーンに表示するため、そのルートパスという意味としています
  • Label
    • ナビゲーションに表示するラベル
    • ページのタイトル
  • Pages
    • ナビゲーションにオンマウスすると表示されるRootPathの子供のページたち

実装

実際に私が行った実装を以下に載せます。

public class ShowChild {
    private final boolean _needBalloon;

    public ShowChild(Resource resource) {
        _needBalloon = resource.getValueMap().get("showChild", false);
    }

    public boolean value() {
        return _needBalloon;
    }
}
public class RootPath {
    private final String _rootPath;

    public RootPath(Resource resource) {
        _rootPath = resource.getValueMap().get("naviRoot", "/content/yamato/jp/ja");
    }

    @Override
    public String toString() {
        return _rootPath;
    }
}
public class Label {
    private final String _label;

    public Label(Page page) {
        _label = page.getTitle();
    }

    @Override
    public String toString() {
        return _label;
    }
}
public class Pages {
    private final List<Page> _pages;

    @SuppressWarnings("unchecked")
    public Pages(Page rootPage) {
        _pages = rootPage.listChildren() == null ? Collections.emptyList() : IteratorUtils.toList(rootPage.listChildren());
    }

    public List<Page> toList() {
        return _pages;
    }
}

Sling Models

上記で作成したクラスをHTL上に出力できるようにSling Modelsを作成します。
このSling Modelsは以下の役割を持ちます。

  • HTLと、上で作った1要素ごとのクラス(値クラス)をつなげる
  • currentResource から値クラスのインスタンスを作成する

以下のような実装としました。

@Model(
    adaptables = {SlingHttpServletRequest.class},
    defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL
)
public class Header {
    @SlingObject
    private Resource currentResource;

    @SlingObject
    private ResourceResolver resourceResolver;

    private List<NavigationItem> navigations = new ArrayList<>();

    @PostConstruct
    public void init() {
        if (currentResource.hasChildren()) {
            currentResource.getChild("items").getChildren().forEach(
                resource -> navigations.add(generateNavigationItem(resource))
            );
        }
    }

    // アクセス修飾子がprotectedなのは、同じような役割を持つFooterクラスで使い回すため
    protected NavigationItem generateNavigationItem(Resource resource) {
        final PageManager pm = resourceResolver.adaptTo(PageManager.class);
        RootPath rootPath = new RootPath(resource);
        Page rootPage = pm.getPage(rootPath.toString());
        return new NavigationItem(new ShowChild(resource), rootPath, new Label(rootPage), new Pages(rootPage));
    }


    public List<NavigationItem> getNavigations() {
        return navigations;
    }

    public boolean isEmpty() {
        return navigations.isEmpty();
    }
}

その他書ききれない部分について

NavigationItem クラスや単体テストについては今回重要ではないと思ったので省きました。
今回の記事を通して知ってもらいたかったのは、「コンポーネント要素ごとにクラスを作る」という部分だったからなので、それらの部分はあまり重要ではないと判断しました。

また、テストは後で書いたのでテスト駆動開発を行ってません。
このへんも少し詰めが甘かったので今後の課題ですね。

想定される疑問とその答え

Q: わざわざこんなに小さいクラスをたくさん作らなくてもSling Models(Header.java)上で全部の処理を書ききれるのでは?
A: そのとおりですが、Sling Models上に書く処理が多すぎた場合、以下のような問題が生じます

  • 不正値の混入
  • 変更があった時に変更箇所の特定が難しい
  • 動作が独立していないため1箇所を変えた時に他の箇所の動作が変わっていないことを保証できない

今回の場合は処理が難しくないた全部Sling Models上に書いたとしても多分そんなに大きな問題では無いですが、もっと複雑なコンポーネントを作る場合は値クラスなどを使って処理を分けた方がメンテナンスなどの観点から見るとベターだと思っています。

Q: こんな方法よりも、自分はもっと優れた手段を知っている
A: 素晴らしい。ぜひ教えて下さい。

最後に

以上となります。
このアドベントカレンダーイベントも最終日となりましたが、この機会に開発者ブログを書いてみて思ったのは、意外と書こうと思えば記事を書くことは難しくないということですね。
これからもここまで高頻度とはいかないですが、記事を更新していきたいと思います。

年内の大和開発者ブログの更新は最後となります。皆様、良いお年を。