Atomic Designの段階の分類の指針について考えていること(2020/6/10バージョン)

概要

掲題についてだらだらと考えたことを表にまとめたのでメモに残しておきます。

大筋としてはそんなに間違えていないと思いますが、兼業フロントエンジニアなのであまり信じすぎないでください…。

前提

nuxt.js

分類とその説明

分類 該当フォルダ 説明
atoms components/atoms 原則一つのタグで表現できるもの。
標準のHTMLタグの見た目を装飾したり、
なんらかのアクションのトリガーを追加したものになる傾向が強い。
molecules components/molecules 2つ以上のタグで構成される意味のあるまとまり。
API呼び出しやvuexの更新といった外部の状態を直接操作するような振る舞いはない。
ルーセルやバリデーション込みの入力フィールドなどが該当する。
organisms components/organisms 2つ以上のタグで構成される意味のあるまとまり。
フォームやモーダル、テーブルが該当する。
コンポーネントを組合せて共闘動作させるための状態を持つことができる。
外部の状態を操作する振る舞いを持つことができる。
状態や振る舞いを持ってなくてもorganismsとして成立する
作成するコンポーネントをどれに分類するかで一番悩むもののはず。。
templates components/layouts organisms/molecules/atomsを配置する。
nuxtのlayoutsと混同しないように注意。
乱暴だが、いわゆるプレゼンテーショナルコンポーネントの考え方に近い。
単体で完結したorganismsのみで構成される場合、省略可(になるはず)
pages pages templateに、状態を流し込むのが役割。
これも乱暴だがコンテナコンポーネントの考え方に近い。
単体で完結したorganismsのみで構成される場合、
ただtemplatesを呼び出すだけになるのでその場合はtemplatesを省略可。

特徴と段階のマトリクス

項目 atoms molecules oarganisms templates pages 備考
振る舞い ない ない ある ない ある 一見、moleclesに見えても振る舞い(ビジネスロジック)があるならorganismsにする
分割可否 不可 これ以上分割できないものはatoms。
ただし、他のatomsのベースになりうるatomsはあり得るかもしれない。
例えばボタンコンポーネントに共通して備えさせたいpropsを定義するためのスーパータイプなど
標準のHTMLタグの数 原則1つ 2つ以上 2つ以上 2つ以上 2つ以上
コンポーネント組合せ数 n/a 2つ以上 2つ以上 2つ以上 2つ以上

io.lettuce.core.RedisCommandExecutionException: ERR no such key

以下のようなエラーを見かけたので調べてみた。

io.lettuce.core.RedisCommandExecutionException: ERR no such key
    at io.lettuce.core.ExceptionFactory.createExecutionException(ExceptionFactory.java:135)
    at io.lettuce.core.ExceptionFactory.createExecutionException(ExceptionFactory.java:108)
    at io.lettuce.core.protocol.AsyncCommand.completeResult(AsyncCommand.java:120)
    at io.lettuce.core.protocol.AsyncCommand.complete(AsyncCommand.java:111)
    at io.lettuce.core.protocol.CommandWrapper.complete(CommandWrapper.java:59)
    at io.lettuce.core.protocol.CommandHandler.complete(CommandHandler.java:646)
    at io.lettuce.core.protocol.CommandHandler.decode(CommandHandler.java:604)
    at io.lettuce.core.protocol.CommandHandler.channelRead(CommandHandler.java:556)
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:362)
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:348)
    at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:340)
    at io.netty.channel.ChannelInboundHandlerAdapter.channelRead(ChannelInboundHandlerAdapter.java:86)
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:362)
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:348)
    at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:340)
    at io.netty.channel.ChannelInboundHandlerAdapter.channelRead(ChannelInboundHandlerAdapter.java:86)
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:362)
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:348)
    at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:340)
    at io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1434)
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:362)
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:348)
    at io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:965)
    at io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:163)
    at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:648)
    at io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:583)
    at io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:500)
    at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:462)
    at io.netty.util.concurrent.SingleThreadEventExecutor$5.run(SingleThreadEventExecutor.java:897)
    at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
    at java.lang.Thread.run(Thread.java:834)
org.springframework.data.redis.RedisSystemException: Error in execution; nested exception is io.lettuce.core.RedisCommandExecutionException: ERR no such key
    at org.springframework.data.redis.connection.lettuce.LettuceExceptionConverter.convert(LettuceExceptionConverter.java:54)
    at org.springframework.data.redis.connection.lettuce.LettuceExceptionConverter.convert(LettuceExceptionConverter.java:52)
    at org.springframework.data.redis.connection.lettuce.LettuceExceptionConverter.convert(LettuceExceptionConverter.java:41)
    at org.springframework.data.redis.PassThroughExceptionTranslationStrategy.translate(PassThroughExceptionTranslationStrategy.java:44)
    at org.springframework.data.redis.FallbackExceptionTranslationStrategy.translate(FallbackExceptionTranslationStrategy.java:42)
    at org.springframework.data.redis.connection.lettuce.LettuceConnection.convertLettuceAccessException(LettuceConnection.java:268)
    at org.springframework.data.redis.connection.lettuce.LettuceKeyCommands.convertLettuceAccessException(LettuceKeyCommands.java:817)
    at org.springframework.data.redis.connection.lettuce.LettuceKeyCommands.rename(LettuceKeyCommands.java:325)
    at org.springframework.data.redis.connection.DefaultedRedisConnection.rename(DefaultedRedisConnection.java:118)
    at org.springframework.data.redis.core.RedisTemplate.lambda$rename$16(RedisTemplate.java:932)
    at org.springframework.data.redis.core.RedisTemplate.execute(RedisTemplate.java:224)
    at org.springframework.data.redis.core.RedisTemplate.execute(RedisTemplate.java:184)
    at org.springframework.data.redis.core.RedisTemplate.rename(RedisTemplate.java:931)
    at org.springframework.session.data.redis.RedisOperationsSessionRepository$RedisSession.saveChangeSessionId(RedisOperationsSessionRepository.java:867)
    at org.springframework.session.data.redis.RedisOperationsSessionRepository$RedisSession.saveDelta(RedisOperationsSessionRepository.java:826)
    at org.springframework.session.data.redis.RedisOperationsSessionRepository$RedisSession.access$000(RedisOperationsSessionRepository.java:703)
    at org.springframework.session.data.redis.RedisOperationsSessionRepository.save(RedisOperationsSessionRepository.java:421)
    at org.springframework.session.data.redis.RedisOperationsSessionRepository.save(RedisOperationsSessionRepository.java:247)
    at org.springframework.session.web.http.SessionRepositoryFilter$SessionRepositoryRequestWrapper.commitSession(SessionRepositoryFilter.java:240)
    at org.springframework.session.web.http.SessionRepositoryFilter$SessionRepositoryRequestWrapper.access$100(SessionRepositoryFilter.java:201)
    at org.springframework.session.web.http.SessionRepositoryFilter.doFilterInternal(SessionRepositoryFilter.java:154)
    at org.springframework.session.web.http.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:81)
    at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1610)
    at org.springframework.cloud.sleuth.instrument.web.ExceptionLoggingFilter.doFilter(ExceptionLoggingFilter.java:50)
    at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1610)
    at brave.servlet.TracingFilter.doFilter(TracingFilter.java:65)
    at org.springframework.cloud.sleuth.instrument.web.LazyTracingFilter.doFilter(TraceWebServletAutoConfiguration.java:145)
    at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1610)
    at org.eclipse.jetty.servlet.ServletHandler.doHandle(ServletHandler.java:540)
    at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:146)
    at org.eclipse.jetty.security.SecurityHandler.handle(SecurityHandler.java:566)
    at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:132)
    at org.eclipse.jetty.server.handler.ScopedHandler.nextHandle(ScopedHandler.java:257)
    at org.eclipse.jetty.server.session.SessionHandler.doHandle(SessionHandler.java:1588)
    at org.eclipse.jetty.server.handler.ScopedHandler.nextHandle(ScopedHandler.java:255)
    at org.eclipse.jetty.server.handler.ContextHandler.doHandle(ContextHandler.java:1345)
    at org.eclipse.jetty.server.handler.ScopedHandler.nextScope(ScopedHandler.java:203)
    at org.eclipse.jetty.servlet.ServletHandler.doScope(ServletHandler.java:480)
    at org.eclipse.jetty.server.session.SessionHandler.doScope(SessionHandler.java:1557)
    at org.eclipse.jetty.server.handler.ScopedHandler.nextScope(ScopedHandler.java:201)
    at org.eclipse.jetty.server.handler.ContextHandler.doScope(ContextHandler.java:1247)
    at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:144)
    at org.eclipse.jetty.server.Dispatcher.forward(Dispatcher.java:227)
    at org.eclipse.jetty.server.Dispatcher.error(Dispatcher.java:86)
    at org.eclipse.jetty.server.handler.ErrorHandler.doError(ErrorHandler.java:119)
    at org.eclipse.jetty.server.handler.ErrorHandler.handle(ErrorHandler.java:78)
    at org.springframework.boot.web.embedded.jetty.JettyEmbeddedErrorHandler.handle(JettyEmbeddedErrorHandler.java:55)
    at org.eclipse.jetty.server.Response.sendError(Response.java:655)
    at org.eclipse.jetty.server.handler.AbstractHandler.doError(AbstractHandler.java:100)
    at org.eclipse.jetty.server.handler.ContextHandler.doHandle(ContextHandler.java:1339)
    at org.eclipse.jetty.server.handler.ScopedHandler.nextScope(ScopedHandler.java:203)
    at org.eclipse.jetty.servlet.ServletHandler.doScope(ServletHandler.java:480)
    at org.eclipse.jetty.server.session.SessionHandler.doScope(SessionHandler.java:1557)
    at org.eclipse.jetty.server.handler.ScopedHandler.nextScope(ScopedHandler.java:201)
    at org.eclipse.jetty.server.handler.ContextHandler.doScope(ContextHandler.java:1247)
    at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:144)
    at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:132)
    at org.eclipse.jetty.server.Server.handle(Server.java:502)
    at org.eclipse.jetty.server.HttpChannel.handle(HttpChannel.java:419)
    at org.eclipse.jetty.server.HttpConnection.onFillable(HttpConnection.java:260)
    at org.eclipse.jetty.io.AbstractConnection$ReadCallback.succeeded(AbstractConnection.java:305)
    at org.eclipse.jetty.io.FillInterest.fillable(FillInterest.java:103)
    at org.eclipse.jetty.io.ChannelEndPoint$2.run(ChannelEndPoint.java:118)
    at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.runTask(EatWhatYouKill.java:333)
    at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.doProduce(EatWhatYouKill.java:310)
    at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.tryProduce(EatWhatYouKill.java:168)
    at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.run(EatWhatYouKill.java:126)
    at org.eclipse.jetty.util.thread.ReservedThreadExecutor$ReservedThread.run(ReservedThreadExecutor.java:366)
    at org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:765)
    at org.eclipse.jetty.util.thread.QueuedThreadPool$2.run(QueuedThreadPool.java:683)
    at java.lang.Thread.run(Thread.java:834)

どうも、spring boot 2.1.x系のspring sessionとspring data redisの組み合わせを使っていると出るらしい。 issueも報告されている

以下の通り、spring boot 2.3.x系では直っているので、解決策はバージョンの最新化をすれば良さそう

github.com

これ日本語情報が全く無かったので、英語が苦手な人だと困りはてる気がしたのでキーワードだけでもと思いブログに情報を残しておきます。

テストコードを書くその前に

今日テストコードを書く前にどんな事を考えているかつぶやいたのでブログにも転記しておく。

要約するとテストも戦略を考えて設計してから書こうね、というお話でした。

私が考えるファーストクラスコレクションを定義しておく意味や意義みたいなもの

私の考え

私は、ドメインオブジェクトのコレクションには常にファーストクラスコレクションを定義しておくと良いと考えています。

例えば、List<DomainObject>という生の配列をクライアントコードに操作させる場合、あちこちのクライアントコードに同じ操作が(しかもちょっとずつ違うやり方で)書かれているという状況を招きかねないためです。

特に別の技術者が参画してきたときはまだコードの書き方を真似しきれないはずですから、その状況を引き起こしやすいでしょう。

例えば、List<DomainObject>を許した場合

どういうことがおきるでしょうか。

同じ操作なのに書き方が違うものがあちこちのクライアントコードに散在しているとします。 影響をうけうるクライアントコードをすべて特定するのが手間です。 また、特定できたとして書き方が違うと本当にそこにも変更を反映すべきなのかどうか迷ってしまうかもしれません、それを調べるコストがもったいないと感じてしまいます。

また、別のケースとしては、属性に変更があったから操作に影響が出たのでとある処理の修正が必要といったときに、その変更が発散していろんなクライアントコードに波及してしまいます。

一方で、ファーストクラスコレクションを定義してそこに操作を集約しておくとどうなるか

操作を実装されるコード間の距離が物理的に近いのでそのコレクションを操作する時の書き方の統一感を期待できますし、何かを操作するコードが1箇所であることを期待できるので、変更の発散がかなり抑制できるはず1です。

開発に関わるエンジニアが少数しかおらず、その入れ替わりがないのであればまだ操作が存在しないコレクションまでファーストクラスコレクションとするのは短期的にはコスパは悪いかもしれません。

しかし、先述の理由から中長期的にはコスパ良いと私は信じています。

なお、ファーストクラスコレクションに限りませんが、私の経験則では、後からリファクタリングして対応するという方針にする場合、似たような処理(分岐)が2箇所で出てきたら黄信号(リファクタリングの必要が高い)、3箇所になったら赤信号(このタイミングでリファクタリングしておかないと高い確率でまずい状況を引き起こす)って感じています。


  1. 運が良ければ完全になくせるかもしれませんが、変更はどうしても波及するものです。しかし、その影響度合いを小さくすることは可能だと考えています。

クラスなどの名付けで抽象的にするか具体的にするかで悩んだら

OperationServiceとするかSomeSpecificOperationServiceで悩んだらSomeSpecificOperationServiceを選びます。

ここって、確かに迷いがちなところですが、抽象度が高い名前をつけると責務が大きくなりすぎる傾向が強いので、具体的な名前をつけるほうが私は好みです。

  1. 具体的な名前をつける
  2. いろんな実装を進める上で他にもやらせたいことが増えてきて名前が不適切になる
  3. (1)で作ったクラス名を適切なものに改名するか、(それが思いつかなければ?)具体的な別の名前のサービスを定義するか考える

みたいな順番で考えることが多いです。

これも制約を強めるか弱めるかの選択肢の話に通じるものがあるのですが、

より具体的な名前 ⇒(用途の)制約が強い より抽象的な名前 ⇒(用途の)制約が弱い

と、私は考えています。

制約が弱いものを強めるのは後からやり辛いので、その2択で結果に大差がなさそうなら制約が強い方を選択するという原則(自分でかってに言ってるだけのやつ)に従って、より具体的な名前をつけておく、という選択になる感じです。

私のコードレビュー観点について

仕事でまとめる機会があったので、ブログに残しておきます。

私が考えるレビュー観点は以下の4点に分類できます。

  1. 仕様
  2. 内部品質
  3. セキュリティ
  4. パフォーマンス

1. 仕様を満たしているか

  • 仕様の漏れがないか
  • 仕様の誤りがないか

2. 内部品質

  • 変数名・関数(メソッド)名・クラス名に関してニュアンスを捉えた英単語を選択しているか
  • コメントに嘘がないか
  • 可読性を考慮できているか
    • 込み入った処理の場合は、説明的変数や説明的メソッドを用いるなど
    • ifやswitchを使わない、使っても深くネストしない(せいぜい2つまで)
  • 使用している言語のイディオムや慣習に従った書き方ができているか
  • パフォーマンスよりも可読性を優先してかけているか
    • 可読性が高いコードであればボトルネックを突き止めるのは簡単なため、パフォーマンスの問題が起きてから最適化すれば良い、という考え方。
  • 適切なデザインパターンを適用できているか
  • typoがないか

3. セキュリティ

  • パスワードの類の情報をハードコードしてたり設定ファイルに書き込んでいたりしないか
  • 脆弱性のあるライブラリやフレームワークのバージョンを指定していないか
  • メンテナンスが止まってしまっているライブラリやフレームワークを採用していないか

4. パフォーマンス

  • 無意味に処理が遅くなる書き方をしていないか(Javaのfor文の中で+を使って文字列を結合する、とか)
  • thread safeにも関わらず毎回インスタンスを生成していないか
  • full scanになってしまうSQLになってないか
  • 必要に応じてテーブルのINDEXを定義できているか

超簡単な内容ですが、簡潔にまとめるとこんな感じです。

Spring BootでMVCを自己署名証明書(オレオレ証明書)を使ってHTTPSでListenさせる方法

オレオレ証明書を使ってSpring Bootをhttpsで起動させてると、macがcatalinaでブラウザがchromeの場合にアクセスしてもERR_CERT_REVOKEDと表示されて先に進めない(警告を無視するオプションが提示されない)事象に出くわして4時間溶けました。

めちゃくちゃハマって辛かったので未来の自分と同じことにハマってる誰かのために記事を残しておきます。

何が起きていたか

macOSがCatalinaでブラウザがChromeだと動かないという事象が起きていました。

Spring BootでMVChttpsをlistenさせる方法はググるとたくさん出てくるので簡単だと思っていたのですが全然そんなことありませんでした…。

どうも、Catalinaになったタイミングでセキュリティが厳しくなった?っぽいです。

keytoolのオプションの組み合わせや設定値をいろいろ工夫してもうまく動かず途方にくれていました。

ググるこんなのが出てきたりします。mojaveじゃ起きないらしいですが、私は試していません。

回避方法がないかkeytool localhost certificate chrome ”ERR_CERT_REVOKED"あたりのキーワードでいろいろググってると、海外のどっかの会社のなにかのプロダクトで同じ問題がおきていたらしく、そのコメントのやり取りの中にこのコマンドで再作成すれば動くようになったぜ(超意訳)!ってのを見つけて、動かすために必要なkeytoolのオプションを特定することができました。

Spring Bootで自己署名証明書オレオレ証明書)でHTTPS通信を受け付けるための設定

というわけで、それを踏まえた上で設定手順を書き出していきます。

動作確認環境

  • Spring Boot 2.1.2
    • 2.x系なら同じ設定で動くと思います。
  • macOS Catalina 10.15.2
    • mojaveなら再現しないっぽい…

自己署名証明書作成手順

以下のコマンドで作成できます。

$ keytool -genkey -alias localhost -keyalg RSA -keysize 2048 -storetype PKCS12 -keystore /pass/to/your/keystore.p12 -dname "CN=localhost" -storepass yourpassword -ext ExtendedKeyUsage=serverAuth -validity 825

ポイントは次の2点です。

  • 有効期限を825日以下にする
  • -ext ExtendedKeyUsage=serverAuthのオプションを追加する ※X.509 証明書の拡張機能

この2点を突き止めるのにすごく時間がかかりました。1

なお、対話モードをスキップするために-dnamestorepassも入ってますがここはお好みです。

また、storetypeはPKCS12を指定していますが別にJWSでも問題なく動きました。

Spring Bootの設定内容

server:
  port: 8443 #任意のポート番号で良い
  ssl:
    enabled: true # sslのセクションがある時点でtrueがデフォルトなので省略可
    key-store: /path/to/your/keystore.p12
    key-store-password: yourpassword
    key-store-type: PKCS12
    key-alias: localhost

たったこれだけでhttpsでlistenするように変わっているはずです。

おまけ

httpsでlistenする場合、加えてhttpsのみ通信を許可する設定をいれたくなるかもしれませんが、1.x系で存在していたsecurity.require-ssl=trueが2.x系では削除されているのでJavaで書く必要があります。


  1. ググっては試すの繰り返し。証明書の仕様に詳しくないのでtry & errorが早いと判断した