[{"data":1,"prerenderedAt":16345},["ShallowReactive",2],{"articles":3},[4,1186,1800,2546,5259,8183,11285,11662,11972,12668,14860,15205,15931],{"_path":5,"_dir":6,"_draft":7,"_partial":7,"_locale":8,"title":9,"description":10,"date":11,"tags":12,"rowTypeId":18,"sitemap":19,"body":21,"_type":1180,"_id":1181,"_source":1182,"_file":1183,"_stem":1184,"_extension":1185},"/articles/career/new-graduate-job-hunting","career",false,"","【27卒エンジニア就活】生殺与奪の権を他人に握らせない就活体験談","27卒WEB系エンジニアが逆求人だけで大手SaaS内定。財務分析・チェックシートで企業を比較した就活体験談。夏インターンから内定承諾まで全プロセスを公開。","2026/03/20",[13,14,15,16,17],"就活","新卒","エンジニア就活","キャリア","27卒",1,{"loc":5,"lastmod":20,"priority":18},"2026-03-20",{"type":22,"children":23,"toc":1148},"root",[24,32,38,47,52,82,87,92,97,110,177,187,192,197,282,294,300,312,318,323,329,334,340,345,394,406,411,416,422,428,433,438,443,469,474,479,485,490,495,500,506,511,523,533,538,544,556,561,573,578,583,588,593,598,604,609,621,627,641,653,658,667,672,677,720,725,730,735,750,755,760,765,770,782,787,814,819,824,912,917,922,935,940,945,950,955,960,970,979,984,990,998,1004,1009,1015,1020,1025,1065,1070,1075,1094,1106,1111,1128,1133,1138,1143],{"type":25,"tag":26,"props":27,"children":29},"element","h2",{"id":28},"はじめに",[30],{"type":31,"value":28},"text",{"type":25,"tag":33,"props":34,"children":35},"p",{},[36],{"type":31,"value":37},"この記事では、27卒WEB系エンジニアとして就活を経験した私が、就活の流れや重要だと感じたポイント、そして就活中に感じた不満とその対策をまとめています。エンジニアの就職活動はいつから何を準備すればいいのか分かりにくいため、これから就活を控えている方の参考になれば幸いです。",{"type":25,"tag":39,"props":40,"children":41},"summary-box",{},[42],{"type":25,"tag":33,"props":43,"children":44},{},[45],{"type":31,"value":46},"27卒WEB系エンジニアの就活体験談です。大学1年生から長期インターンで実務経験を積み、大学3年生の夏インターンを経て年内に内定を獲得しました。早く動くほど次のステップの選択肢が広がること、そして内定承諾は「人的資本の投資判断」として客観的な基準で行ったことをまとめています。",{"type":25,"tag":26,"props":48,"children":50},{"id":49},"対象読者",[51],{"type":31,"value":49},{"type":25,"tag":53,"props":54,"children":55},"ul",{},[56,62,67,72,77],{"type":25,"tag":57,"props":58,"children":59},"li",{},[60],{"type":31,"value":61},"WEB系エンジニアとして新卒採用での就活を考えている大学生",{"type":25,"tag":57,"props":63,"children":64},{},[65],{"type":31,"value":66},"エンジニア就活の全体像やスケジュール感を知りたい方",{"type":25,"tag":57,"props":68,"children":69},{},[70],{"type":31,"value":71},"サマーインターンや長期インターンシップについて情報を集めている方",{"type":25,"tag":57,"props":73,"children":74},{},[75],{"type":31,"value":76},"現在の就活事情を知りたい社会人",{"type":25,"tag":57,"props":78,"children":79},{},[80],{"type":31,"value":81},"就活生が何を考えているか知りたい人事担当者",{"type":25,"tag":26,"props":83,"children":85},{"id":84},"結果",[86],{"type":31,"value":84},{"type":25,"tag":33,"props":88,"children":89},{},[90],{"type":31,"value":91},"3社受けて2社から内定が得られました。全社サマーインターン経由の選考です。1社の落選はカルチャーミスマッチが大きかったかもと感じています。",{"type":25,"tag":26,"props":93,"children":95},{"id":94},"就活のプロセス",[96],{"type":31,"value":94},{"type":25,"tag":33,"props":98,"children":99},{},[100,102,108],{"type":31,"value":101},"実質的に",{"type":25,"tag":103,"props":104,"children":105},"strong",{},[106],{"type":31,"value":107},"就活は大学1年生から始まりました",{"type":31,"value":109},"。",{"type":25,"tag":111,"props":112,"children":113},"timeline",{},[114],{"type":25,"tag":53,"props":115,"children":116},{},[117,127,137,147,157,167],{"type":25,"tag":57,"props":118,"children":119},{},[120,125],{"type":25,"tag":103,"props":121,"children":122},{},[123],{"type":31,"value":124},"大学1年生 4月〜",{"type":31,"value":126},"\nエンジニア長期インターン探し",{"type":25,"tag":57,"props":128,"children":129},{},[130,135],{"type":25,"tag":103,"props":131,"children":132},{},[133],{"type":31,"value":134},"大学1年生 夏",{"type":31,"value":136},"\nエンジニア長期インターン開始",{"type":25,"tag":57,"props":138,"children":139},{},[140,145],{"type":25,"tag":103,"props":141,"children":142},{},[143],{"type":31,"value":144},"大学2〜3年生の春",{"type":31,"value":146},"\nサマーインターン探し・選考",{"type":25,"tag":57,"props":148,"children":149},{},[150,155],{"type":25,"tag":103,"props":151,"children":152},{},[153],{"type":31,"value":154},"大学3年生 夏",{"type":31,"value":156},"\nサマーインターン（就業型2社、ワークショップ型1社）",{"type":25,"tag":57,"props":158,"children":159},{},[160,165],{"type":25,"tag":103,"props":161,"children":162},{},[163],{"type":31,"value":164},"大学3年生 秋",{"type":31,"value":166},"\n本選考",{"type":25,"tag":57,"props":168,"children":169},{},[170,175],{"type":25,"tag":103,"props":171,"children":172},{},[173],{"type":31,"value":174},"大学3年生 11月",{"type":31,"value":176},"\n内定",{"type":25,"tag":33,"props":178,"children":179},{},[180,185],{"type":25,"tag":103,"props":181,"children":182},{},[183],{"type":31,"value":184},"大学3年生かつ年内で内定が得られる時代",{"type":31,"value":186},"になりました。",{"type":25,"tag":26,"props":188,"children":190},{"id":189},"就活で感じた重要ポイント",[191],{"type":31,"value":189},{"type":25,"tag":33,"props":193,"children":194},{},[195],{"type":31,"value":196},"エンジニア就活で重要だと感じたポイントをまとめました。各ポイントの詳細は後続のMISSIONで振り返っています。",{"type":25,"tag":198,"props":199,"children":200},"ol",{},[201,231,254,267,272,277],{"type":25,"tag":57,"props":202,"children":203},{},[204,206,211,213],{"type":31,"value":205},"スキル面は",{"type":25,"tag":103,"props":207,"children":208},{},[209],{"type":31,"value":210},"実務経験やチームでの開発経験が何よりも求められた",{"type":31,"value":212}," → MISSION 1で詳述\n",{"type":25,"tag":198,"props":214,"children":215},{},[216,221,226],{"type":25,"tag":57,"props":217,"children":218},{},[219],{"type":31,"value":220},"個人開発の「結果」はあまり評価されなかった。その過程を説明できるとGoodだった",{"type":25,"tag":57,"props":222,"children":223},{},[224],{"type":31,"value":225},"実務経験を積む機会がない場合は大学のサークルなどを活用してチームで開発する経験があると良かったと思う",{"type":25,"tag":57,"props":227,"children":228},{},[229],{"type":31,"value":230},"自分が何をしたいかをしっかり語れるようにすることが重要だった。「技術力を磨いてテックリードになりたいです」ではなく、「どういうものを作って、どういうことを成し遂げたいか」というような野望を語れるかが問われた",{"type":25,"tag":57,"props":232,"children":233},{},[234,239,241],{"type":25,"tag":103,"props":235,"children":236},{},[237],{"type":31,"value":238},"夏インターンは実質本選考のプロセスだった",{"type":31,"value":240}," → MISSION 2で詳述\n",{"type":25,"tag":198,"props":242,"children":243},{},[244,249],{"type":25,"tag":57,"props":245,"children":246},{},[247],{"type":31,"value":248},"企業が優秀な学生を囲い込むためのイベントだった",{"type":25,"tag":57,"props":250,"children":251},{},[252],{"type":31,"value":253},"優秀というのは現在のスキルが多い人ではなく、今後成長できそうという意味。新卒なので",{"type":25,"tag":57,"props":255,"children":256},{},[257,259],{"type":31,"value":258},"夏インターンの募集は大学3年生の4月頃から始まった\n",{"type":25,"tag":198,"props":260,"children":261},{},[262],{"type":25,"tag":57,"props":263,"children":264},{},[265],{"type":31,"value":266},"つまり、大学2年生までにある程度の準備が必要だった",{"type":25,"tag":57,"props":268,"children":269},{},[270],{"type":31,"value":271},"夏インターン(特に就業型)の募集枠は小さかった",{"type":25,"tag":57,"props":273,"children":274},{},[275],{"type":31,"value":276},"夏インターンで結果を残せたことで本選考で優遇された",{"type":25,"tag":57,"props":278,"children":279},{},[280],{"type":31,"value":281},"夏インターン以上に本選考では厳しく査定された → MISSION 3で詳述",{"type":25,"tag":33,"props":283,"children":284},{},[285,287,292],{"type":31,"value":286},"このように、",{"type":25,"tag":103,"props":288,"children":289},{},[290],{"type":31,"value":291},"夏インターンに参加できるかがカギ",{"type":31,"value":293},"でした。さらに、内定をもらった後も「どの企業に自分の時間を投資するか」という判断が待っています。そこで、実際に就活時に設定していた3つのMISSIONを紹介します。",{"type":25,"tag":26,"props":295,"children":297},{"id":296},"mission-１実務経験を得よ",[298],{"type":31,"value":299},"MISSION １：実務経験を得よ",{"type":25,"tag":33,"props":301,"children":302},{},[303,305,310],{"type":31,"value":304},"夏インターンの選考でも、本選考でも、実務経験に関する内容はよく聞かれました。",{"type":25,"tag":103,"props":306,"children":307},{},[308],{"type":31,"value":309},"実務経験があるというだけで就活難易度が大きく変わった",{"type":31,"value":311},"と感じたので、積極的に狙いました。",{"type":25,"tag":313,"props":314,"children":316},"h3",{"id":315},"どうやって探したか",[317],{"type":31,"value":315},{"type":25,"tag":33,"props":319,"children":320},{},[321],{"type":31,"value":322},"InfraやWantedlyなどのサイトを利用しました。求人は多くありましたが、未経験OKな求人を探すのは難しかったです。",{"type":25,"tag":313,"props":324,"children":326},{"id":325},"実務経験が欲しい理由チームでの経験が重要だから",[327],{"type":31,"value":328},"実務経験が欲しい理由：チームでの経験が重要だから",{"type":25,"tag":33,"props":330,"children":331},{},[332],{"type":31,"value":333},"個人開発と実務は全然違うと感じました。特に、「どのくらいのチーム規模でどのようにコミュニケーションをとったか」、「チームで働きやすい人と働きにくい人の違い」というような個人ではなく集団でどのように動いたかという質問は本選考でもよく聞かれました。具体的に個人開発と何が違うか感じたことをまとめます。",{"type":25,"tag":335,"props":336,"children":338},"h4",{"id":337},"個人開発と実務の違い",[339],{"type":31,"value":337},{"type":25,"tag":33,"props":341,"children":342},{},[343],{"type":31,"value":344},"ソースコードの規模が違う程度だと最初は思っていましたが、その程度ではありません。何が違うか。それは、自由度が全然違うということです。",{"type":25,"tag":346,"props":347,"children":350},"tradeoff-box",{"cons-label":348,"pros-label":349},"実務の特徴","個人開発の特徴",[351,373],{"type":25,"tag":352,"props":353,"children":354},"template",{"v-slot:pros":8},[355],{"type":25,"tag":53,"props":356,"children":357},{},[358,363,368],{"type":25,"tag":57,"props":359,"children":360},{},[361],{"type":31,"value":362},"好きな技術で開発できる",{"type":25,"tag":57,"props":364,"children":365},{},[366],{"type":31,"value":367},"自由に時間をかけられる",{"type":25,"tag":57,"props":369,"children":370},{},[371],{"type":31,"value":372},"作りたいものが作れる",{"type":25,"tag":352,"props":374,"children":375},{"v-slot:cons":8},[376],{"type":25,"tag":53,"props":377,"children":378},{},[379,384,389],{"type":25,"tag":57,"props":380,"children":381},{},[382],{"type":31,"value":383},"既存の技術を踏襲する必要がある",{"type":25,"tag":57,"props":385,"children":386},{},[387],{"type":31,"value":388},"納期（いつまでに）の制約がある",{"type":25,"tag":57,"props":390,"children":391},{},[392],{"type":31,"value":393},"泥臭い制約が多い",{"type":25,"tag":33,"props":395,"children":396},{},[397,399,404],{"type":31,"value":398},"個人開発ではやりたい放題できましたが、実務ではこのような制約が多くありました。これを経験したかしていないかで大きな違いが出るので実務経験は必要とされていました。企業から求められる技術力というのは",{"type":25,"tag":103,"props":400,"children":401},{},[402],{"type":31,"value":403},"キラキラした経験ではなく、くすんだ経験の積み重ね",{"type":31,"value":405},"でした。",{"type":25,"tag":33,"props":407,"children":408},{},[409],{"type":31,"value":410},"重要ポイント1で述べた「実務経験やチームでの開発経験が何よりも求められた」というのは、こうした背景があったからです。",{"type":25,"tag":33,"props":412,"children":413},{},[414],{"type":31,"value":415},"また、これは大学1年生当時は予想できておらず結果論になりますが、AIの技術が以上に発展し、個人開発で評価を得る難易度が高くなっているというのも実務経験が欲しい理由になると思います。詳細は以下の記事をご覧ください。",{"type":25,"tag":417,"props":418,"children":421},"link-card",{"label":419,"to":420},"個人開発とAIについてはこちら⬇️","/articles/tech/development/portal-with-claude-code",[],{"type":25,"tag":26,"props":423,"children":425},{"id":424},"mission-2夏インターン先を確保せよ",[426],{"type":31,"value":427},"MISSION 2：夏インターン先を確保せよ",{"type":25,"tag":33,"props":429,"children":430},{},[431],{"type":31,"value":432},"夏インターンは参加すれば色々お得だったので、必ず参加したいと考えていました。",{"type":25,"tag":313,"props":434,"children":436},{"id":435},"サマーインターン参加のメリット",[437],{"type":31,"value":435},{"type":25,"tag":33,"props":439,"children":440},{},[441],{"type":31,"value":442},"以下のメリットがあると感じました。",{"type":25,"tag":198,"props":444,"children":445},{},[446,459,464],{"type":25,"tag":57,"props":447,"children":448},{},[449,451],{"type":31,"value":450},"実際に働くことで、会社の雰囲気や文化を感じられた\n",{"type":25,"tag":198,"props":452,"children":453},{},[454],{"type":25,"tag":57,"props":455,"children":456},{},[457],{"type":31,"value":458},"インターン向けに良い部分だけを見せるという会社は少ないのである程度信用できました。内定承諾先を選ぶ際にも役立ちました",{"type":25,"tag":57,"props":460,"children":461},{},[462],{"type":31,"value":463},"給料ももらえた（時給2000円〜が相場）",{"type":25,"tag":57,"props":465,"children":466},{},[467],{"type":31,"value":468},"本選考の一部をスキップできた",{"type":25,"tag":313,"props":470,"children":472},{"id":471},"どうやって探したか-1",[473],{"type":31,"value":315},{"type":25,"tag":33,"props":475,"children":476},{},[477],{"type":31,"value":478},"サポーターズの1on1イベントなど、逆求人イベントを活用しました。実務経験があったので参加するための選考は比較的簡単に通りました。そこで多くの企業と面談して、「自分はどのような会社で働いて、どのようなエンジニアになりたいか」自問自答しました。",{"type":25,"tag":335,"props":480,"children":482},{"id":481},"就活時短ポイントなぜ逆求人",[483],{"type":31,"value":484},"就活時短ポイント！なぜ逆求人？",{"type":25,"tag":33,"props":486,"children":487},{},[488],{"type":31,"value":489},"逆求人イベントは選考スキップがもらえることがあります。具体的には、書類選考スキップ、面接スキップなどがあります。",{"type":25,"tag":313,"props":491,"children":493},{"id":492},"サマーインターン選考対策",[494],{"type":31,"value":492},{"type":25,"tag":33,"props":496,"children":497},{},[498],{"type":31,"value":499},"「自分がエンジニアとして何を大事にしているのか」というようなマインドセット、そして「どのような開発をしてきてどのように考えたか」というような技術力を具体的なエピソードとともに語れるように準備しました。志望理由はまだ技術的な目的（貴社の〇〇の開発を通して△△を学びたい）程度で十分でした。",{"type":25,"tag":335,"props":501,"children":503},{"id":502},"面接対策は一問一答ではなく木構造で考える",[504],{"type":31,"value":505},"面接対策は「一問一答」ではなく木構造で考える",{"type":25,"tag":33,"props":507,"children":508},{},[509],{"type":31,"value":510},"少しエンジニアっぽい考えを選考対策に加えます。\n面接対策としてよくあるのは「質問と回答を暗記する」という方法ですが、私はあまり効果的ではないと感じました。\n面接ではほぼ確実に深掘りが入ります。\nそのため、一問一答で回答を準備しても途中で破綻します。",{"type":25,"tag":33,"props":512,"children":513},{},[514,516,521],{"type":31,"value":515},"そこで私は、回答を ",{"type":25,"tag":103,"props":517,"children":518},{},[519],{"type":31,"value":520},"木構造（ツリー構造）",{"type":31,"value":522}," で整理しました。",{"type":25,"tag":524,"props":525,"children":527},"pre",{"code":526},"例：  \n・なぜエンジニアを志望したのか(よくある質問ですね)  \n　├ きっかけ  \n　├ どのような経験をしてきたか（ここが重要！）  \n　└ 将来どのようなものを作りたいか  \n",[528],{"type":25,"tag":529,"props":530,"children":531},"code",{"__ignoreMap":8},[532],{"type":31,"value":526},{"type":25,"tag":33,"props":534,"children":535},{},[536],{"type":31,"value":537},"このように背景・経験・将来像をセットで整理しておくと、どの方向に深掘りされても一貫した回答ができました。",{"type":25,"tag":335,"props":539,"children":541},{"id":540},"結果だけでは評価されない逆にありがたい",[542],{"type":31,"value":543},"結果だけでは評価されない(逆にありがたい)",{"type":25,"tag":33,"props":545,"children":546},{},[547,549,554],{"type":31,"value":548},"ここでいう、結果というのは「〇〇を開発した」、「インターンに参加した」、「〇〇の資格を取った」というようなものです。\nどこの企業でも ",{"type":25,"tag":103,"props":550,"children":551},{},[552],{"type":31,"value":553},"「プロセス」に興味を持ちます",{"type":31,"value":555},"。何がきっかけで、どのようなことをして、どのような結果になって、どう感じたか。\n就活以前にこれが説明できないとどうしても胡散臭い自慢話になってしまうと感じました。\nあと、私は遭遇していませんが、もし結果だけを評価してくる企業があったとしたら、入社後にメンタルを潰されると思います。",{"type":25,"tag":335,"props":557,"children":559},{"id":558},"たくさん選考を受けるのは重要だった",[560],{"type":31,"value":558},{"type":25,"tag":33,"props":562,"children":563},{},[564,566,571],{"type":31,"value":565},"面接に慣れることが何よりも大事だと感じました。自分が伝えたいことをしっかり伝えるためには何よりも",{"type":25,"tag":103,"props":567,"children":568},{},[569],{"type":31,"value":570},"面接慣れ",{"type":31,"value":572},"が重要でした。",{"type":25,"tag":335,"props":574,"children":576},{"id":575},"鋼の精神を持った",[577],{"type":31,"value":575},{"type":25,"tag":33,"props":579,"children":580},{},[581],{"type":31,"value":582},"夏インターン選考では一部企業では落選でした。ですが、ここで落ち込むと他企業の選考に響くので、単に縁がなかっただけと割り切りました。",{"type":25,"tag":33,"props":584,"children":585},{},[586],{"type":31,"value":587},"これを言うと人事担当者に怒られそうですが、「夏インターンの選考落としてきた企業は、絶対本選考も受けない」と心に決めていたのでそれくらいの厚顔無恥精神で戦っていました。「生殺与奪の権を他人に握らせるな」ということですね！",{"type":25,"tag":33,"props":589,"children":590},{},[591],{"type":31,"value":592},"※あくまで長期戦で精神を強く保つためです。貴重な時間をいただいているのでしっかり感謝の気持ちを持ちましょう。ちなみに、夏インターンに落ちても本選考では合格という例もあるらしいので機会損失には気をつけてください。",{"type":25,"tag":33,"props":594,"children":595},{},[596],{"type":31,"value":597},"重要ポイント2〜5で述べたように、夏インターンは実質本選考のプロセスであり、ここで実績を作れるかが就活全体の流れを左右しました。",{"type":25,"tag":26,"props":599,"children":601},{"id":600},"mission-3本選考を突破し内定を承諾せよ",[602],{"type":31,"value":603},"MISSION 3：本選考を突破し、内定を承諾せよ",{"type":25,"tag":33,"props":605,"children":606},{},[607],{"type":31,"value":608},"本選考の対策自体はサマーインターンと同じ選考対策に加え、志望動機の言語化ができていれば問題ありませんでした。",{"type":25,"tag":33,"props":610,"children":611},{},[612,614,619],{"type":31,"value":613},"しかし、本選考を進める中で強く感じたのは、",{"type":25,"tag":103,"props":615,"children":616},{},[617],{"type":31,"value":618},"合格をもらうことがゴールではない",{"type":31,"value":620},"ということでした。ゴールは最適なキャリアを選択することです。できる限り転職せずにキャリアを積みたいと考えていたので、この辺りからキャリアプラン・ライフプラン・企業文化などがマッチングしているかを考え始めました。夏インターンはあくまで選択肢を増やすためでしたが、本選考以降は選択肢を絞っていきました。",{"type":25,"tag":313,"props":622,"children":624},{"id":623},"内定承諾は人的資本への投資",[625],{"type":31,"value":626},"内定承諾は「人的資本への投資」",{"type":25,"tag":33,"props":628,"children":629},{},[630,632,639],{"type":31,"value":631},"内定承諾というのは、単に会社を選ぶという行為ではありません。\n自分という",{"type":25,"tag":633,"props":634,"children":636},"tooltip",{"content":635},"自分のスキル・時間・労働力を経済的な資本として捉える考え方",[637],{"type":31,"value":638},"人的資本",{"type":31,"value":640},"をどの企業に投資するかを決める意思決定だと考えていました。",{"type":25,"tag":33,"props":642,"children":643},{},[644,646,651],{"type":31,"value":645},"社会人として働く時間は人生の大きな割合を占めます。学生時代も小学校から大学まで16年ですから。その時間をどの企業に投じるかによって、身につくスキル・経験・人脈は大きく変わります。そのため、企業選びをできるだけ",{"type":25,"tag":103,"props":647,"children":648},{},[649],{"type":31,"value":650},"感覚ではなく、客観的な基準で判断",{"type":31,"value":652},"したいと考えました。",{"type":25,"tag":33,"props":654,"children":655},{},[656],{"type":31,"value":657},"この考えのもと、納得のいく選択をするために実際にやったことを紹介します。",{"type":25,"tag":659,"props":660,"children":661},"caution-box",{},[662],{"type":25,"tag":33,"props":663,"children":664},{},[665],{"type":31,"value":666},"ここから先は人事担当者の方は閲覧注意です",{"type":25,"tag":313,"props":668,"children":670},{"id":669},"自分が何をしたいかを再言語化した",[671],{"type":31,"value":669},{"type":25,"tag":33,"props":673,"children":674},{},[675],{"type":31,"value":676},"志望動機にも関わるのでこれは最優先で進めました。自分が何をしたくて、この企業ではこういうことができて……というのを言語化しました。具体的には以下のような観点でまとめました。",{"type":25,"tag":53,"props":678,"children":679},{},[680,690,700,710],{"type":25,"tag":57,"props":681,"children":682},{},[683,688],{"type":25,"tag":103,"props":684,"children":685},{},[686],{"type":31,"value":687},"どのようなものを作りたいか",{"type":31,"value":689},": 技術を手段として何を実現したいのか",{"type":25,"tag":57,"props":691,"children":692},{},[693,698],{"type":25,"tag":103,"props":694,"children":695},{},[696],{"type":31,"value":697},"どのようなエンジニアになりたいか",{"type":31,"value":699},": 目指すキャリアパスの方向性（マネジメント寄りか技術寄りかなど）",{"type":25,"tag":57,"props":701,"children":702},{},[703,708],{"type":25,"tag":103,"props":704,"children":705},{},[706],{"type":31,"value":707},"働く上で何を大事にしたいか",{"type":31,"value":709},": 企業文化やチームの雰囲気に求めるもの",{"type":25,"tag":57,"props":711,"children":712},{},[713,718],{"type":25,"tag":103,"props":714,"children":715},{},[716],{"type":31,"value":717},"中長期的なライフプランとの整合性",{"type":31,"value":719},": 働き方（リモート可否）や勤務地が将来の生活設計と合うか",{"type":25,"tag":33,"props":721,"children":722},{},[723],{"type":31,"value":724},"これだけで2000文字程度のレポートになりました。この言語化が後の企業比較レポートやチェックシートの評価軸にもそのまま使えたので、最初にやっておいて正解でした。",{"type":25,"tag":417,"props":726,"children":729},{"label":727,"to":728},"「何をしたいか」の言語化についてはこちら⬇️","/articles/career/what-do-i-want-to-do",[],{"type":25,"tag":313,"props":731,"children":733},{"id":732},"定量評価するためのチェックシートを作成した",[734],{"type":31,"value":732},{"type":25,"tag":33,"props":736,"children":737},{},[738,744,748],{"type":25,"tag":739,"props":740,"children":743},"img",{"alt":741,"src":742},"チェックシート","/images/articles/career/checksheet.png",[],{"type":25,"tag":745,"props":746,"children":747},"br",{},[],{"type":31,"value":749},"\n画像のようなチェックシートを作成し、点数づけ、その点数をつけた理由を言語化しました。価値観がぶれないようにするためにも重要な指標でした。",{"type":25,"tag":33,"props":751,"children":752},{},[753],{"type":31,"value":754},"なお、技術スタックの魅力・先進性の重み値はあえて0にしました。これを意識する人は多いですが、一番効率よく学べる環境というのは最新技術を導入しているとか表面的なものではなく、文化・人・制度という技術とは切り離した環境だと考えています。また、どんなに良い環境でも成長できない人はできないままなので自力でなんとかするという考えがベースにあります。",{"type":25,"tag":313,"props":756,"children":758},{"id":757},"リクルーターとの面談で懸念点を探った",[759],{"type":31,"value":757},{"type":25,"tag":33,"props":761,"children":762},{},[763],{"type":31,"value":764},"この辺のすり合わせは大事だと感じました。選考で落ちるリスクは増えますが、誤った選択をするリスクは減ります。この辺を忖度する必要は一切ないと思います。自分の人生は自分のものですから。",{"type":25,"tag":313,"props":766,"children":768},{"id":767},"内定後に企業比較レポートを書いた",[769],{"type":31,"value":767},{"type":25,"tag":33,"props":771,"children":772},{},[773,775,780],{"type":31,"value":774},"内定承諾先を",{"type":25,"tag":103,"props":776,"children":777},{},[778],{"type":31,"value":779},"感覚ではなくデータに基づいて判断",{"type":31,"value":781},"したかったので、内定先を比較するレポートを作成しました。最終的に5000文字規模になりましたが、書き上げたことで「なぜこの企業を選ぶのか」を自分自身に対して明確に説明できる状態になりました。以下のような観点で分析しました。",{"type":25,"tag":335,"props":783,"children":785},{"id":784},"給与構造の分析",[786],{"type":31,"value":784},{"type":25,"tag":33,"props":788,"children":789},{},[790,792,797,799,805,807,812],{"type":31,"value":791},"単純に年収の額面を比較するだけでなく、月給のうち手当が占める割合（手当比率）や時間単価を算出しました。",{"type":25,"tag":103,"props":793,"children":794},{},[795],{"type":31,"value":796},"手当比率が高いと、手当は本給と比べて変更されやすいため減給リスクが大きくなります",{"type":31,"value":798},"。また、",{"type":25,"tag":633,"props":800,"children":802},{"content":801},"あらかじめ一定時間分の残業代を給与に含めて支払う制度。みなし残業代とも呼ばれる",[803],{"type":31,"value":804},"固定残業代",{"type":31,"value":806},"から逆算して時間単価を比較することで、残業が増えた場合のリスクも見積もりました。最近は",{"type":25,"tag":103,"props":808,"children":809},{},[810],{"type":31,"value":811},"固定残業代で額面を大きく見せている企業が多い",{"type":31,"value":813},"のでしっかり確認しました。",{"type":25,"tag":335,"props":815,"children":817},{"id":816},"ビジネスモデルと財務の分析",[818],{"type":31,"value":816},{"type":25,"tag":33,"props":820,"children":821},{},[822],{"type":31,"value":823},"半期報告書などのIR情報をもとに、売上高の成長率・純利益率・自己資本比率などを比較しました。上場企業であれば公開が義務付けられており、投資家向けの情報なので会社がどの方向に向かうかが一番わかりやすく書かれていました。",{"type":25,"tag":825,"props":826,"children":827},"table",{},[828,847],{"type":25,"tag":829,"props":830,"children":831},"thead",{},[832],{"type":25,"tag":833,"props":834,"children":835},"tr",{},[836,842],{"type":25,"tag":837,"props":838,"children":839},"th",{},[840],{"type":31,"value":841},"項目",{"type":25,"tag":837,"props":843,"children":844},{},[845],{"type":31,"value":846},"わかること",{"type":25,"tag":848,"props":849,"children":850},"tbody",{},[851,869,886,899],{"type":25,"tag":833,"props":852,"children":853},{},[854,864],{"type":25,"tag":855,"props":856,"children":857},"td",{},[858],{"type":25,"tag":633,"props":859,"children":861},{"content":860},"総資産のうち返済不要な自己資本の割合。高いほど財務が安定",[862],{"type":31,"value":863},"自己資本比率",{"type":25,"tag":855,"props":865,"children":866},{},[867],{"type":31,"value":868},"財務の安定性。高いほど借入に依存しておらず倒産リスクが低い",{"type":25,"tag":833,"props":870,"children":871},{},[872,881],{"type":25,"tag":855,"props":873,"children":874},{},[875],{"type":25,"tag":633,"props":876,"children":878},{"content":877},"売上高に対する本業の利益の割合",[879],{"type":31,"value":880},"営業利益率",{"type":25,"tag":855,"props":882,"children":883},{},[884],{"type":31,"value":885},"本業の稼ぐ力。高いほど事業そのものが儲かっている",{"type":25,"tag":833,"props":887,"children":888},{},[889,894],{"type":25,"tag":855,"props":890,"children":891},{},[892],{"type":31,"value":893},"純利益率",{"type":25,"tag":855,"props":895,"children":896},{},[897],{"type":31,"value":898},"最終的な収益性。税金や特別損失を含めた総合的な利益体質",{"type":25,"tag":833,"props":900,"children":901},{},[902,907],{"type":25,"tag":855,"props":903,"children":904},{},[905],{"type":31,"value":906},"参入障壁の高さ",{"type":25,"tag":855,"props":908,"children":909},{},[910],{"type":31,"value":911},"競合の入りにくさ。高いほど価格競争に巻き込まれにくく事業が安定する",{"type":25,"tag":33,"props":913,"children":914},{},[915],{"type":31,"value":916},"特に自己資本比率は高いほど健全な財務といえますが、企業のフェーズ（グロースフェーズ）であれば低くなりがちなのであくまで参考程度にした方が良いと思います。また、ビジネスモデルやサプライチェーンによっても営業利益率も変わってくるので、単純に高い方が優れていると比較できるわけではないことに注意しました。なので基本的な企業研究が済んでいる前提で行った方が良いですね。",{"type":25,"tag":335,"props":918,"children":920},{"id":919},"資産形成シミュレーション",[921],{"type":31,"value":919},{"type":25,"tag":33,"props":923,"children":924},{},[925,927,933],{"type":31,"value":926},"持株会の奨励金率やストックオプションの条件を比較し、長期的にどちらが低リスクで資産形成できるかを検討しました。",{"type":25,"tag":633,"props":928,"children":930},{"content":929},"同じ金額でも早く手に入れるほど運用期間が長くなり価値が高いという考え方",[931],{"type":31,"value":932},"貨幣の時間価値など",{"type":31,"value":934},"（若いうちに得る収入とそれによって得られる経験ほど複利が利く）の観点も加えました。20歳で得る100万円と50歳で得る100万円ではできることが違いますからね。\n資産形成の話は以下の記事にまとめたのでぜひご覧ください。大学生から考えてても早すぎない状況になっていると思います。",{"type":25,"tag":417,"props":936,"children":939},{"label":937,"to":938},"資産形成の詳細はこちら⬇️","/articles/career/student-asset-building",[],{"type":25,"tag":335,"props":941,"children":943},{"id":942},"ライフプランとの整合性",[944],{"type":31,"value":942},{"type":25,"tag":33,"props":946,"children":947},{},[948],{"type":31,"value":949},"勤務形態（リモート可否）、転職時の不利にならないか（本給の水準）、住宅ローンの借入可能額への影響など、中長期的なライフステージの変化を見据えた比較も行いました。",{"type":25,"tag":33,"props":951,"children":952},{},[953],{"type":31,"value":954},"重要ポイント6で「夏インターン以上に本選考では厳しく査定された」と述べましたが、それは選考の難易度だけでなく、自分自身の判断基準もより厳しくなったという意味でもあります。選考を突破するだけでなく、その先の「どの企業に人生を投資するか」まで考え抜いたのがこのMISSIONでした。",{"type":25,"tag":313,"props":956,"children":958},{"id":957},"運命の選択",[959],{"type":31,"value":957},{"type":25,"tag":33,"props":961,"children":962},{},[963,965,968],{"type":31,"value":964},"ここまでチェックシート、レポート、財務分析までしてきました。材料は出揃い、完璧なはずです。",{"type":25,"tag":745,"props":966,"children":967},{},[],{"type":31,"value":969},"\nしかし！",{"type":25,"tag":971,"props":972,"children":973},"punchline-box",{},[974],{"type":25,"tag":33,"props":975,"children":976},{},[977],{"type":31,"value":978},"あれ？両方とも楽しそうじゃね？",{"type":25,"tag":33,"props":980,"children":981},{},[982],{"type":31,"value":983},"というように結局迷う事になりました。\n気持ち的には分身をもう１人用意してそれぞれで内定承諾させたかったです(ついでに収入もほぼ２倍ですからね。分身すればお得です)。",{"type":25,"tag":26,"props":985,"children":987},{"id":986},"エンジニア就活ブチギレポイント対策",[988],{"type":31,"value":989},"エンジニア就活ブチギレポイント・対策",{"type":25,"tag":659,"props":991,"children":992},{},[993],{"type":25,"tag":33,"props":994,"children":995},{},[996],{"type":31,"value":997},"ここから先はかなり過激です。",{"type":25,"tag":313,"props":999,"children":1001},{"id":1000},"長期インターンシップの募集は実務経験必須の募集が多い",[1002],{"type":31,"value":1003},"長期インターンシップの募集は実務経験必須の募集が多い！",{"type":25,"tag":33,"props":1005,"children":1006},{},[1007],{"type":31,"value":1008},"よくある「鶏と卵どちらが先か」という問題です。実務経験が欲しくて長期インターンシップを探しているのに実務経験を求められるのが困りました。これに関しては文句を言っても仕方がないので頑張って求人を探しました。この社会で生きると言うことは理不尽な矛盾との戦いに出るということですね。就活も例外ではありません。",{"type":25,"tag":313,"props":1010,"children":1012},{"id":1011},"こんな早い時期に就活をさせて学生に何を求めてるんだ",[1013],{"type":31,"value":1014},"こんな早い時期に就活をさせて、学生に何を求めてるんだ！！",{"type":25,"tag":33,"props":1016,"children":1017},{},[1018],{"type":31,"value":1019},"2年生から3年生にかけての春休みから就活が始まるのは正直かなり早いと感じました。\nまだ研究室にも配属されていない時期です。\nしかもESや面接では大学の成績について聞かれることはほぼありませんでした。\nこれを経験して「大学とは何なのか」と哲学的な問いを抱えた就活生は私だけではないはず。\nなので、大学は学問を学ぶ場所ではなく就活予備校だと思っていた方が精神衛生上良いと思います。大学の授業は役に立ちます。ですが、3年生でまだ授業が多くある中、就活と並行して力を入れるのは無理です。このようにリソース不足に追いやるのが現代の就職活動なのです。周りの大人が「大学は学問がー」とか講釈を垂れようと自分の意思を貫きましょう。時代は変わりました。",{"type":25,"tag":33,"props":1021,"children":1022},{},[1023],{"type":31,"value":1024},"就活早期化のメリット・デメリットをまとめると以下になります。",{"type":25,"tag":346,"props":1026,"children":1027},{},[1028,1039],{"type":25,"tag":352,"props":1029,"children":1030},{"v-slot:pros":8},[1031],{"type":25,"tag":53,"props":1032,"children":1033},{},[1034],{"type":25,"tag":57,"props":1035,"children":1036},{},[1037],{"type":31,"value":1038},"大学4年生では卒業研究だけに集中できる",{"type":25,"tag":352,"props":1040,"children":1041},{"v-slot:cons":8},[1042],{"type":25,"tag":53,"props":1043,"children":1044},{},[1045,1050,1055,1060],{"type":25,"tag":57,"props":1046,"children":1047},{},[1048],{"type":31,"value":1049},"2年→3年の春休みから就活開始で早すぎる",{"type":25,"tag":57,"props":1051,"children":1052},{},[1053],{"type":31,"value":1054},"研究室配属前なのに就活が始まる",{"type":25,"tag":57,"props":1056,"children":1057},{},[1058],{"type":31,"value":1059},"大学の成績はほぼ聞かれない",{"type":25,"tag":57,"props":1061,"children":1062},{},[1063],{"type":31,"value":1064},"リソース不足に陥りやすい",{"type":25,"tag":33,"props":1066,"children":1067},{},[1068],{"type":31,"value":1069},"個人的にデメリットが大きく感じました。3年生で就活するのはしんどいので勘弁してくれというのが本音です。売り手市場なのはありがたいですがやりすぎだと思います。",{"type":25,"tag":26,"props":1071,"children":1073},{"id":1072},"結論",[1074],{"type":31,"value":1072},{"type":25,"tag":33,"props":1076,"children":1077},{},[1078,1080,1085,1087,1092],{"type":31,"value":1079},"エンジニアの就職活動を振り返って最も伝えたいのは、",{"type":25,"tag":103,"props":1081,"children":1082},{},[1083],{"type":31,"value":1084},"早く動き始めること",{"type":31,"value":1086},"と",{"type":25,"tag":103,"props":1088,"children":1089},{},[1090],{"type":31,"value":1091},"自分の判断軸を持つこと",{"type":31,"value":1093},"の2つです。",{"type":25,"tag":33,"props":1095,"children":1096},{},[1097,1099,1104],{"type":31,"value":1098},"MISSION 1で実務経験を早期に積んだことがMISSION 2の夏インターン選考を有利にし、MISSION 2で得た経験と実績がMISSION 3の本選考・内定承諾の判断材料になりました。このように、",{"type":25,"tag":103,"props":1100,"children":1101},{},[1102],{"type":31,"value":1103},"早く動くほど次のステップの選択肢が広がる構造",{"type":31,"value":1105},"になっていました。",{"type":25,"tag":33,"props":1107,"children":1108},{},[1109],{"type":31,"value":1110},"そして、自分が何をしたいかを言語化し、チェックシートやレポートで客観的に評価したことで、内定承諾の判断に後悔がなくなりました。",{"type":25,"tag":33,"props":1112,"children":1113},{},[1114,1119,1121,1126],{"type":25,"tag":103,"props":1115,"children":1116},{},[1117],{"type":31,"value":1118},"就活は企業に選ばれるだけのイベントではありません",{"type":31,"value":1120},"。自分がどの企業に",{"type":25,"tag":103,"props":1122,"children":1123},{},[1124],{"type":31,"value":1125},"人生の時間を投資するか",{"type":31,"value":1127},"を決める意思決定です。そのために必要な情報を集め、自分なりの基準で判断する。それが「生殺与奪の権を他人に握らせない」就活だと思っています。",{"type":25,"tag":26,"props":1129,"children":1131},{"id":1130},"最後に",[1132],{"type":31,"value":1130},{"type":25,"tag":33,"props":1134,"children":1135},{},[1136],{"type":31,"value":1137},"ここまで色々と率直なことを書きましたが、今回の就活を通して関わってくださった企業の皆様には本当に感謝しています。",{"type":25,"tag":33,"props":1139,"children":1140},{},[1141],{"type":31,"value":1142},"面接や面談ではどの企業も対等かつ丁寧に時間をかけて対応してくださいました。特にリクルーターの方々には、キャリアについて真剣に向き合っていただき、多くの学びを得ることができました。",{"type":25,"tag":33,"props":1144,"children":1145},{},[1146],{"type":31,"value":1147},"この場を借りて改めて感謝申し上げます。\nそして、ここまで読んでいただきありがとうございました。\nこの記事が、これから就活を迎えるエンジニア志望の方の参考になれば幸いです。",{"title":8,"searchDepth":1149,"depth":1149,"links":1150},2,[1151,1152,1153,1154,1155,1156,1161,1166,1174,1178,1179],{"id":28,"depth":1149,"text":28},{"id":49,"depth":1149,"text":49},{"id":84,"depth":1149,"text":84},{"id":94,"depth":1149,"text":94},{"id":189,"depth":1149,"text":189},{"id":296,"depth":1149,"text":299,"children":1157},[1158,1160],{"id":315,"depth":1159,"text":315},3,{"id":325,"depth":1159,"text":328},{"id":424,"depth":1149,"text":427,"children":1162},[1163,1164,1165],{"id":435,"depth":1159,"text":435},{"id":471,"depth":1159,"text":315},{"id":492,"depth":1159,"text":492},{"id":600,"depth":1149,"text":603,"children":1167},[1168,1169,1170,1171,1172,1173],{"id":623,"depth":1159,"text":626},{"id":669,"depth":1159,"text":669},{"id":732,"depth":1159,"text":732},{"id":757,"depth":1159,"text":757},{"id":767,"depth":1159,"text":767},{"id":957,"depth":1159,"text":957},{"id":986,"depth":1149,"text":989,"children":1175},[1176,1177],{"id":1000,"depth":1159,"text":1003},{"id":1011,"depth":1159,"text":1014},{"id":1072,"depth":1149,"text":1072},{"id":1130,"depth":1149,"text":1130},"markdown","content:articles:career:new-graduate-job-hunting.md","content","articles/career/new-graduate-job-hunting.md","articles/career/new-graduate-job-hunting","md",{"_path":938,"_dir":6,"_draft":7,"_partial":7,"_locale":8,"title":1187,"description":1188,"date":1189,"tags":1190,"rowTypeId":18,"sitemap":1195,"body":1196,"_type":1180,"_id":1797,"_source":1182,"_file":1798,"_stem":1799,"_extension":1185},"【就活戦後処理】持株会・NISA・生活防衛資金を大学生なりに考えた話","就活が終わり、新卒エンジニアを前に持株会・NISA・生活防衛資金を検討した大学生の体験記。何を買うべきかではなく、どう考えどう仕組みを作ったかをまとめました。","2026-03-21",[1191,1192,1193,1194],"資産形成","大学生","NISA","持株会",{"loc":938,"lastmod":1189,"priority":18},{"type":22,"children":1197,"toc":1772},[1198,1202,1207,1212,1235,1243,1248,1253,1298,1303,1308,1314,1319,1324,1329,1334,1340,1345,1361,1367,1372,1386,1392,1397,1411,1417,1422,1427,1432,1437,1460,1465,1470,1475,1480,1493,1498,1511,1516,1522,1527,1559,1564,1616,1629,1634,1639,1644,1649,1654,1662,1667,1680,1685,1690,1695,1699,1704,1709,1714,1719,1724,1729,1734,1739,1744,1749],{"type":25,"tag":26,"props":1199,"children":1200},{"id":28},[1201],{"type":31,"value":28},{"type":25,"tag":33,"props":1203,"children":1204},{},[1205],{"type":31,"value":1206},"この記事は、大学生の自分が資産形成についてゼロから調べて考えた体験記です。特定の投資商品や銘柄を勧める意図は一切ありません。あくまで「自分はこう調べて、こう考えた」という記録として参考程度にご覧ください。",{"type":25,"tag":313,"props":1208,"children":1210},{"id":1209},"この記事でわかること",[1211],{"type":31,"value":1209},{"type":25,"tag":53,"props":1213,"children":1214},{},[1215,1220,1225,1230],{"type":25,"tag":57,"props":1216,"children":1217},{},[1218],{"type":31,"value":1219},"資産形成がなぜ必要か",{"type":25,"tag":57,"props":1221,"children":1222},{},[1223],{"type":31,"value":1224},"資産形成についてどのように情報収集すれば良いか",{"type":25,"tag":57,"props":1226,"children":1227},{},[1228],{"type":31,"value":1229},"大学生でもできること・できないこと",{"type":25,"tag":57,"props":1231,"children":1232},{},[1233],{"type":31,"value":1234},"大学生が資産形成について知るメリット",{"type":25,"tag":39,"props":1236,"children":1237},{},[1238],{"type":25,"tag":33,"props":1239,"children":1240},{},[1241],{"type":31,"value":1242},"就活を終えた大学生が、持株会・NISA・生活防衛資金について調べて考えた体験記です。「守り」から優先順位を決め、投資信託をコアにした仕組みづくりや、家計管理アプリの自作まで行いました。特定の商品を勧めるものではなく、思考プロセスの記録です。",{"type":25,"tag":26,"props":1244,"children":1246},{"id":1245},"なぜ資産形成を考え始めたのか",[1247],{"type":31,"value":1245},{"type":25,"tag":33,"props":1249,"children":1250},{},[1251],{"type":31,"value":1252},"以下の理由が組み合わさって資産形成を考えるようになりました。",{"type":25,"tag":53,"props":1254,"children":1255},{},[1256,1269,1282,1293],{"type":25,"tag":57,"props":1257,"children":1258},{},[1259,1261,1267],{"type":31,"value":1260},"就活で持株会や",{"type":25,"tag":633,"props":1262,"children":1264},{"content":1263},"会社が従業員に対して、あらかじめ決められた価格で自社株を購入できる権利を付与する制度",[1265],{"type":31,"value":1266},"ストックオプション",{"type":31,"value":1268},"のような「株式」の知識が必要になった",{"type":25,"tag":57,"props":1270,"children":1271},{},[1272,1274],{"type":31,"value":1273},"将来のライフイベントでまとまったお金がかかる\n",{"type":25,"tag":53,"props":1275,"children":1276},{},[1277],{"type":25,"tag":57,"props":1278,"children":1279},{},[1280],{"type":31,"value":1281},"引越し、住宅購入、結婚など",{"type":25,"tag":57,"props":1283,"children":1284},{},[1285,1287],{"type":31,"value":1286},"物価高騰・",{"type":25,"tag":633,"props":1288,"children":1290},{"content":1289},"インフレによる資産価値の目減りを防ぐための対策",[1291],{"type":31,"value":1292},"インフレヘッジ",{"type":25,"tag":57,"props":1294,"children":1295},{},[1296],{"type":31,"value":1297},"ただの好奇心",{"type":25,"tag":33,"props":1299,"children":1300},{},[1301],{"type":31,"value":1302},"せっかくなので、長期インターンで沢山働いて生まれた寝かしているお金を働かせたいと考えるようになりました。",{"type":25,"tag":26,"props":1304,"children":1306},{"id":1305},"どのように調べたか",[1307],{"type":31,"value":1305},{"type":25,"tag":313,"props":1309,"children":1311},{"id":1310},"とりあえずyoutubeを見る",[1312],{"type":31,"value":1313},"とりあえずYouTubeを見る",{"type":25,"tag":33,"props":1315,"children":1316},{},[1317],{"type":31,"value":1318},"最初はYouTubeで投資系インフルエンサーの動画などさまざまな情報を見ました。確かにわかりやすく、様々な知識を仕入れることはできました。",{"type":25,"tag":33,"props":1320,"children":1321},{},[1322],{"type":31,"value":1323},"しかし、これだけ買えば絶対安心といった断定的な発信や、特定の商材やサービスへの誘導も散見され、全てを鵜呑みにするのは危険だと感じました。情報源は複数用意する。これが基本方針です。",{"type":25,"tag":313,"props":1325,"children":1327},{"id":1326},"最終的に金融庁へ辿り着く",[1328],{"type":31,"value":1326},{"type":25,"tag":33,"props":1330,"children":1331},{},[1332],{"type":31,"value":1333},"金融庁が様々な情報を公開していました。政府機関なのでここが一番信用できる情報源だと思います。",{"type":25,"tag":335,"props":1335,"children":1337},{"id":1336},"金融庁パンフレットページ",[1338],{"type":31,"value":1339},"金融庁：パンフレットページ",{"type":25,"tag":33,"props":1341,"children":1342},{},[1343],{"type":31,"value":1344},"このページでは「基礎から学べる金融ガイド」など金融の基礎から学べる教材が多く用意されています。しかも、かなりわかりやすいです。資産形成だけでなく、家計管理、生活、保険、ローンなど今後関わるであろうものを網羅的に解説しているので、必ず一度は目を通した方が良い内容だと感じました。",{"type":25,"tag":33,"props":1346,"children":1347},{},[1348,1350,1353],{"type":31,"value":1349},"⬇️ 金融庁のパンフレット",{"type":25,"tag":745,"props":1351,"children":1352},{},[],{"type":25,"tag":1354,"props":1355,"children":1359},"a",{"href":1356,"rel":1357},"https://www.fsa.go.jp/common/about/pamphlet.html",[1358],"nofollow",[1360],{"type":31,"value":1356},{"type":25,"tag":335,"props":1362,"children":1364},{"id":1363},"金融庁利用者の方へページ",[1365],{"type":31,"value":1366},"金融庁：利用者の方へページ",{"type":25,"tag":33,"props":1368,"children":1369},{},[1370],{"type":31,"value":1371},"このページでは投資詐欺や高リスクな金融商品の注意喚起、制度の解説を行なっています。全てに目を通したわけでは無いですが、この取引大丈夫か？と気になったときはここを一度確認してみると良いかもしれません。",{"type":25,"tag":33,"props":1373,"children":1374},{},[1375,1377,1380],{"type":31,"value":1376},"⬇️ 金融庁：利用者の方へ",{"type":25,"tag":745,"props":1378,"children":1379},{},[],{"type":25,"tag":1354,"props":1381,"children":1384},{"href":1382,"rel":1383},"https://www.fsa.go.jp/user/index.html",[1358],[1385],{"type":31,"value":1382},{"type":25,"tag":335,"props":1387,"children":1389},{"id":1388},"金融庁nisa公式サイト",[1390],{"type":31,"value":1391},"金融庁：NISA公式サイト",{"type":25,"tag":33,"props":1393,"children":1394},{},[1395],{"type":31,"value":1396},"NISAは資産形成の代名詞のようになっており、名前だけ聞いたことがあるという人は多いと思います。簡単に説明すると、投資をした際の利益に対して非課税になる制度です。詳細はNISAの公式サイトをご覧ください。",{"type":25,"tag":33,"props":1398,"children":1399},{},[1400,1402,1405],{"type":31,"value":1401},"⬇️ 金融庁のNISA公式サイト",{"type":25,"tag":745,"props":1403,"children":1404},{},[],{"type":25,"tag":1354,"props":1406,"children":1409},{"href":1407,"rel":1408},"https://www.fsa.go.jp/policy/nisa2/index.html",[1358],[1410],{"type":31,"value":1407},{"type":25,"tag":26,"props":1412,"children":1414},{"id":1413},"情報収集が完了守りから考える",[1415],{"type":31,"value":1416},"情報収集が完了。「守り」から考える",{"type":25,"tag":33,"props":1418,"children":1419},{},[1420],{"type":31,"value":1421},"資産運用となると大体は株式、不動産、債券、暗号資産、FX、貴金属など多種多様ですが、リスクが伴うものが多いことがわかりました。となると、どのようにリスク軽減しつつ運用するかということが重要だと考えました。一番避けたいのは「お金を使いたいのに使えない」という状態になることです。そこで、以下のルールを作りました。",{"type":25,"tag":313,"props":1423,"children":1425},{"id":1424},"生活防衛資金を最優先で用意する",[1426],{"type":31,"value":1424},{"type":25,"tag":33,"props":1428,"children":1429},{},[1430],{"type":31,"value":1431},"仮に収入が途絶えても生活ができるようにするための資金を生活防衛資金と言います。これを確保するのが最優先。というルールを決めました。\nただ、まだ学生でそこまで重く考える必要はないので、運用にかけるお金と貯金する額を分けるということをしました。",{"type":25,"tag":33,"props":1433,"children":1434},{},[1435],{"type":31,"value":1436},"このような経緯で以下の優先順位にしました。",{"type":25,"tag":198,"props":1438,"children":1439},{},[1440,1445,1450,1455],{"type":25,"tag":57,"props":1441,"children":1442},{},[1443],{"type":31,"value":1444},"生活費",{"type":25,"tag":57,"props":1446,"children":1447},{},[1448],{"type":31,"value":1449},"生活防衛資金",{"type":25,"tag":57,"props":1451,"children":1452},{},[1453],{"type":31,"value":1454},"持株会やNISAなどの制度活用(つまりこの階層には余ったお金しか来ない)",{"type":25,"tag":57,"props":1456,"children":1457},{},[1458],{"type":31,"value":1459},"そのほか",{"type":25,"tag":26,"props":1461,"children":1463},{"id":1462},"持株会を活用すべきかを考える",[1464],{"type":31,"value":1462},{"type":25,"tag":33,"props":1466,"children":1467},{},[1468],{"type":31,"value":1469},"内定承諾先の企業に従業員持株会が存在するため加入を検討しました。\n持株会とは、企業が福利厚生などで用意している、自社の株式を取得することを奨励する制度です。会社によっては奨励金を出してくれます。",{"type":25,"tag":335,"props":1471,"children":1473},{"id":1472},"具体例と注意点",[1474],{"type":31,"value":1472},{"type":25,"tag":33,"props":1476,"children":1477},{},[1478],{"type":31,"value":1479},"奨励金が10%出る会社を例にします。毎月10000円拠出する場合、奨励金が10%出るので毎月11000円で自社株を買うことができます。つまり、10000円しか天引きされていないのに、それ以上の株式を取得できるということになります。",{"type":25,"tag":33,"props":1481,"children":1482},{},[1483,1485,1491],{"type":31,"value":1484},"ただし、奨励金は税法上給与所得として扱われることに注意が必要です(",{"type":25,"tag":633,"props":1486,"children":1488},{"content":1487},"所得が高くなるほど税率が段階的に上がる課税方式",[1489],{"type":31,"value":1490},"累進課税",{"type":31,"value":1492},"の対象になる)。",{"type":25,"tag":313,"props":1494,"children":1496},{"id":1495},"持株会は自社の経営に関わる第一歩",[1497],{"type":31,"value":1495},{"type":25,"tag":33,"props":1499,"children":1500},{},[1501,1503,1509],{"type":31,"value":1502},"持株会に加入することで、投資・経営視点でプロダクト開発のインパクトを考え、開発時の意思決定をできるようになるのではと考えました。ただ開発するだけでなく、顧客や経営の視点を持つエンジニアになりたいという思想からこのような考えに至っています。\nまた、持株会から株式を自身の口座に引き出し、株式の名義が自分になれば",{"type":25,"tag":633,"props":1504,"children":1506},{"content":1505},"株主総会で会社の重要事項（役員選任・配当など）に賛否を投票できる権利",[1507],{"type":31,"value":1508},"議決権",{"type":31,"value":1510},"を得て、自社の経営に関わることができるようになります。",{"type":25,"tag":33,"props":1512,"children":1513},{},[1514],{"type":31,"value":1515},"それに、自分が開発したプロダクトが市場から評価されていることが株価という形で定量的にわかることも魅力だと思います。\n株価が高いから顧客から評価されているとは必ずしも言えないことには注意です。",{"type":25,"tag":26,"props":1517,"children":1519},{"id":1518},"nisaをどのように活用するかを考える",[1520],{"type":31,"value":1521},"NISAをどのように活用するかを考える",{"type":25,"tag":33,"props":1523,"children":1524},{},[1525],{"type":31,"value":1526},"新NISAでは「つみたて投資枠」と「成長投資枠」がありますが、それぞれ仕様が異なります。基本的に投信か上場株式を扱うことになりますが、どのように運用するかを考える必要があります。",{"type":25,"tag":53,"props":1528,"children":1529},{},[1530,1535,1554],{"type":25,"tag":57,"props":1531,"children":1532},{},[1533],{"type":31,"value":1534},"どういう商品を買おうか",{"type":25,"tag":57,"props":1536,"children":1537},{},[1538,1544,1546,1552],{"type":25,"tag":633,"props":1539,"children":1541},{"content":1540},"配当金や利息など、資産を保有し続けることで得られる利益",[1542],{"type":31,"value":1543},"\nインカムゲイン\n",{"type":31,"value":1545},"\nを狙うか、\n",{"type":25,"tag":633,"props":1547,"children":1549},{"content":1548},"資産の売買差額によって得られる利益（値上がり益）",[1550],{"type":31,"value":1551},"\nキャピタルゲイン\n",{"type":31,"value":1553},"\nを狙うか\n",{"type":25,"tag":57,"props":1555,"children":1556},{},[1557],{"type":31,"value":1558},"個別株は買うべきか",{"type":25,"tag":33,"props":1560,"children":1561},{},[1562],{"type":31,"value":1563},"考えることはいろいろありましたが、次のように考え、何をコアにするかを決めました。",{"type":25,"tag":53,"props":1565,"children":1566},{},[1567,1585,1598,1603],{"type":25,"tag":57,"props":1568,"children":1569},{},[1570,1572],{"type":31,"value":1571},"元本が小さすぎて配当金などを軸にしてもあまり利益にならないよな\n",{"type":25,"tag":53,"props":1573,"children":1574},{},[1575,1580],{"type":25,"tag":57,"props":1576,"children":1577},{},[1578],{"type":31,"value":1579},"高配当株投資でも配当利回りは10%を超えることはほとんどない",{"type":25,"tag":57,"props":1581,"children":1582},{},[1583],{"type":31,"value":1584},"配当金狙いというだけで個別銘柄のリスクを負うのは流石に怖い",{"type":25,"tag":57,"props":1586,"children":1587},{},[1588,1590,1596],{"type":31,"value":1589},"それよりもDCA（",{"type":25,"tag":633,"props":1591,"children":1593},{"content":1592},"定額を定期的に投資し続けることで、購入価格を平均化しリスクを抑える手法",[1594],{"type":31,"value":1595},"ドルコスト平均法",{"type":31,"value":1597},"）で積み立てて値上がり益を狙いたい",{"type":25,"tag":57,"props":1599,"children":1600},{},[1601],{"type":31,"value":1602},"まだ若いからリスク許容度は大きくできそう",{"type":25,"tag":57,"props":1604,"children":1605},{},[1606,1608],{"type":31,"value":1607},"本業に集中したいからできる限り管理コストを抑えたい\n",{"type":25,"tag":53,"props":1609,"children":1610},{},[1611],{"type":25,"tag":57,"props":1612,"children":1613},{},[1614],{"type":31,"value":1615},"手数料など余分なコストもかけたくない",{"type":25,"tag":33,"props":1617,"children":1618},{},[1619,1621,1627],{"type":31,"value":1620},"となると、投資信託をコアにしようと考えました。投資信託といっても",{"type":25,"tag":633,"props":1622,"children":1624},{"content":1623},"投資信託の運用・管理にかかる手数料。保有中は毎日差し引かれるため、長期投資ではコストに大きく影響する",[1625],{"type":31,"value":1626},"信託報酬",{"type":31,"value":1628},"が安く、市場全体に分散投資できるようなファンド(インデックスファンド)です。これを定額で積み立てていこうというのが基本方針です。",{"type":25,"tag":33,"props":1630,"children":1631},{},[1632],{"type":31,"value":1633},"ただ、私は知的好奇心もあるので資産形成とは別枠で余った資金を使って個別株を成長投資枠で買おうとなりました。単純に国内企業を応援する、いわば推し活のような感覚です。",{"type":25,"tag":33,"props":1635,"children":1636},{},[1637],{"type":31,"value":1638},"とりあえずこの辺で一旦始めてみようと思って証券口座を作り、NISAを始めてみました。",{"type":25,"tag":313,"props":1640,"children":1642},{"id":1641},"実際に始めてみて感じたこと",[1643],{"type":31,"value":1641},{"type":25,"tag":335,"props":1645,"children":1647},{"id":1646},"ルール設計が重要と感じた",[1648],{"type":31,"value":1646},{"type":25,"tag":33,"props":1650,"children":1651},{},[1652],{"type":31,"value":1653},"一番怖いのは暴落でもなんでもなく、適当に曖昧なルールで投資をしてしまうことだと感じました。最終的に以下のルールで投資することにしました。",{"type":25,"tag":524,"props":1655,"children":1657},{"code":1656},"個別株はわがままな指値（さしね）、刺さらなかったら投信へ。\n",[1658],{"type":25,"tag":529,"props":1659,"children":1660},{"__ignoreMap":8},[1661],{"type":31,"value":1656},{"type":25,"tag":33,"props":1663,"children":1664},{},[1665],{"type":31,"value":1666},"というルールです。なぜこのようなルールにしたかというと、",{"type":25,"tag":33,"props":1668,"children":1669},{},[1670,1672,1678],{"type":31,"value":1671},"個別株は適当なルールで買うと",{"type":25,"tag":633,"props":1673,"children":1675},{"content":1674},"株価が高い時に買ってしまい、その後値下がりして含み損を抱えること",[1676],{"type":31,"value":1677},"高値掴み",{"type":31,"value":1679},"しやすいです。その企業特有のリスクを負うことになるので、できる限り安く買い、値動きに対する防御力を高めたいという考えがあります。ただし、あまりに厳しいルールだとそもそも買えなくなり、機会損失のリスクが生じます。そこで、指値が刺さらなかった場合は余った資金を投資信託に回すことで、少なくとも寝ている資金をなくし、市場で働かせることができるのではないかと考えました。",{"type":25,"tag":33,"props":1681,"children":1682},{},[1683],{"type":31,"value":1684},"他にもルールがありますが、中身を理解している商品を買うとかビジネスモデルとか実際に利用しているなど知っている企業に投資するというような、一般的なルールです。",{"type":25,"tag":335,"props":1686,"children":1688},{"id":1687},"いくら投資に回せばいいのかわからなくなる",[1689],{"type":31,"value":1687},{"type":25,"tag":33,"props":1691,"children":1692},{},[1693],{"type":31,"value":1694},"投資に回せる額を決めるには、生活費や防衛資金を除いた余剰資金を把握する必要があります。しかし、複数の決済手段や口座を使っていると全体像の把握が難しいと感じました。そこで、家計管理アプリを自作することにしました。",{"type":25,"tag":417,"props":1696,"children":1698},{"label":1697,"to":420},"家計管理アプリ開発の詳細はこちら⬇️",[],{"type":25,"tag":33,"props":1700,"children":1701},{},[1702],{"type":31,"value":1703},"このアプリを活用することで、毎月予算を編成し、月末時点でいくら持っているか、何にお金を使ったかなどを一元管理できるようになりました。とにかく失敗しづらい仕組みを作ることに専念しました。このような考え方はエンジニア特有かもしれません。",{"type":25,"tag":26,"props":1705,"children":1707},{"id":1706},"大学生のうちに考えて良かったと感じたこと",[1708],{"type":31,"value":1706},{"type":25,"tag":33,"props":1710,"children":1711},{},[1712],{"type":31,"value":1713},"一番は金融知識が身についたということが良かったと思います。",{"type":25,"tag":313,"props":1715,"children":1717},{"id":1716},"視野が広がった",[1718],{"type":31,"value":1716},{"type":25,"tag":33,"props":1720,"children":1721},{},[1722],{"type":31,"value":1723},"これが一番のメリットでした。今までニュースを見ても「日銀が利上げした」とか「円安が進行している」などと漠然と理解していた程度でしたが、資産運用を始めてからは「利上げしたから銀行は収益を得やすくなるけど、中小企業などが多少締め付けられるな」というように１つのニュースから複数のシナリオを予想して考えられるようになりました。",{"type":25,"tag":313,"props":1725,"children":1727},{"id":1726},"リスクに対する考え方が変わった",[1728],{"type":31,"value":1726},{"type":25,"tag":33,"props":1730,"children":1731},{},[1732],{"type":31,"value":1733},"今までは元本が割れるリスクを取ることは危険だから避けたいというような考え方をしていましたが、適切にリスクを取らなければそれ相応のリターンが得られないということも実感することができました。確かに株式などリスク資産を持つとその資産は無になる可能性はあります。だからこそ、無になっても問題ない資金で資産運用をする。ということを理解できました。",{"type":25,"tag":313,"props":1735,"children":1737},{"id":1736},"複利は時間がある方が有利",[1738],{"type":31,"value":1736},{"type":25,"tag":33,"props":1740,"children":1741},{},[1742],{"type":31,"value":1743},"資産形成は長期的に行うものです。そのため、早いうちに始められるなら始めたほうが良いので、実際に始められて良かったと思いました。ただ、今使えるお金ほど価値があるものはないので、将来のためにドケチ生活するのではなく、バランスをとることが大事だとも実感しました。",{"type":25,"tag":26,"props":1745,"children":1747},{"id":1746},"まとめ",[1748],{"type":31,"value":1746},{"type":25,"tag":53,"props":1750,"children":1751},{},[1752,1757,1762,1767],{"type":25,"tag":57,"props":1753,"children":1754},{},[1755],{"type":31,"value":1756},"将来のライフイベントを考えると資産形成も視野に入れる必要がある",{"type":25,"tag":57,"props":1758,"children":1759},{},[1760],{"type":31,"value":1761},"情報収集は複数ソースから。金融庁の公式サイトがわかりやすい",{"type":25,"tag":57,"props":1763,"children":1764},{},[1765],{"type":31,"value":1766},"言われた通りにするという受け身ではなく、自分の目的に合うやり方を考えることが大事",{"type":25,"tag":57,"props":1768,"children":1769},{},[1770],{"type":31,"value":1771},"大学生から資産形成を考える一番のメリットは知識が身につき視野が広がることだった",{"title":8,"searchDepth":1149,"depth":1149,"links":1773},[1774,1777,1778,1782,1785,1788,1791,1796],{"id":28,"depth":1149,"text":28,"children":1775},[1776],{"id":1209,"depth":1159,"text":1209},{"id":1245,"depth":1149,"text":1245},{"id":1305,"depth":1149,"text":1305,"children":1779},[1780,1781],{"id":1310,"depth":1159,"text":1313},{"id":1326,"depth":1159,"text":1326},{"id":1413,"depth":1149,"text":1416,"children":1783},[1784],{"id":1424,"depth":1159,"text":1424},{"id":1462,"depth":1149,"text":1462,"children":1786},[1787],{"id":1495,"depth":1159,"text":1495},{"id":1518,"depth":1149,"text":1521,"children":1789},[1790],{"id":1641,"depth":1159,"text":1641},{"id":1706,"depth":1149,"text":1706,"children":1792},[1793,1794,1795],{"id":1716,"depth":1159,"text":1716},{"id":1726,"depth":1159,"text":1726},{"id":1736,"depth":1159,"text":1736},{"id":1746,"depth":1149,"text":1746},"content:articles:career:student-asset-building.md","articles/career/student-asset-building.md","articles/career/student-asset-building",{"_path":728,"_dir":6,"_draft":7,"_partial":7,"_locale":8,"title":1801,"description":1802,"date":1803,"tags":1804,"rowTypeId":18,"sitemap":1807,"body":1808,"_type":1180,"_id":2543,"_source":1182,"_file":2544,"_stem":2545,"_extension":1185},"【27卒エンジニア就活】4STEPで自己分析したプロセス","就活の自己分析で「やりたいことがわからない」と悩んだ筆者が、抽象的なビジョンから具体的な企業選びまで逆算した自己分析プロセスを体験談ベースで紹介します。","2026-03-15",[13,16,1805,1806],"マインドセット","自己分析",{"loc":728,"lastmod":1803,"priority":18},{"type":22,"children":1809,"toc":2521},[1810,1814,1819,1824,1832,1840,1846,1852,1864,1870,1875,1880,1885,1890,1896,1958,1964,1969,1974,1979,2061,2073,2079,2084,2089,2150,2155,2161,2166,2171,2176,2181,2186,2191,2196,2208,2214,2219,2224,2229,2235,2264,2270,2275,2318,2324,2351,2357,2362,2376,2381,2386,2409,2415,2420,2425,2430,2435,2453,2457,2462,2504,2516],{"type":25,"tag":26,"props":1811,"children":1812},{"id":28},[1813],{"type":31,"value":28},{"type":25,"tag":33,"props":1815,"children":1816},{},[1817],{"type":31,"value":1818},"就活の時になると自分が何をしたいのかがわからなくなることが多いと思います。しかも、厄介なのは志望動機を言語化するためには「自分のやりたいこと」と「志望企業がやりたいこと」をリンクさせなければなりません。",{"type":25,"tag":33,"props":1820,"children":1821},{},[1822],{"type":31,"value":1823},"私もまさにこの壁にぶつかりました。「なぜその会社なのか？」と理由はあるはずなのに言語化できない。そんな状態から、自己分析を通じてどうやって自分の軸を見つけたのかを、プロセスとして紹介します。",{"type":25,"tag":659,"props":1825,"children":1826},{},[1827],{"type":25,"tag":33,"props":1828,"children":1829},{},[1830],{"type":31,"value":1831},"この記事はあくまで私個人の体験談です。自己分析に正解はないので、「こういうやり方もあるんだ」くらいの気持ちで読んでいただけると嬉しいです。",{"type":25,"tag":39,"props":1833,"children":1834},{},[1835],{"type":25,"tag":33,"props":1836,"children":1837},{},[1838],{"type":31,"value":1839},"「やりたいことがわからない」を解消するには、抽象的なゴールから逆算 → 課題意識の深掘り → 構造化(３つの視点に分けるなど) → 企業マッチングという4つのSTEPで進めると軸が見えてきます。この記事では、私が実際にやった自己分析のプロセスを具体例付きで紹介します。",{"type":25,"tag":26,"props":1841,"children":1843},{"id":1842},"step-1抽象的なゴールから逆算する",[1844],{"type":31,"value":1845},"STEP 1：抽象的なゴールから逆算する",{"type":25,"tag":313,"props":1847,"children":1849},{"id":1848},"まず漠然としたやりたいことを言葉にする",[1850],{"type":31,"value":1851},"まず「漠然としたやりたいこと」を言葉にする",{"type":25,"tag":33,"props":1853,"children":1854},{},[1855,1857,1862],{"type":31,"value":1856},"理想は自分の経験から楽しいとか、やりがいを感じたことは何かを振り返るのが良いと思います。私の場合は「使っていて楽しいソフトウェアを作る」と言語化しました。ここでの「楽しい」とは、単なるデザインや遊び心に留まらない、 ",{"type":25,"tag":103,"props":1858,"children":1859},{},[1860],{"type":31,"value":1861},"「ユーザーが本来の目的にストレスなく到達できる、心地よい体験」",{"type":31,"value":1863}," のことを指しています。高速なレスポンスや高い信頼性、安心してデータを預けられるセキュリティが、その体験を支える土台になると考えています。",{"type":25,"tag":335,"props":1865,"children":1867},{"id":1866},"それすらなく方向性も定まらない場合はどうするか",[1868],{"type":31,"value":1869},"それすらなく、方向性も定まらない場合はどうするか",{"type":25,"tag":33,"props":1871,"children":1872},{},[1873],{"type":31,"value":1874},"ですが、これまでの経験からも漠然としたやりたいことすらない場合どうするかわからないという場合もあると思います。そこで、さらに抽象的に考えると良いと思います。\n例えば、「生活を便利にしたい」、「健康で長生きできる社会にしたい」とかそういう感じです。",{"type":25,"tag":33,"props":1876,"children":1877},{},[1878],{"type":31,"value":1879},"業界・業種はこのやりたいことを解決する手段でしかないので、まずはここから考えると良いかもしれません。とにかくどの方向を向くかが決まれば迷いづらくなります。",{"type":25,"tag":313,"props":1881,"children":1883},{"id":1882},"抽象から具体へ",[1884],{"type":31,"value":1882},{"type":25,"tag":33,"props":1886,"children":1887},{},[1888],{"type":31,"value":1889},"漠然とやりたいことが決まったら、これを実現するための手段を考えました。\n具体的な例(エンジニア以外向け)と実例(エンジニア向け)を示します。",{"type":25,"tag":335,"props":1891,"children":1893},{"id":1892},"例生活を便利にしたいを叶えるために何が必要か",[1894],{"type":31,"value":1895},"例）「生活を便利にしたい」を叶えるために何が必要か",{"type":25,"tag":198,"props":1897,"children":1898},{},[1899,1917,1935],{"type":25,"tag":57,"props":1900,"children":1901},{},[1902,1904],{"type":31,"value":1903},"生活を便利にするって具体的にどこが不便なんだろう（問題を考える）\n",{"type":25,"tag":198,"props":1905,"children":1906},{},[1907,1912],{"type":25,"tag":57,"props":1908,"children":1909},{},[1910],{"type":31,"value":1911},"買い物に行くのが面倒、届くまで時間がかかる",{"type":25,"tag":57,"props":1913,"children":1914},{},[1915],{"type":31,"value":1916},"役所の手続きが複雑で何度も足を運ぶ必要がある",{"type":25,"tag":57,"props":1918,"children":1919},{},[1920,1922],{"type":31,"value":1921},"その不便さを解消する手段は何だろう（解決策を考える）\n",{"type":25,"tag":198,"props":1923,"children":1924},{},[1925,1930],{"type":25,"tag":57,"props":1926,"children":1927},{},[1928],{"type":31,"value":1929},"ECやネットスーパーのように、欲しいものがすぐ届く仕組み",{"type":25,"tag":57,"props":1931,"children":1932},{},[1933],{"type":31,"value":1934},"行政手続きをオンラインで完結できるサービス",{"type":25,"tag":57,"props":1936,"children":1937},{},[1938,1940],{"type":31,"value":1939},"じゃあ自分はどの立場で関わりたいんだろう（自分の役割を考える）\n",{"type":25,"tag":198,"props":1941,"children":1942},{},[1943,1948,1953],{"type":25,"tag":57,"props":1944,"children":1945},{},[1946],{"type":31,"value":1947},"サービスを作る側？運用する側？企画する側？",{"type":25,"tag":57,"props":1949,"children":1950},{},[1951],{"type":31,"value":1952},"作るのはエンジニアリングスキルがないから無理だ",{"type":25,"tag":57,"props":1954,"children":1955},{},[1956],{"type":31,"value":1957},"発想力はある気がするから企画をしてみたいかも",{"type":25,"tag":335,"props":1959,"children":1961},{"id":1960},"実例使っていて楽しいソフトウェアを開発するには何が必要か",[1962],{"type":31,"value":1963},"実例）「使っていて楽しいソフトウェア」を開発するには何が必要か",{"type":25,"tag":33,"props":1965,"children":1966},{},[1967],{"type":31,"value":1968},"これは私が実際にやった例になります。",{"type":25,"tag":33,"props":1970,"children":1971},{},[1972],{"type":31,"value":1973},"まず思いつくのは、使いやすいUIアプリに実装するということが挙げられます。ただ、これまでエンジニアをしてきてこれだけで実現できるわけではないと感じました。そこで、「UIのようなユーザが直接触る部分だけではない」と仮定し、「利用者からは見えない裏側も重要なのでは」と仮の問いを立てました。\nそして、次の順序で考えました。",{"type":25,"tag":33,"props":1975,"children":1976},{},[1977],{"type":31,"value":1978},"以下は私の思考の流れです。技術的な内容が含まれますが、この後STEP 3で整理するので、今は流れだけ追ってください",{"type":25,"tag":198,"props":1980,"children":1981},{},[1982,1993,1998,2035,2048],{"type":25,"tag":57,"props":1983,"children":1984},{},[1985,1991],{"type":25,"tag":633,"props":1986,"children":1988},{"content":1987},"ユーザーからは見えない、裏側で動いている処理やシステムのこと",[1989],{"type":31,"value":1990},"\nサーバサイド\n",{"type":31,"value":1992},"\nが複雑な実装になっている\n",{"type":25,"tag":57,"props":1994,"children":1995},{},[1996],{"type":31,"value":1997},"UIも複雑になり、結果的に利用者は混乱する",{"type":25,"tag":57,"props":1999,"children":2000},{},[2001,2003],{"type":31,"value":2002},"サーバサイドを複雑にしないためには何が必要か？\n",{"type":25,"tag":198,"props":2004,"children":2005},{},[2006,2019,2030],{"type":25,"tag":57,"props":2007,"children":2008},{},[2009,2011],{"type":31,"value":2010},"利用者が何を求めているか、適切に要件を分析できる必要がある\n",{"type":25,"tag":198,"props":2012,"children":2013},{},[2014],{"type":25,"tag":57,"props":2015,"children":2016},{},[2017],{"type":31,"value":2018},"利用者視点に立つには実際に利用者の立場で体験しないといけないな",{"type":25,"tag":57,"props":2020,"children":2021},{},[2022,2028],{"type":25,"tag":633,"props":2023,"children":2025},{"content":2024},"業務の構造を整理し、ソフトウェアの設計に落とし込む手法",[2026],{"type":31,"value":2027},"\nドメインモデリング\n",{"type":31,"value":2029},"\nを適切に行い、単一の責任を持つモジュールを設計できる必要がある\n",{"type":25,"tag":57,"props":2031,"children":2032},{},[2033],{"type":31,"value":2034},"データ構造も工夫しないと将来的に膨大なデータを抱えた際に動作が遅くなったり、拡張しづらくなって結果的に遠回りすることになりそうだな",{"type":25,"tag":57,"props":2036,"children":2037},{},[2038,2040],{"type":31,"value":2039},"サーバサイドが複雑だとテストもしづらくなって、バグの温床になりそう\n",{"type":25,"tag":198,"props":2041,"children":2042},{},[2043],{"type":25,"tag":57,"props":2044,"children":2045},{},[2046],{"type":31,"value":2047},"テストしやすくするためにも設計技術は重要だな",{"type":25,"tag":57,"props":2049,"children":2050},{},[2051,2053],{"type":31,"value":2052},"複雑さを解消するための保守に時間を割くと、ビジネス的にインパクトがある新機能開発に時間が割けないかもしれない\n",{"type":25,"tag":198,"props":2054,"children":2055},{},[2056],{"type":25,"tag":57,"props":2057,"children":2058},{},[2059],{"type":31,"value":2060},"これを説得するにはビジネスの視点で他業種の人に説明できるスキルが必要だ",{"type":25,"tag":33,"props":2062,"children":2063},{},[2064,2066,2071],{"type":31,"value":2065},"というように、実現する手段をどんどん掘り下げていきました。つまり、今列挙したことが",{"type":25,"tag":103,"props":2067,"children":2068},{},[2069],{"type":31,"value":2070},"必要なことだ",{"type":31,"value":2072},"と言うことができます。",{"type":25,"tag":313,"props":2074,"children":2076},{"id":2075},"原体験を振り返りなぜそう思ったのかの説得力を持たせる",[2077],{"type":31,"value":2078},"原体験を振り返り、なぜそう思ったのかの説得力を持たせる",{"type":25,"tag":33,"props":2080,"children":2081},{},[2082],{"type":31,"value":2083},"この抽象的なビジョンに説得力を持たせるために、自分の過去を振り返りました。",{"type":25,"tag":33,"props":2085,"children":2086},{},[2087],{"type":31,"value":2088},"もともとプログラミングは好きでしたが、最初は完全に自分用のツールを作っているだけでした。転機になったのは高校3年生の時です。",{"type":25,"tag":111,"props":2090,"children":2091},{},[2092],{"type":25,"tag":53,"props":2093,"children":2094},{},[2095,2124,2137],{"type":25,"tag":57,"props":2096,"children":2097},{},[2098,2103,2106,2108,2114,2116,2122],{"type":25,"tag":103,"props":2099,"children":2100},{},[2101],{"type":31,"value":2102},"高校3年生",{"type":25,"tag":745,"props":2104,"children":2105},{},[],{"type":31,"value":2107},"\n初めて他者のためにソフトウェアを開発。読書会運営用の",{"type":25,"tag":633,"props":2109,"children":2111},{"content":2110},"チャットアプリDiscord上で動く自動応答プログラム",[2112],{"type":31,"value":2113},"Discord Bot",{"type":31,"value":2115},"や、学習管理アプリ「",{"type":25,"tag":633,"props":2117,"children":2119},{"content":2118},"筆者が高校生時代に開発したアプリ",[2120],{"type":31,"value":2121},"FrogNote",{"type":31,"value":2123},"」を作りました。",{"type":25,"tag":57,"props":2125,"children":2126},{},[2127,2132,2135],{"type":25,"tag":103,"props":2128,"children":2129},{},[2130],{"type":31,"value":2131},"利用者からの反応",{"type":25,"tag":745,"props":2133,"children":2134},{},[],{"type":31,"value":2136},"\n「便利だ」という声をもらい、自分が作ったものが誰かの役に立つ喜びを知りました。",{"type":25,"tag":57,"props":2138,"children":2139},{},[2140,2145,2148],{"type":25,"tag":103,"props":2141,"children":2142},{},[2143],{"type":31,"value":2144},"挫折と学び",{"type":25,"tag":745,"props":2146,"children":2147},{},[],{"type":31,"value":2149},"\n同時に、パフォーマンスの課題でユーザーに不便をかけてしまう悔しさも経験。「良いアイデア」だけでは足りない、「実行品質」が伴わなければ価値は半減すると痛感しました。",{"type":25,"tag":33,"props":2151,"children":2152},{},[2153],{"type":31,"value":2154},"この経験が、「使っていて楽しいソフトウェアを作る」というビジョンの裏付けになりました。それにこのような経験の積み重ねから必要な手段が思いついたのだと思います。面接でも原体験を交えて話すことで、ただの綺麗事ではなく自分の言葉として伝えられるようになりました。",{"type":25,"tag":26,"props":2156,"children":2158},{"id":2157},"step-2自分の課題意識を深掘りする",[2159],{"type":31,"value":2160},"STEP 2：自分の課題意識を深掘りする",{"type":25,"tag":33,"props":2162,"children":2163},{},[2164],{"type":31,"value":2165},"これまで、「個人」の経験をベースに考えてきました。しかし、企業で働く場合は他者と協働する必要があります。企業が求めているベースはここにあるので、「個人」と「他者」でどのように動くかという部分にも注目します。",{"type":25,"tag":33,"props":2167,"children":2168},{},[2169],{"type":31,"value":2170},"私の場合ここで役立ったのは「実務経験」でした。なぜかというと、他者と協力して開発をした経験だからです。実務経験がなくとも、バイトやサークルなども可能です。",{"type":25,"tag":313,"props":2172,"children":2174},{"id":2173},"コミュニケーションコスト",[2175],{"type":31,"value":2173},{"type":25,"tag":33,"props":2177,"children":2178},{},[2179],{"type":31,"value":2180},"テキストでのやりとりに時間がかかる、口頭での情報共有をメモに残すだけでは細かいニュアンスが失われる。チームで開発していると、コミュニケーション自体がボトルネックになる場面を何度も目にしました。",{"type":25,"tag":33,"props":2182,"children":2183},{},[2184],{"type":31,"value":2185},"おそらくこれはサークルでもバイトでも共感できる人は多いと思います。",{"type":25,"tag":313,"props":2187,"children":2189},{"id":2188},"データの管理コスト",[2190],{"type":31,"value":2188},{"type":25,"tag":33,"props":2192,"children":2193},{},[2194],{"type":31,"value":2195},"Slack、Confluence、Google Driveなど、情報があちこちに散逸して探すだけで膨大な時間がかかる。実は、先ほど触れた学習管理アプリ「FrogNote」も、このようなデータ管理コストに課題を感じて作り始めたものでした。",{"type":25,"tag":33,"props":2197,"children":2198},{},[2199,2201,2206],{"type":31,"value":2200},"こうして振り返ると、",{"type":25,"tag":103,"props":2202,"children":2203},{},[2204],{"type":31,"value":2205},"ビジョン（楽しいソフトウェアを作る）→ 具体的な要件（ストレスなく目的に到達できる）→ 課題意識（コミュニケーション・データ管理のコスト）",{"type":31,"value":2207}," という一本の線が見えてきました。",{"type":25,"tag":26,"props":2209,"children":2211},{"id":2210},"step-3他の人が理解しやすいように構造化する",[2212],{"type":31,"value":2213},"STEP 3：他の人が理解しやすいように構造化する",{"type":25,"tag":33,"props":2215,"children":2216},{},[2217],{"type":31,"value":2218},"これまで私の例で説明してきました。かなり専門的な内容で、一部のエンジニア以外は理解できない内容が多かったと思います。",{"type":25,"tag":33,"props":2220,"children":2221},{},[2222],{"type":31,"value":2223},"これが問題です。この状態では、どんなに考えても相手は理解してくれません。\n面接では技術職でない人事担当者が同席することが多いです。全員に伝わる話し方をするために、内容を3つの視点に分けて整理しました。具体例を挙げます。先述した内容では、「顧客に提供する価値」のこと、「技術的」なこと、他職種との連携など「経営的」なことの３つに分けられると考えました。このように分けることで、技術的な内容がわからなくても、他の顧客、経営の話が頭に入りやすくなります。エンジニア面接の都合上、技術的な話はどうしても必要なので「技術」と「それ以外」で分ける対応をしたことになります。",{"type":25,"tag":33,"props":2225,"children":2226},{},[2227],{"type":31,"value":2228},"この時、先述した手順で考えた「何をすべきか」と「なぜそう思ったのかの原体験」を紐づけます。また、より具体化したり、願望に変えます。",{"type":25,"tag":313,"props":2230,"children":2232},{"id":2231},"顧客視点",[2233],{"type":31,"value":2234},"① 顧客視点",{"type":25,"tag":53,"props":2236,"children":2237},{},[2238,2251],{"type":25,"tag":57,"props":2239,"children":2240},{},[2241,2246,2249],{"type":25,"tag":103,"props":2242,"children":2243},{},[2244],{"type":31,"value":2245},"何をすべきか",{"type":25,"tag":745,"props":2247,"children":2248},{},[],{"type":31,"value":2250},"\n利用者が何を求めているか適切に要件を分析できる必要がある。そのためには、実際に利用者の立場で体験しないといけない。お客様の生の声を日常的にインプットし、仕様書に書かれていることの「一歩先」を考え、ユーザーの真の課題解決に繋がる改善案を提案できるようになりたい。",{"type":25,"tag":57,"props":2252,"children":2253},{},[2254,2259,2262],{"type":25,"tag":103,"props":2255,"children":2256},{},[2257],{"type":31,"value":2258},"原体験",{"type":25,"tag":745,"props":2260,"children":2261},{},[],{"type":31,"value":2263},"\nFrogNoteを開発した際、自分が「便利だろう」と思って作った機能と、実際に利用者が求めていた機能にズレがあった。利用者から「便利だ」と言われた機能は、自分が想定していなかった使い方をされていたものだった。",{"type":25,"tag":313,"props":2265,"children":2267},{"id":2266},"技術者視点",[2268],{"type":31,"value":2269},"② 技術者視点",{"type":25,"tag":33,"props":2271,"children":2272},{},[2273],{"type":31,"value":2274},"ここにエンジニア特有の難しい内容を入れ、他の内容を理解しやすくします。",{"type":25,"tag":53,"props":2276,"children":2277},{},[2278,2298],{"type":25,"tag":57,"props":2279,"children":2280},{},[2281,2285,2288,2290,2296],{"type":25,"tag":103,"props":2282,"children":2283},{},[2284],{"type":31,"value":2245},{"type":25,"tag":745,"props":2286,"children":2287},{},[],{"type":31,"value":2289},"\nドメインモデリングを行い、単一の責任を持つモジュールを設計できる必要がある。データ構造も工夫しないと将来的にパフォーマンスや拡張性の問題が起きる。テストしやすい設計も重要。また、実装に着手する段階から「なぜこの技術選定なのか」を自ら深掘りする習慣をつけることで、将来的に",{"type":25,"tag":633,"props":2291,"children":2293},{"content":2292},"システム全体の設計方針や技術選定を主導するエンジニア",[2294],{"type":31,"value":2295},"アーキテクト",{"type":31,"value":2297},"として大規模システムの意思決定に携われるようになりたい。",{"type":25,"tag":57,"props":2299,"children":2300},{},[2301,2305,2308,2310,2316],{"type":25,"tag":103,"props":2302,"children":2303},{},[2304],{"type":31,"value":2258},{"type":25,"tag":745,"props":2306,"children":2307},{},[],{"type":31,"value":2309},"\nインターン中、データベースアクセス処理で同期/非同期メソッドが混在している箇所があり、実装中に疑問を感じていたのに質問しなかった。コードレビュー時に初めてその理由を知り、「なぜこうなっているのか」を自ら深掘りする習慣の重要性を痛感した。また、FrogNoteではパフォーマンスの課題でユーザーに不便をかけた経験があり、設計段階での技術的な意思決定の重要性を学んだ。さらに、インターン先では多くの",{"type":25,"tag":633,"props":2311,"children":2313},{"content":2312},"コードの変更を自動でテスト・検証する仕組み（継続的インテグレーション）",[2314],{"type":31,"value":2315},"CI",{"type":31,"value":2317},"が高速に動作したり、テスト環境を自動で立ち上げる仕組みが整備されていたことに驚いた。このような開発基盤があるからこそプロダクトの価値を高める活動に注力できるのだと実感し、開発時に抱えている問題は慣れると気づくことが難しいため、常に現状を疑うことが大事だと再確認した。",{"type":25,"tag":313,"props":2319,"children":2321},{"id":2320},"経営者視点",[2322],{"type":31,"value":2323},"③ 経営者視点",{"type":25,"tag":53,"props":2325,"children":2326},{},[2327,2339],{"type":25,"tag":57,"props":2328,"children":2329},{},[2330,2334,2337],{"type":25,"tag":103,"props":2331,"children":2332},{},[2333],{"type":31,"value":2245},{"type":25,"tag":745,"props":2335,"children":2336},{},[],{"type":31,"value":2338},"\nサーバサイドが複雑になるとUIも複雑になり、結果的に利用者は混乱する。つまり、技術的な負債はそのままユーザー体験の悪化とビジネス上の損失に繋がる。「この施策を行うことでどのくらい開発が効率化され、どのくらいユーザへ価値が届けられ、どのくらい効果が得られるか」を常に意識して問題提起・解決を行う必要がある。",{"type":25,"tag":57,"props":2340,"children":2341},{},[2342,2346,2349],{"type":25,"tag":103,"props":2343,"children":2344},{},[2345],{"type":31,"value":2258},{"type":25,"tag":745,"props":2347,"children":2348},{},[],{"type":31,"value":2350},"\n個人開発でFrogNoteを運用する中で、機能を増やすほど保守コストが増え、本当に必要な改善に手が回らなくなる経験をした。限られたリソースの中で何を優先すべきかを考える視点が身についた。このような経験から、個人サービスを商用化まで実践し、コスト意識と事業を俯瞰する視点を培いたいと考えている。",{"type":25,"tag":26,"props":2352,"children":2354},{"id":2353},"step-4実際に企業に当てはめてみる",[2355],{"type":31,"value":2356},"STEP 4：実際に企業に当てはめてみる",{"type":25,"tag":33,"props":2358,"children":2359},{},[2360],{"type":31,"value":2361},"ここまでで「自分がやりたいこと」を深掘りしてきました。これは志望理由書だけでなく面接の時にも役立つので、後から見れるように文章化してまとめると良いと思います。面接時の深掘りを想定して木構造にして管理するのもおすすめです。",{"type":25,"tag":313,"props":2363,"children":2365},{"id":2364},"例業務効率化saasを開発している企業の場合",[2366,2368,2374],{"type":31,"value":2367},"例）業務効率化",{"type":25,"tag":633,"props":2369,"children":2371},{"content":2370},"インターネット経由で利用できるソフトウェアサービス",[2372],{"type":31,"value":2373},"SaaS",{"type":31,"value":2375},"を開発している企業の場合",{"type":25,"tag":335,"props":2377,"children":2379},{"id":2378},"企業がやりたいことを調べる",[2380],{"type":31,"value":2378},{"type":25,"tag":33,"props":2382,"children":2383},{},[2384],{"type":31,"value":2385},"企業が何をやりたいかは公式のホームページを見ればわかるはずです。\n具体的に何を見るかを列挙します。",{"type":25,"tag":53,"props":2387,"children":2388},{},[2389,2394,2399,2404],{"type":25,"tag":57,"props":2390,"children":2391},{},[2392],{"type":31,"value":2393},"会社説明会資料（以下に書かれている全てが網羅的にわかります。企業が伝えたいことは全て詰まっています）",{"type":25,"tag":57,"props":2395,"children":2396},{},[2397],{"type":31,"value":2398},"企業理念(STEP 1で作ったような抽象的な企業のやりたいことがわかります)",{"type":25,"tag":57,"props":2400,"children":2401},{},[2402],{"type":31,"value":2403},"採用方針・募集要項（どういう人が来てほしいかの要望がわかります）",{"type":25,"tag":57,"props":2405,"children":2406},{},[2407],{"type":31,"value":2408},"IR情報（企業が力を入れたいことや財務状況など現実的な話がわかります）",{"type":25,"tag":2410,"props":2411,"children":2413},"h5",{"id":2412},"例",[2414],{"type":31,"value":2412},{"type":25,"tag":33,"props":2416,"children":2417},{},[2418],{"type":31,"value":2419},"この企業は、組織内の情報共有やコミュニケーションを円滑にしつつ、伝統的な業界の複雑な業務フローをエンドツーエンドでソフトウェアに置き換えている。ユーザーが細かい仕組みを知らなくても目的を達成できるサービスを提供しており、自分が課題に感じていた「コミュニケーションコスト」「データ管理コスト」の削減に正面から取り組んでいる事業である。",{"type":25,"tag":335,"props":2421,"children":2423},{"id":2422},"これまでのステップで考えた内容と関連付ける",[2424],{"type":31,"value":2422},{"type":25,"tag":33,"props":2426,"children":2427},{},[2428],{"type":31,"value":2429},"自分がやりたいことと企業がやりたいことを関連づけましょう。",{"type":25,"tag":33,"props":2431,"children":2432},{},[2433],{"type":31,"value":2434},"「私が課題に感じているコミュニケーション・データ管理のコスト削減に取り組みつつ、業務の仕組みごとソフトウェアで改善してユーザーが本来の目的に集中できる体験を作っている点が、自分の目指すソフトウェア像と一致した」",{"type":25,"tag":33,"props":2436,"children":2437},{},[2438,2440,2445,2446,2451],{"type":31,"value":2439},"このように、自己分析で見つけた",{"type":25,"tag":103,"props":2441,"children":2442},{},[2443],{"type":31,"value":2444},"課題意識",{"type":31,"value":1086},{"type":25,"tag":103,"props":2447,"children":2448},{},[2449],{"type":31,"value":2450},"企業の事業内容",{"type":31,"value":2452},"をリンクさせることで、「なぜこの会社なのか」に対する答えが自然と出てくるようになります。",{"type":25,"tag":26,"props":2454,"children":2455},{"id":1746},[2456],{"type":31,"value":1746},{"type":25,"tag":33,"props":2458,"children":2459},{},[2460],{"type":31,"value":2461},"私が実践した自己分析のプロセスを振り返ると、こんな流れでした。",{"type":25,"tag":111,"props":2463,"children":2464},{},[2465],{"type":25,"tag":53,"props":2466,"children":2467},{},[2468,2477,2486,2495],{"type":25,"tag":57,"props":2469,"children":2470},{},[2471,2475],{"type":25,"tag":103,"props":2472,"children":2473},{},[2474],{"type":31,"value":1845},{"type":31,"value":2476},"\n漠然としたやりたいことを言語化し、具体的な要件に落とし込む",{"type":25,"tag":57,"props":2478,"children":2479},{},[2480,2484],{"type":25,"tag":103,"props":2481,"children":2482},{},[2483],{"type":31,"value":2160},{"type":31,"value":2485},"\nコミュニケーションコスト、データ管理コストに問題意識がある、など……",{"type":25,"tag":57,"props":2487,"children":2488},{},[2489,2493],{"type":25,"tag":103,"props":2490,"children":2491},{},[2492],{"type":31,"value":2213},{"type":31,"value":2494},"\n顧客・技術・経営の3つの視点で整理する、など……",{"type":25,"tag":57,"props":2496,"children":2497},{},[2498,2502],{"type":25,"tag":103,"props":2499,"children":2500},{},[2501],{"type":31,"value":2356},{"type":31,"value":2503},"\n課題意識と企業の方向性をリンクさせる",{"type":25,"tag":33,"props":2505,"children":2506},{},[2507,2509,2514],{"type":31,"value":2508},"完璧な答えが一発で出ることはないと思います。私もこの過程を何度も行ったり来たりしながら、少しずつ言語化していきました。大事なのは、",{"type":25,"tag":103,"props":2510,"children":2511},{},[2512],{"type":31,"value":2513},"抽象から具体へ逆算するプロセスを回し続けること",{"type":31,"value":2515},"です。",{"type":25,"tag":33,"props":2517,"children":2518},{},[2519],{"type":31,"value":2520},"この記事が、「自分は何をしたいんだ」と悩んでいる方の参考になれば嬉しいです。",{"title":8,"searchDepth":1149,"depth":1149,"links":2522},[2523,2524,2529,2533,2538,2542],{"id":28,"depth":1149,"text":28},{"id":1842,"depth":1149,"text":1845,"children":2525},[2526,2527,2528],{"id":1848,"depth":1159,"text":1851},{"id":1882,"depth":1159,"text":1882},{"id":2075,"depth":1159,"text":2078},{"id":2157,"depth":1149,"text":2160,"children":2530},[2531,2532],{"id":2173,"depth":1159,"text":2173},{"id":2188,"depth":1159,"text":2188},{"id":2210,"depth":1149,"text":2213,"children":2534},[2535,2536,2537],{"id":2231,"depth":1159,"text":2234},{"id":2266,"depth":1159,"text":2269},{"id":2320,"depth":1159,"text":2323},{"id":2353,"depth":1149,"text":2356,"children":2539},[2540],{"id":2364,"depth":1159,"text":2541},"例）業務効率化SaaSを開発している企業の場合",{"id":1746,"depth":1149,"text":1746},"content:articles:career:what-do-i-want-to-do.md","articles/career/what-do-i-want-to-do.md","articles/career/what-do-i-want-to-do",{"_path":2547,"_dir":2548,"_draft":7,"_partial":7,"_locale":8,"title":2549,"description":2550,"date":2551,"tags":2552,"rowTypeId":18,"sitemap":2558,"body":2559,"_type":1180,"_id":5256,"_source":1182,"_file":5257,"_stem":5258,"_extension":1185},"/articles/tech/blazor/diff-detectable-object","blazor","ObservableObjectとDiffDetectableObjectで「変更された？」を自動判定する","Blazor WebAssemblyで「保存ボタンを押すべきか」を自動判定する仕組みを、ObservableObjectとDiffDetectableObjectという2つのシンプルなクラスで実装しました。MVVM ToolkitのObservableObjectとの違いや、イベント駆動で状態フラグを同期させる設計のコツを解説します。","2026-04-14",[2553,2554,2555,2556,2557],"C#","MVVM","Blazor","INotifyPropertyChanged","PICOM",{"loc":2547,"lastmod":2551,"priority":18},{"type":22,"children":2560,"toc":5244},[2561,2565,2592,2620,2647,2653,2672,3220,3231,3399,3405,3418,3430,3488,3494,3513,3942,3998,4017,4136,4141,4151,4495,4514,4520,4525,4799,4835,4989,4995,5007,5165,5175,5196,5200,5238],{"type":25,"tag":26,"props":2562,"children":2563},{"id":28},[2564],{"type":31,"value":28},{"type":25,"tag":33,"props":2566,"children":2567},{},[2568,2570,2575,2577,2583,2585,2590],{"type":31,"value":2569},"楽譜エディタを作っていて、地味に困るのが「",{"type":25,"tag":103,"props":2571,"children":2572},{},[2573],{"type":31,"value":2574},"未保存の変更があるかどうか",{"type":31,"value":2576},"」の判定です。タイトルバーに",{"type":25,"tag":529,"props":2578,"children":2580},{"className":2579},[],[2581],{"type":31,"value":2582},"*",{"type":31,"value":2584},"を出したり、保存ボタンの活性を切り替えたり、タブを閉じようとしたら「未保存の変更があります」と警告したい。これをやるには、現在のデータが",{"type":25,"tag":103,"props":2586,"children":2587},{},[2588],{"type":31,"value":2589},"初期状態から変更されているか",{"type":31,"value":2591},"を常に知っておく必要があります。",{"type":25,"tag":33,"props":2593,"children":2594},{},[2595,2597,2603,2604,2610,2612,2618],{"type":31,"value":2596},"PICOMでは、この判定を",{"type":25,"tag":529,"props":2598,"children":2600},{"className":2599},[],[2601],{"type":31,"value":2602},"ObservableObject",{"type":31,"value":1086},{"type":25,"tag":529,"props":2605,"children":2607},{"className":2606},[],[2608],{"type":31,"value":2609},"DiffDetectableObject\u003CT>",{"type":31,"value":2611},"という2つの小さなクラスで実装しました。この記事では、その設計と、",{"type":25,"tag":633,"props":2613,"children":2615},{"content":2614},"Microsoftが公開しているMVVM用のユーティリティライブラリ。ObservableObjectやRelayCommandなどを提供する",[2616],{"type":31,"value":2617},"MVVM Community Toolkit",{"type":31,"value":2619},"との違いについて書きます。",{"type":25,"tag":39,"props":2621,"children":2622},{},[2623],{"type":25,"tag":33,"props":2624,"children":2625},{},[2626,2631,2633,2638,2640,2645],{"type":25,"tag":529,"props":2627,"children":2629},{"className":2628},[],[2630],{"type":31,"value":2556},{"type":31,"value":2632},"を実装した",{"type":25,"tag":529,"props":2634,"children":2636},{"className":2635},[],[2637],{"type":31,"value":2602},{"type":31,"value":2639},"をベースに、初期スナップショットと現在のオブジェクトを持つ",{"type":25,"tag":529,"props":2641,"children":2643},{"className":2642},[],[2644],{"type":31,"value":2609},{"type":31,"value":2646},"を被せることで、差分の有無をイベントで通知できるようにしました。保存ボタンの活性判定や、変更通知バッジの表示など、UI側は状態フラグをそのままバインドするだけで済みます。",{"type":25,"tag":26,"props":2648,"children":2650},{"id":2649},"observableobject最小限のプロパティ変更通知",[2651],{"type":31,"value":2652},"ObservableObject：最小限のプロパティ変更通知",{"type":25,"tag":33,"props":2654,"children":2655},{},[2656,2658,2663,2665,2670],{"type":31,"value":2657},"まずは下敷きとなる",{"type":25,"tag":529,"props":2659,"children":2661},{"className":2660},[],[2662],{"type":31,"value":2602},{"type":31,"value":2664},"です。中身は",{"type":25,"tag":529,"props":2666,"children":2668},{"className":2667},[],[2669],{"type":31,"value":2556},{"type":31,"value":2671},"の実装だけで、めちゃくちゃ小さいです。",{"type":25,"tag":524,"props":2673,"children":2677},{"className":2674,"code":2675,"language":2676,"meta":8,"style":8},"language-csharp shiki shiki-themes vitesse-dark","using System.ComponentModel;\nusing System.Runtime.CompilerServices;\n\nnamespace Sample;\n\npublic abstract class ObservableObject : INotifyPropertyChanged\n{\n    public event PropertyChangedEventHandler? PropertyChanged;\n\n    protected void SetProperty\u003CT>(ref T prop, T value, [CallerMemberName] string? name = null)\n    {\n        if (name is null) throw new Exception();\n        prop = value;\n        NotifyPropertyChanged(name);\n    }\n\n    protected void NotifyPropertyChanged(string name)\n    {\n        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));\n    }\n}\n","csharp",[2678],{"type":25,"tag":529,"props":2679,"children":2680},{"__ignoreMap":8},[2681,2714,2747,2756,2775,2783,2817,2826,2860,2868,2979,2988,3042,3063,3086,3095,3103,3137,3145,3203,3211],{"type":25,"tag":2682,"props":2683,"children":2685},"span",{"class":2684,"line":18},"line",[2686,2692,2698,2704,2709],{"type":25,"tag":2682,"props":2687,"children":2689},{"style":2688},"--shiki-default:#4D9375",[2690],{"type":31,"value":2691},"using",{"type":25,"tag":2682,"props":2693,"children":2695},{"style":2694},"--shiki-default:#5DA994",[2696],{"type":31,"value":2697}," System",{"type":25,"tag":2682,"props":2699,"children":2701},{"style":2700},"--shiki-default:#666666",[2702],{"type":31,"value":2703},".",{"type":25,"tag":2682,"props":2705,"children":2706},{"style":2694},[2707],{"type":31,"value":2708},"ComponentModel",{"type":25,"tag":2682,"props":2710,"children":2711},{"style":2700},[2712],{"type":31,"value":2713},";\n",{"type":25,"tag":2682,"props":2715,"children":2716},{"class":2684,"line":1149},[2717,2721,2725,2729,2734,2738,2743],{"type":25,"tag":2682,"props":2718,"children":2719},{"style":2688},[2720],{"type":31,"value":2691},{"type":25,"tag":2682,"props":2722,"children":2723},{"style":2694},[2724],{"type":31,"value":2697},{"type":25,"tag":2682,"props":2726,"children":2727},{"style":2700},[2728],{"type":31,"value":2703},{"type":25,"tag":2682,"props":2730,"children":2731},{"style":2694},[2732],{"type":31,"value":2733},"Runtime",{"type":25,"tag":2682,"props":2735,"children":2736},{"style":2700},[2737],{"type":31,"value":2703},{"type":25,"tag":2682,"props":2739,"children":2740},{"style":2694},[2741],{"type":31,"value":2742},"CompilerServices",{"type":25,"tag":2682,"props":2744,"children":2745},{"style":2700},[2746],{"type":31,"value":2713},{"type":25,"tag":2682,"props":2748,"children":2749},{"class":2684,"line":1159},[2750],{"type":25,"tag":2682,"props":2751,"children":2753},{"emptyLinePlaceholder":2752},true,[2754],{"type":31,"value":2755},"\n",{"type":25,"tag":2682,"props":2757,"children":2759},{"class":2684,"line":2758},4,[2760,2766,2771],{"type":25,"tag":2682,"props":2761,"children":2763},{"style":2762},"--shiki-default:#CB7676",[2764],{"type":31,"value":2765},"namespace",{"type":25,"tag":2682,"props":2767,"children":2768},{"style":2694},[2769],{"type":31,"value":2770}," Sample",{"type":25,"tag":2682,"props":2772,"children":2773},{"style":2700},[2774],{"type":31,"value":2713},{"type":25,"tag":2682,"props":2776,"children":2778},{"class":2684,"line":2777},5,[2779],{"type":25,"tag":2682,"props":2780,"children":2781},{"emptyLinePlaceholder":2752},[2782],{"type":31,"value":2755},{"type":25,"tag":2682,"props":2784,"children":2786},{"class":2684,"line":2785},6,[2787,2792,2797,2802,2807,2812],{"type":25,"tag":2682,"props":2788,"children":2789},{"style":2762},[2790],{"type":31,"value":2791},"public",{"type":25,"tag":2682,"props":2793,"children":2794},{"style":2762},[2795],{"type":31,"value":2796}," abstract",{"type":25,"tag":2682,"props":2798,"children":2799},{"style":2762},[2800],{"type":31,"value":2801}," class",{"type":25,"tag":2682,"props":2803,"children":2804},{"style":2694},[2805],{"type":31,"value":2806}," ObservableObject",{"type":25,"tag":2682,"props":2808,"children":2809},{"style":2700},[2810],{"type":31,"value":2811}," :",{"type":25,"tag":2682,"props":2813,"children":2814},{"style":2694},[2815],{"type":31,"value":2816}," INotifyPropertyChanged\n",{"type":25,"tag":2682,"props":2818,"children":2820},{"class":2684,"line":2819},7,[2821],{"type":25,"tag":2682,"props":2822,"children":2823},{"style":2700},[2824],{"type":31,"value":2825},"{\n",{"type":25,"tag":2682,"props":2827,"children":2829},{"class":2684,"line":2828},8,[2830,2835,2840,2845,2850,2856],{"type":25,"tag":2682,"props":2831,"children":2832},{"style":2762},[2833],{"type":31,"value":2834},"    public",{"type":25,"tag":2682,"props":2836,"children":2837},{"style":2762},[2838],{"type":31,"value":2839}," event",{"type":25,"tag":2682,"props":2841,"children":2842},{"style":2694},[2843],{"type":31,"value":2844}," PropertyChangedEventHandler",{"type":25,"tag":2682,"props":2846,"children":2847},{"style":2700},[2848],{"type":31,"value":2849},"?",{"type":25,"tag":2682,"props":2851,"children":2853},{"style":2852},"--shiki-default:#80A665",[2854],{"type":31,"value":2855}," PropertyChanged",{"type":25,"tag":2682,"props":2857,"children":2858},{"style":2700},[2859],{"type":31,"value":2713},{"type":25,"tag":2682,"props":2861,"children":2863},{"class":2684,"line":2862},9,[2864],{"type":25,"tag":2682,"props":2865,"children":2866},{"emptyLinePlaceholder":2752},[2867],{"type":31,"value":2755},{"type":25,"tag":2682,"props":2869,"children":2871},{"class":2684,"line":2870},10,[2872,2877,2882,2887,2892,2897,2902,2907,2912,2917,2922,2926,2931,2935,2940,2945,2950,2955,2959,2964,2969,2974],{"type":25,"tag":2682,"props":2873,"children":2874},{"style":2762},[2875],{"type":31,"value":2876},"    protected",{"type":25,"tag":2682,"props":2878,"children":2879},{"style":2688},[2880],{"type":31,"value":2881}," void",{"type":25,"tag":2682,"props":2883,"children":2884},{"style":2852},[2885],{"type":31,"value":2886}," SetProperty",{"type":25,"tag":2682,"props":2888,"children":2889},{"style":2700},[2890],{"type":31,"value":2891},"\u003C",{"type":25,"tag":2682,"props":2893,"children":2894},{"style":2694},[2895],{"type":31,"value":2896},"T",{"type":25,"tag":2682,"props":2898,"children":2899},{"style":2700},[2900],{"type":31,"value":2901},">(",{"type":25,"tag":2682,"props":2903,"children":2904},{"style":2762},[2905],{"type":31,"value":2906},"ref",{"type":25,"tag":2682,"props":2908,"children":2909},{"style":2694},[2910],{"type":31,"value":2911}," T",{"type":25,"tag":2682,"props":2913,"children":2914},{"style":2852},[2915],{"type":31,"value":2916}," prop",{"type":25,"tag":2682,"props":2918,"children":2919},{"style":2700},[2920],{"type":31,"value":2921},",",{"type":25,"tag":2682,"props":2923,"children":2924},{"style":2694},[2925],{"type":31,"value":2911},{"type":25,"tag":2682,"props":2927,"children":2928},{"style":2852},[2929],{"type":31,"value":2930}," value",{"type":25,"tag":2682,"props":2932,"children":2933},{"style":2700},[2934],{"type":31,"value":2921},{"type":25,"tag":2682,"props":2936,"children":2937},{"style":2700},[2938],{"type":31,"value":2939}," [",{"type":25,"tag":2682,"props":2941,"children":2942},{"style":2694},[2943],{"type":31,"value":2944},"CallerMemberName",{"type":25,"tag":2682,"props":2946,"children":2947},{"style":2700},[2948],{"type":31,"value":2949},"]",{"type":25,"tag":2682,"props":2951,"children":2952},{"style":2688},[2953],{"type":31,"value":2954}," string",{"type":25,"tag":2682,"props":2956,"children":2957},{"style":2700},[2958],{"type":31,"value":2849},{"type":25,"tag":2682,"props":2960,"children":2961},{"style":2852},[2962],{"type":31,"value":2963}," name",{"type":25,"tag":2682,"props":2965,"children":2966},{"style":2700},[2967],{"type":31,"value":2968}," =",{"type":25,"tag":2682,"props":2970,"children":2971},{"style":2762},[2972],{"type":31,"value":2973}," null",{"type":25,"tag":2682,"props":2975,"children":2976},{"style":2700},[2977],{"type":31,"value":2978},")\n",{"type":25,"tag":2682,"props":2980,"children":2982},{"class":2684,"line":2981},11,[2983],{"type":25,"tag":2682,"props":2984,"children":2985},{"style":2700},[2986],{"type":31,"value":2987},"    {\n",{"type":25,"tag":2682,"props":2989,"children":2991},{"class":2684,"line":2990},12,[2992,2997,3002,3008,3013,3017,3022,3027,3032,3037],{"type":25,"tag":2682,"props":2993,"children":2994},{"style":2688},[2995],{"type":31,"value":2996},"        if",{"type":25,"tag":2682,"props":2998,"children":2999},{"style":2700},[3000],{"type":31,"value":3001}," (",{"type":25,"tag":2682,"props":3003,"children":3005},{"style":3004},"--shiki-default:#BD976A",[3006],{"type":31,"value":3007},"name",{"type":25,"tag":2682,"props":3009,"children":3010},{"style":2762},[3011],{"type":31,"value":3012}," is",{"type":25,"tag":2682,"props":3014,"children":3015},{"style":2762},[3016],{"type":31,"value":2973},{"type":25,"tag":2682,"props":3018,"children":3019},{"style":2700},[3020],{"type":31,"value":3021},")",{"type":25,"tag":2682,"props":3023,"children":3024},{"style":2688},[3025],{"type":31,"value":3026}," throw",{"type":25,"tag":2682,"props":3028,"children":3029},{"style":2762},[3030],{"type":31,"value":3031}," new",{"type":25,"tag":2682,"props":3033,"children":3034},{"style":2694},[3035],{"type":31,"value":3036}," Exception",{"type":25,"tag":2682,"props":3038,"children":3039},{"style":2700},[3040],{"type":31,"value":3041},"();\n",{"type":25,"tag":2682,"props":3043,"children":3045},{"class":2684,"line":3044},13,[3046,3051,3055,3059],{"type":25,"tag":2682,"props":3047,"children":3048},{"style":3004},[3049],{"type":31,"value":3050},"        prop",{"type":25,"tag":2682,"props":3052,"children":3053},{"style":2700},[3054],{"type":31,"value":2968},{"type":25,"tag":2682,"props":3056,"children":3057},{"style":3004},[3058],{"type":31,"value":2930},{"type":25,"tag":2682,"props":3060,"children":3061},{"style":2700},[3062],{"type":31,"value":2713},{"type":25,"tag":2682,"props":3064,"children":3066},{"class":2684,"line":3065},14,[3067,3072,3077,3081],{"type":25,"tag":2682,"props":3068,"children":3069},{"style":2852},[3070],{"type":31,"value":3071},"        NotifyPropertyChanged",{"type":25,"tag":2682,"props":3073,"children":3074},{"style":2700},[3075],{"type":31,"value":3076},"(",{"type":25,"tag":2682,"props":3078,"children":3079},{"style":3004},[3080],{"type":31,"value":3007},{"type":25,"tag":2682,"props":3082,"children":3083},{"style":2700},[3084],{"type":31,"value":3085},");\n",{"type":25,"tag":2682,"props":3087,"children":3089},{"class":2684,"line":3088},15,[3090],{"type":25,"tag":2682,"props":3091,"children":3092},{"style":2700},[3093],{"type":31,"value":3094},"    }\n",{"type":25,"tag":2682,"props":3096,"children":3098},{"class":2684,"line":3097},16,[3099],{"type":25,"tag":2682,"props":3100,"children":3101},{"emptyLinePlaceholder":2752},[3102],{"type":31,"value":2755},{"type":25,"tag":2682,"props":3104,"children":3106},{"class":2684,"line":3105},17,[3107,3111,3115,3120,3124,3129,3133],{"type":25,"tag":2682,"props":3108,"children":3109},{"style":2762},[3110],{"type":31,"value":2876},{"type":25,"tag":2682,"props":3112,"children":3113},{"style":2688},[3114],{"type":31,"value":2881},{"type":25,"tag":2682,"props":3116,"children":3117},{"style":2852},[3118],{"type":31,"value":3119}," NotifyPropertyChanged",{"type":25,"tag":2682,"props":3121,"children":3122},{"style":2700},[3123],{"type":31,"value":3076},{"type":25,"tag":2682,"props":3125,"children":3126},{"style":2688},[3127],{"type":31,"value":3128},"string",{"type":25,"tag":2682,"props":3130,"children":3131},{"style":2852},[3132],{"type":31,"value":2963},{"type":25,"tag":2682,"props":3134,"children":3135},{"style":2700},[3136],{"type":31,"value":2978},{"type":25,"tag":2682,"props":3138,"children":3140},{"class":2684,"line":3139},18,[3141],{"type":25,"tag":2682,"props":3142,"children":3143},{"style":2700},[3144],{"type":31,"value":2987},{"type":25,"tag":2682,"props":3146,"children":3148},{"class":2684,"line":3147},19,[3149,3154,3158,3162,3167,3171,3177,3181,3185,3190,3194,3198],{"type":25,"tag":2682,"props":3150,"children":3151},{"style":3004},[3152],{"type":31,"value":3153},"        PropertyChanged",{"type":25,"tag":2682,"props":3155,"children":3156},{"style":2762},[3157],{"type":31,"value":2849},{"type":25,"tag":2682,"props":3159,"children":3160},{"style":2700},[3161],{"type":31,"value":2703},{"type":25,"tag":2682,"props":3163,"children":3164},{"style":2852},[3165],{"type":31,"value":3166},"Invoke",{"type":25,"tag":2682,"props":3168,"children":3169},{"style":2700},[3170],{"type":31,"value":3076},{"type":25,"tag":2682,"props":3172,"children":3174},{"style":3173},"--shiki-default:#C99076",[3175],{"type":31,"value":3176},"this",{"type":25,"tag":2682,"props":3178,"children":3179},{"style":2700},[3180],{"type":31,"value":2921},{"type":25,"tag":2682,"props":3182,"children":3183},{"style":2762},[3184],{"type":31,"value":3031},{"type":25,"tag":2682,"props":3186,"children":3187},{"style":2694},[3188],{"type":31,"value":3189}," PropertyChangedEventArgs",{"type":25,"tag":2682,"props":3191,"children":3192},{"style":2700},[3193],{"type":31,"value":3076},{"type":25,"tag":2682,"props":3195,"children":3196},{"style":3004},[3197],{"type":31,"value":3007},{"type":25,"tag":2682,"props":3199,"children":3200},{"style":2700},[3201],{"type":31,"value":3202},"));\n",{"type":25,"tag":2682,"props":3204,"children":3206},{"class":2684,"line":3205},20,[3207],{"type":25,"tag":2682,"props":3208,"children":3209},{"style":2700},[3210],{"type":31,"value":3094},{"type":25,"tag":2682,"props":3212,"children":3214},{"class":2684,"line":3213},21,[3215],{"type":25,"tag":2682,"props":3216,"children":3217},{"style":2700},[3218],{"type":31,"value":3219},"}\n",{"type":25,"tag":33,"props":3221,"children":3222},{},[3223,3229],{"type":25,"tag":529,"props":3224,"children":3226},{"className":3225},[],[3227],{"type":31,"value":3228},"[CallerMemberName]",{"type":31,"value":3230},"でプロパティ名を自動取得しているので、派生クラスでは次のように書けます。",{"type":25,"tag":524,"props":3232,"children":3234},{"className":2674,"code":3233,"language":2676,"meta":8,"style":8},"public class MusicMetadata : ObservableObject\n{\n    private string _title = \"\";\n    public string Title\n    {\n        get => _title;\n        set => SetProperty(ref _title, value);\n    }\n}\n",[3235],{"type":25,"tag":529,"props":3236,"children":3237},{"__ignoreMap":8},[3238,3263,3270,3301,3317,3324,3345,3385,3392],{"type":25,"tag":2682,"props":3239,"children":3240},{"class":2684,"line":18},[3241,3245,3249,3254,3258],{"type":25,"tag":2682,"props":3242,"children":3243},{"style":2762},[3244],{"type":31,"value":2791},{"type":25,"tag":2682,"props":3246,"children":3247},{"style":2762},[3248],{"type":31,"value":2801},{"type":25,"tag":2682,"props":3250,"children":3251},{"style":2694},[3252],{"type":31,"value":3253}," MusicMetadata",{"type":25,"tag":2682,"props":3255,"children":3256},{"style":2700},[3257],{"type":31,"value":2811},{"type":25,"tag":2682,"props":3259,"children":3260},{"style":2694},[3261],{"type":31,"value":3262}," ObservableObject\n",{"type":25,"tag":2682,"props":3264,"children":3265},{"class":2684,"line":1149},[3266],{"type":25,"tag":2682,"props":3267,"children":3268},{"style":2700},[3269],{"type":31,"value":2825},{"type":25,"tag":2682,"props":3271,"children":3272},{"class":2684,"line":1159},[3273,3278,3282,3287,3291,3297],{"type":25,"tag":2682,"props":3274,"children":3275},{"style":2762},[3276],{"type":31,"value":3277},"    private",{"type":25,"tag":2682,"props":3279,"children":3280},{"style":2688},[3281],{"type":31,"value":2954},{"type":25,"tag":2682,"props":3283,"children":3284},{"style":2852},[3285],{"type":31,"value":3286}," _title",{"type":25,"tag":2682,"props":3288,"children":3289},{"style":2700},[3290],{"type":31,"value":2968},{"type":25,"tag":2682,"props":3292,"children":3294},{"style":3293},"--shiki-default:#C98A7D77",[3295],{"type":31,"value":3296}," \"\"",{"type":25,"tag":2682,"props":3298,"children":3299},{"style":2700},[3300],{"type":31,"value":2713},{"type":25,"tag":2682,"props":3302,"children":3303},{"class":2684,"line":2758},[3304,3308,3312],{"type":25,"tag":2682,"props":3305,"children":3306},{"style":2762},[3307],{"type":31,"value":2834},{"type":25,"tag":2682,"props":3309,"children":3310},{"style":2688},[3311],{"type":31,"value":2954},{"type":25,"tag":2682,"props":3313,"children":3314},{"style":2852},[3315],{"type":31,"value":3316}," Title\n",{"type":25,"tag":2682,"props":3318,"children":3319},{"class":2684,"line":2777},[3320],{"type":25,"tag":2682,"props":3321,"children":3322},{"style":2700},[3323],{"type":31,"value":2987},{"type":25,"tag":2682,"props":3325,"children":3326},{"class":2684,"line":2785},[3327,3332,3337,3341],{"type":25,"tag":2682,"props":3328,"children":3329},{"style":2762},[3330],{"type":31,"value":3331},"        get",{"type":25,"tag":2682,"props":3333,"children":3334},{"style":2762},[3335],{"type":31,"value":3336}," =>",{"type":25,"tag":2682,"props":3338,"children":3339},{"style":3004},[3340],{"type":31,"value":3286},{"type":25,"tag":2682,"props":3342,"children":3343},{"style":2700},[3344],{"type":31,"value":2713},{"type":25,"tag":2682,"props":3346,"children":3347},{"class":2684,"line":2819},[3348,3353,3357,3361,3365,3369,3373,3377,3381],{"type":25,"tag":2682,"props":3349,"children":3350},{"style":2762},[3351],{"type":31,"value":3352},"        set",{"type":25,"tag":2682,"props":3354,"children":3355},{"style":2762},[3356],{"type":31,"value":3336},{"type":25,"tag":2682,"props":3358,"children":3359},{"style":2852},[3360],{"type":31,"value":2886},{"type":25,"tag":2682,"props":3362,"children":3363},{"style":2700},[3364],{"type":31,"value":3076},{"type":25,"tag":2682,"props":3366,"children":3367},{"style":2762},[3368],{"type":31,"value":2906},{"type":25,"tag":2682,"props":3370,"children":3371},{"style":3004},[3372],{"type":31,"value":3286},{"type":25,"tag":2682,"props":3374,"children":3375},{"style":2700},[3376],{"type":31,"value":2921},{"type":25,"tag":2682,"props":3378,"children":3379},{"style":3004},[3380],{"type":31,"value":2930},{"type":25,"tag":2682,"props":3382,"children":3383},{"style":2700},[3384],{"type":31,"value":3085},{"type":25,"tag":2682,"props":3386,"children":3387},{"class":2684,"line":2828},[3388],{"type":25,"tag":2682,"props":3389,"children":3390},{"style":2700},[3391],{"type":31,"value":3094},{"type":25,"tag":2682,"props":3393,"children":3394},{"class":2684,"line":2862},[3395],{"type":25,"tag":2682,"props":3396,"children":3397},{"style":2700},[3398],{"type":31,"value":3219},{"type":25,"tag":313,"props":3400,"children":3402},{"id":3401},"mvvm-community-toolkit-ではダメなのか",[3403],{"type":31,"value":3404},"MVVM Community Toolkit ではダメなのか",{"type":25,"tag":33,"props":3406,"children":3407},{},[3408,3410,3416],{"type":31,"value":3409},"同じことはMVVM Community Toolkitの",{"type":25,"tag":529,"props":3411,"children":3413},{"className":3412},[],[3414],{"type":31,"value":3415},"[ObservableProperty]",{"type":31,"value":3417},"でソースジェネレータ任せにできます。実際、本格的な.NETアプリならそれを使う方が記述量は少なくなります。",{"type":25,"tag":33,"props":3419,"children":3420},{},[3421,3423,3428],{"type":31,"value":3422},"PICOMでToolkitを使わなかった理由は、この程度であれば自前実装した方が良いと判断したからです。実際、PICOMの",{"type":25,"tag":529,"props":3424,"children":3426},{"className":3425},[],[3427],{"type":31,"value":2602},{"type":31,"value":3429},"は20行程度で書き切れています。",{"type":25,"tag":346,"props":3431,"children":3434},{"cons-label":3432,"pros-label":3433},"デメリット","メリット",[3435,3456],{"type":25,"tag":352,"props":3436,"children":3437},{"v-slot:pros":8},[3438],{"type":25,"tag":53,"props":3439,"children":3440},{},[3441,3446,3451],{"type":25,"tag":57,"props":3442,"children":3443},{},[3444],{"type":31,"value":3445},"依存がゼロ（Blazor WASMのバイナリサイズに寄与しない）",{"type":25,"tag":57,"props":3447,"children":3448},{},[3449],{"type":31,"value":3450},"派生クラスから見たAPIも自分で設計できる",{"type":25,"tag":57,"props":3452,"children":3453},{},[3454],{"type":31,"value":3455},"ライブラリの管理コスト削減",{"type":25,"tag":352,"props":3457,"children":3458},{"v-slot:cons":8},[3459,3470,3475],{"type":25,"tag":57,"props":3460,"children":3461},{},[3462,3468],{"type":25,"tag":529,"props":3463,"children":3465},{"className":3464},[],[3466],{"type":31,"value":3467},"SetProperty",{"type":31,"value":3469},"を手動で呼ぶ分、記述が冗長になる",{"type":25,"tag":57,"props":3471,"children":3472},{},[3473],{"type":31,"value":3474},"比較ロジック（変更が実質あったかどうか）を自前で書く必要がある",{"type":25,"tag":57,"props":3476,"children":3477},{},[3478,3480],{"type":31,"value":3479},"機能を足すと結局Toolkitと似た形に寄ってくる\n",{"type":25,"tag":53,"props":3481,"children":3482},{},[3483],{"type":25,"tag":57,"props":3484,"children":3485},{},[3486],{"type":31,"value":3487},"あくまで低機能前提",{"type":25,"tag":26,"props":3489,"children":3491},{"id":3490},"diffdetectableobject差分の有無をイベントで通知する",[3492],{"type":31,"value":3493},"DiffDetectableObject：差分の有無をイベントで通知する",{"type":25,"tag":33,"props":3495,"children":3496},{},[3497,3499,3504,3506,3511],{"type":31,"value":3498},"本題の",{"type":25,"tag":529,"props":3500,"children":3502},{"className":3501},[],[3503],{"type":31,"value":2609},{"type":31,"value":3505},"は、",{"type":25,"tag":529,"props":3507,"children":3509},{"className":3508},[],[3510],{"type":31,"value":2602},{"type":31,"value":3512},"を2つ（初期スナップショットと編集中のもの）保持するラッパーです。",{"type":25,"tag":524,"props":3514,"children":3516},{"className":2674,"code":3515,"language":2676,"meta":8,"style":8},"public class DiffDetectableObject\u003CT> where T : ObservableObject, IDifferenceComparable\u003CT>\n{\n    public delegate void DiffStateChangedEventHandler(T editable, bool hasDiff);\n    public event DiffStateChangedEventHandler DiffStateChanged;\n\n    public bool HasDiff { get; private set; } = false;\n\n    private T _initObj;\n    private T _editableObj;\n\n    public DiffDetectableObject(T init, T editable)\n    {\n        _initObj = init;\n        _editableObj = editable;\n        _editableObj.PropertyChanged += Editable_PropertyChanged;\n    }\n\n    // ...\n}\n",[3517],{"type":25,"tag":529,"props":3518,"children":3519},{"__ignoreMap":8},[3520,3588,3595,3647,3671,3678,3741,3748,3768,3788,3795,3835,3842,3862,3882,3912,3919,3926,3935],{"type":25,"tag":2682,"props":3521,"children":3522},{"class":2684,"line":18},[3523,3527,3531,3536,3540,3544,3549,3554,3558,3562,3566,3570,3575,3579,3583],{"type":25,"tag":2682,"props":3524,"children":3525},{"style":2762},[3526],{"type":31,"value":2791},{"type":25,"tag":2682,"props":3528,"children":3529},{"style":2762},[3530],{"type":31,"value":2801},{"type":25,"tag":2682,"props":3532,"children":3533},{"style":2694},[3534],{"type":31,"value":3535}," DiffDetectableObject",{"type":25,"tag":2682,"props":3537,"children":3538},{"style":2700},[3539],{"type":31,"value":2891},{"type":25,"tag":2682,"props":3541,"children":3542},{"style":2694},[3543],{"type":31,"value":2896},{"type":25,"tag":2682,"props":3545,"children":3546},{"style":2700},[3547],{"type":31,"value":3548},">",{"type":25,"tag":2682,"props":3550,"children":3551},{"style":2762},[3552],{"type":31,"value":3553}," where",{"type":25,"tag":2682,"props":3555,"children":3556},{"style":2694},[3557],{"type":31,"value":2911},{"type":25,"tag":2682,"props":3559,"children":3560},{"style":2700},[3561],{"type":31,"value":2811},{"type":25,"tag":2682,"props":3563,"children":3564},{"style":2694},[3565],{"type":31,"value":2806},{"type":25,"tag":2682,"props":3567,"children":3568},{"style":2700},[3569],{"type":31,"value":2921},{"type":25,"tag":2682,"props":3571,"children":3572},{"style":2694},[3573],{"type":31,"value":3574}," IDifferenceComparable",{"type":25,"tag":2682,"props":3576,"children":3577},{"style":2700},[3578],{"type":31,"value":2891},{"type":25,"tag":2682,"props":3580,"children":3581},{"style":2694},[3582],{"type":31,"value":2896},{"type":25,"tag":2682,"props":3584,"children":3585},{"style":2700},[3586],{"type":31,"value":3587},">\n",{"type":25,"tag":2682,"props":3589,"children":3590},{"class":2684,"line":1149},[3591],{"type":25,"tag":2682,"props":3592,"children":3593},{"style":2700},[3594],{"type":31,"value":2825},{"type":25,"tag":2682,"props":3596,"children":3597},{"class":2684,"line":1159},[3598,3602,3607,3611,3616,3620,3624,3629,3633,3638,3643],{"type":25,"tag":2682,"props":3599,"children":3600},{"style":2762},[3601],{"type":31,"value":2834},{"type":25,"tag":2682,"props":3603,"children":3604},{"style":2762},[3605],{"type":31,"value":3606}," delegate",{"type":25,"tag":2682,"props":3608,"children":3609},{"style":2688},[3610],{"type":31,"value":2881},{"type":25,"tag":2682,"props":3612,"children":3613},{"style":2694},[3614],{"type":31,"value":3615}," DiffStateChangedEventHandler",{"type":25,"tag":2682,"props":3617,"children":3618},{"style":2700},[3619],{"type":31,"value":3076},{"type":25,"tag":2682,"props":3621,"children":3622},{"style":2694},[3623],{"type":31,"value":2896},{"type":25,"tag":2682,"props":3625,"children":3626},{"style":2852},[3627],{"type":31,"value":3628}," editable",{"type":25,"tag":2682,"props":3630,"children":3631},{"style":2700},[3632],{"type":31,"value":2921},{"type":25,"tag":2682,"props":3634,"children":3635},{"style":2688},[3636],{"type":31,"value":3637}," bool",{"type":25,"tag":2682,"props":3639,"children":3640},{"style":2852},[3641],{"type":31,"value":3642}," hasDiff",{"type":25,"tag":2682,"props":3644,"children":3645},{"style":2700},[3646],{"type":31,"value":3085},{"type":25,"tag":2682,"props":3648,"children":3649},{"class":2684,"line":2758},[3650,3654,3658,3662,3667],{"type":25,"tag":2682,"props":3651,"children":3652},{"style":2762},[3653],{"type":31,"value":2834},{"type":25,"tag":2682,"props":3655,"children":3656},{"style":2762},[3657],{"type":31,"value":2839},{"type":25,"tag":2682,"props":3659,"children":3660},{"style":2694},[3661],{"type":31,"value":3615},{"type":25,"tag":2682,"props":3663,"children":3664},{"style":2852},[3665],{"type":31,"value":3666}," DiffStateChanged",{"type":25,"tag":2682,"props":3668,"children":3669},{"style":2700},[3670],{"type":31,"value":2713},{"type":25,"tag":2682,"props":3672,"children":3673},{"class":2684,"line":2777},[3674],{"type":25,"tag":2682,"props":3675,"children":3676},{"emptyLinePlaceholder":2752},[3677],{"type":31,"value":2755},{"type":25,"tag":2682,"props":3679,"children":3680},{"class":2684,"line":2785},[3681,3685,3689,3694,3699,3704,3709,3714,3719,3723,3728,3732,3737],{"type":25,"tag":2682,"props":3682,"children":3683},{"style":2762},[3684],{"type":31,"value":2834},{"type":25,"tag":2682,"props":3686,"children":3687},{"style":2688},[3688],{"type":31,"value":3637},{"type":25,"tag":2682,"props":3690,"children":3691},{"style":2852},[3692],{"type":31,"value":3693}," HasDiff",{"type":25,"tag":2682,"props":3695,"children":3696},{"style":2700},[3697],{"type":31,"value":3698}," {",{"type":25,"tag":2682,"props":3700,"children":3701},{"style":2762},[3702],{"type":31,"value":3703}," get",{"type":25,"tag":2682,"props":3705,"children":3706},{"style":2700},[3707],{"type":31,"value":3708},";",{"type":25,"tag":2682,"props":3710,"children":3711},{"style":2762},[3712],{"type":31,"value":3713}," private",{"type":25,"tag":2682,"props":3715,"children":3716},{"style":2762},[3717],{"type":31,"value":3718}," set",{"type":25,"tag":2682,"props":3720,"children":3721},{"style":2700},[3722],{"type":31,"value":3708},{"type":25,"tag":2682,"props":3724,"children":3725},{"style":2700},[3726],{"type":31,"value":3727}," }",{"type":25,"tag":2682,"props":3729,"children":3730},{"style":2700},[3731],{"type":31,"value":2968},{"type":25,"tag":2682,"props":3733,"children":3734},{"style":2688},[3735],{"type":31,"value":3736}," false",{"type":25,"tag":2682,"props":3738,"children":3739},{"style":2700},[3740],{"type":31,"value":2713},{"type":25,"tag":2682,"props":3742,"children":3743},{"class":2684,"line":2819},[3744],{"type":25,"tag":2682,"props":3745,"children":3746},{"emptyLinePlaceholder":2752},[3747],{"type":31,"value":2755},{"type":25,"tag":2682,"props":3749,"children":3750},{"class":2684,"line":2828},[3751,3755,3759,3764],{"type":25,"tag":2682,"props":3752,"children":3753},{"style":2762},[3754],{"type":31,"value":3277},{"type":25,"tag":2682,"props":3756,"children":3757},{"style":2694},[3758],{"type":31,"value":2911},{"type":25,"tag":2682,"props":3760,"children":3761},{"style":2852},[3762],{"type":31,"value":3763}," _initObj",{"type":25,"tag":2682,"props":3765,"children":3766},{"style":2700},[3767],{"type":31,"value":2713},{"type":25,"tag":2682,"props":3769,"children":3770},{"class":2684,"line":2862},[3771,3775,3779,3784],{"type":25,"tag":2682,"props":3772,"children":3773},{"style":2762},[3774],{"type":31,"value":3277},{"type":25,"tag":2682,"props":3776,"children":3777},{"style":2694},[3778],{"type":31,"value":2911},{"type":25,"tag":2682,"props":3780,"children":3781},{"style":2852},[3782],{"type":31,"value":3783}," _editableObj",{"type":25,"tag":2682,"props":3785,"children":3786},{"style":2700},[3787],{"type":31,"value":2713},{"type":25,"tag":2682,"props":3789,"children":3790},{"class":2684,"line":2870},[3791],{"type":25,"tag":2682,"props":3792,"children":3793},{"emptyLinePlaceholder":2752},[3794],{"type":31,"value":2755},{"type":25,"tag":2682,"props":3796,"children":3797},{"class":2684,"line":2981},[3798,3802,3806,3810,3814,3819,3823,3827,3831],{"type":25,"tag":2682,"props":3799,"children":3800},{"style":2762},[3801],{"type":31,"value":2834},{"type":25,"tag":2682,"props":3803,"children":3804},{"style":2852},[3805],{"type":31,"value":3535},{"type":25,"tag":2682,"props":3807,"children":3808},{"style":2700},[3809],{"type":31,"value":3076},{"type":25,"tag":2682,"props":3811,"children":3812},{"style":2694},[3813],{"type":31,"value":2896},{"type":25,"tag":2682,"props":3815,"children":3816},{"style":2852},[3817],{"type":31,"value":3818}," init",{"type":25,"tag":2682,"props":3820,"children":3821},{"style":2700},[3822],{"type":31,"value":2921},{"type":25,"tag":2682,"props":3824,"children":3825},{"style":2694},[3826],{"type":31,"value":2911},{"type":25,"tag":2682,"props":3828,"children":3829},{"style":2852},[3830],{"type":31,"value":3628},{"type":25,"tag":2682,"props":3832,"children":3833},{"style":2700},[3834],{"type":31,"value":2978},{"type":25,"tag":2682,"props":3836,"children":3837},{"class":2684,"line":2990},[3838],{"type":25,"tag":2682,"props":3839,"children":3840},{"style":2700},[3841],{"type":31,"value":2987},{"type":25,"tag":2682,"props":3843,"children":3844},{"class":2684,"line":3044},[3845,3850,3854,3858],{"type":25,"tag":2682,"props":3846,"children":3847},{"style":3004},[3848],{"type":31,"value":3849},"        _initObj",{"type":25,"tag":2682,"props":3851,"children":3852},{"style":2700},[3853],{"type":31,"value":2968},{"type":25,"tag":2682,"props":3855,"children":3856},{"style":3004},[3857],{"type":31,"value":3818},{"type":25,"tag":2682,"props":3859,"children":3860},{"style":2700},[3861],{"type":31,"value":2713},{"type":25,"tag":2682,"props":3863,"children":3864},{"class":2684,"line":3065},[3865,3870,3874,3878],{"type":25,"tag":2682,"props":3866,"children":3867},{"style":3004},[3868],{"type":31,"value":3869},"        _editableObj",{"type":25,"tag":2682,"props":3871,"children":3872},{"style":2700},[3873],{"type":31,"value":2968},{"type":25,"tag":2682,"props":3875,"children":3876},{"style":3004},[3877],{"type":31,"value":3628},{"type":25,"tag":2682,"props":3879,"children":3880},{"style":2700},[3881],{"type":31,"value":2713},{"type":25,"tag":2682,"props":3883,"children":3884},{"class":2684,"line":3088},[3885,3889,3893,3898,3903,3908],{"type":25,"tag":2682,"props":3886,"children":3887},{"style":3004},[3888],{"type":31,"value":3869},{"type":25,"tag":2682,"props":3890,"children":3891},{"style":2700},[3892],{"type":31,"value":2703},{"type":25,"tag":2682,"props":3894,"children":3895},{"style":3004},[3896],{"type":31,"value":3897},"PropertyChanged",{"type":25,"tag":2682,"props":3899,"children":3900},{"style":2762},[3901],{"type":31,"value":3902}," +=",{"type":25,"tag":2682,"props":3904,"children":3905},{"style":3004},[3906],{"type":31,"value":3907}," Editable_PropertyChanged",{"type":25,"tag":2682,"props":3909,"children":3910},{"style":2700},[3911],{"type":31,"value":2713},{"type":25,"tag":2682,"props":3913,"children":3914},{"class":2684,"line":3097},[3915],{"type":25,"tag":2682,"props":3916,"children":3917},{"style":2700},[3918],{"type":31,"value":3094},{"type":25,"tag":2682,"props":3920,"children":3921},{"class":2684,"line":3105},[3922],{"type":25,"tag":2682,"props":3923,"children":3924},{"emptyLinePlaceholder":2752},[3925],{"type":31,"value":2755},{"type":25,"tag":2682,"props":3927,"children":3928},{"class":2684,"line":3139},[3929],{"type":25,"tag":2682,"props":3930,"children":3932},{"style":3931},"--shiki-default:#758575DD",[3933],{"type":31,"value":3934},"    // ...\n",{"type":25,"tag":2682,"props":3936,"children":3937},{"class":2684,"line":3147},[3938],{"type":25,"tag":2682,"props":3939,"children":3940},{"style":2700},[3941],{"type":31,"value":3219},{"type":25,"tag":33,"props":3943,"children":3944},{},[3945,3947,3953,3954,3960,3962,3967,3969,3974,3976,3981,3983,3989,3991,3996],{"type":31,"value":3946},"肝は",{"type":25,"tag":529,"props":3948,"children":3950},{"className":3949},[],[3951],{"type":31,"value":3952},"_initObj",{"type":31,"value":1086},{"type":25,"tag":529,"props":3955,"children":3957},{"className":3956},[],[3958],{"type":31,"value":3959},"_editableObj",{"type":31,"value":3961},"の2本持ちです。",{"type":25,"tag":529,"props":3963,"children":3965},{"className":3964},[],[3966],{"type":31,"value":3952},{"type":31,"value":3968},"は「最後に保存した時点のデータ」、",{"type":25,"tag":529,"props":3970,"children":3972},{"className":3971},[],[3973],{"type":31,"value":3959},{"type":31,"value":3975},"は「編集中のデータ」を指し、どちらも同じ型",{"type":25,"tag":529,"props":3977,"children":3979},{"className":3978},[],[3980],{"type":31,"value":2896},{"type":31,"value":3982},"で持ちます。",{"type":25,"tag":529,"props":3984,"children":3986},{"className":3985},[],[3987],{"type":31,"value":3988},"where T : ObservableObject, IDifferenceComparable\u003CT>",{"type":31,"value":3990},"という型制約で、",{"type":25,"tag":529,"props":3992,"children":3994},{"className":3993},[],[3995],{"type":31,"value":2896},{"type":31,"value":3997},"は必ず「変更通知できる」「比較できる」の2つを満たすことを強制しています。",{"type":25,"tag":33,"props":3999,"children":4000},{},[4001,4007,4009,4015],{"type":25,"tag":529,"props":4002,"children":4004},{"className":4003},[],[4005],{"type":31,"value":4006},"IDifferenceComparable\u003CT>",{"type":31,"value":4008},"は自前の最小インターフェースで、中身は次のように",{"type":25,"tag":529,"props":4010,"children":4012},{"className":4011},[],[4013],{"type":31,"value":4014},"CompareDifference",{"type":31,"value":4016},"メソッド1本だけです。",{"type":25,"tag":524,"props":4018,"children":4020},{"className":2674,"code":4019,"language":2676,"meta":8,"style":8},"public interface IDifferenceComparable\u003CTSelf>\n{\n    /// \u003Csummary>差分がある場合:true\u003C/summary>\n    bool CompareDifference(TSelf other);\n}\n",[4021],{"type":25,"tag":529,"props":4022,"children":4023},{"__ignoreMap":8},[4024,4053,4060,4099,4129],{"type":25,"tag":2682,"props":4025,"children":4026},{"class":2684,"line":18},[4027,4031,4036,4040,4044,4049],{"type":25,"tag":2682,"props":4028,"children":4029},{"style":2762},[4030],{"type":31,"value":2791},{"type":25,"tag":2682,"props":4032,"children":4033},{"style":2762},[4034],{"type":31,"value":4035}," interface",{"type":25,"tag":2682,"props":4037,"children":4038},{"style":2694},[4039],{"type":31,"value":3574},{"type":25,"tag":2682,"props":4041,"children":4042},{"style":2700},[4043],{"type":31,"value":2891},{"type":25,"tag":2682,"props":4045,"children":4046},{"style":2694},[4047],{"type":31,"value":4048},"TSelf",{"type":25,"tag":2682,"props":4050,"children":4051},{"style":2700},[4052],{"type":31,"value":3587},{"type":25,"tag":2682,"props":4054,"children":4055},{"class":2684,"line":1149},[4056],{"type":25,"tag":2682,"props":4057,"children":4058},{"style":2700},[4059],{"type":31,"value":2825},{"type":25,"tag":2682,"props":4061,"children":4062},{"class":2684,"line":1159},[4063,4068,4072,4077,4081,4086,4091,4095],{"type":25,"tag":2682,"props":4064,"children":4065},{"style":3931},[4066],{"type":31,"value":4067},"    /// ",{"type":25,"tag":2682,"props":4069,"children":4070},{"style":2700},[4071],{"type":31,"value":2891},{"type":25,"tag":2682,"props":4073,"children":4074},{"style":2688},[4075],{"type":31,"value":4076},"summary",{"type":25,"tag":2682,"props":4078,"children":4079},{"style":2700},[4080],{"type":31,"value":3548},{"type":25,"tag":2682,"props":4082,"children":4083},{"style":3931},[4084],{"type":31,"value":4085},"差分がある場合:true",{"type":25,"tag":2682,"props":4087,"children":4088},{"style":2700},[4089],{"type":31,"value":4090},"\u003C/",{"type":25,"tag":2682,"props":4092,"children":4093},{"style":2688},[4094],{"type":31,"value":4076},{"type":25,"tag":2682,"props":4096,"children":4097},{"style":2700},[4098],{"type":31,"value":3587},{"type":25,"tag":2682,"props":4100,"children":4101},{"class":2684,"line":2758},[4102,4107,4112,4116,4120,4125],{"type":25,"tag":2682,"props":4103,"children":4104},{"style":2688},[4105],{"type":31,"value":4106},"    bool",{"type":25,"tag":2682,"props":4108,"children":4109},{"style":2852},[4110],{"type":31,"value":4111}," CompareDifference",{"type":25,"tag":2682,"props":4113,"children":4114},{"style":2700},[4115],{"type":31,"value":3076},{"type":25,"tag":2682,"props":4117,"children":4118},{"style":2694},[4119],{"type":31,"value":4048},{"type":25,"tag":2682,"props":4121,"children":4122},{"style":2852},[4123],{"type":31,"value":4124}," other",{"type":25,"tag":2682,"props":4126,"children":4127},{"style":2700},[4128],{"type":31,"value":3085},{"type":25,"tag":2682,"props":4130,"children":4131},{"class":2684,"line":2777},[4132],{"type":25,"tag":2682,"props":4133,"children":4134},{"style":2700},[4135],{"type":31,"value":3219},{"type":25,"tag":313,"props":4137,"children":4139},{"id":4138},"プロパティ変更を検知してフラグを切り替える",[4140],{"type":31,"value":4138},{"type":25,"tag":33,"props":4142,"children":4143},{},[4144,4149],{"type":25,"tag":529,"props":4145,"children":4147},{"className":4146},[],[4148],{"type":31,"value":3897},{"type":31,"value":4150},"イベントをフックして、そのたびに差分状態を再計算します。",{"type":25,"tag":524,"props":4152,"children":4154},{"className":2674,"code":4153,"language":2676,"meta":8,"style":8},"private void Editable_PropertyChanged(object sender, PropertyChangedEventArgs e)\n{\n    // 既に差分がある状態で、差分がなくなった場合\n    if (!_initObj.CompareDifference(_editableObj) && HasDiff)\n    {\n        HasDiff = false;\n        DiffStateChanged?.Invoke(_editableObj, false);\n    }\n    // 差分がない状態で差分が発生した場合\n    else if (_initObj.CompareDifference(_editableObj) && !HasDiff)\n    {\n        HasDiff = true;\n        DiffStateChanged?.Invoke(_editableObj, true);\n    }\n}\n",[4155],{"type":25,"tag":529,"props":4156,"children":4157},{"__ignoreMap":8},[4158,4205,4212,4220,4274,4281,4301,4341,4348,4356,4415,4422,4442,4481,4488],{"type":25,"tag":2682,"props":4159,"children":4160},{"class":2684,"line":18},[4161,4166,4170,4174,4178,4183,4188,4192,4196,4201],{"type":25,"tag":2682,"props":4162,"children":4163},{"style":2762},[4164],{"type":31,"value":4165},"private",{"type":25,"tag":2682,"props":4167,"children":4168},{"style":2688},[4169],{"type":31,"value":2881},{"type":25,"tag":2682,"props":4171,"children":4172},{"style":2852},[4173],{"type":31,"value":3907},{"type":25,"tag":2682,"props":4175,"children":4176},{"style":2700},[4177],{"type":31,"value":3076},{"type":25,"tag":2682,"props":4179,"children":4180},{"style":2688},[4181],{"type":31,"value":4182},"object",{"type":25,"tag":2682,"props":4184,"children":4185},{"style":2852},[4186],{"type":31,"value":4187}," sender",{"type":25,"tag":2682,"props":4189,"children":4190},{"style":2700},[4191],{"type":31,"value":2921},{"type":25,"tag":2682,"props":4193,"children":4194},{"style":2694},[4195],{"type":31,"value":3189},{"type":25,"tag":2682,"props":4197,"children":4198},{"style":2852},[4199],{"type":31,"value":4200}," e",{"type":25,"tag":2682,"props":4202,"children":4203},{"style":2700},[4204],{"type":31,"value":2978},{"type":25,"tag":2682,"props":4206,"children":4207},{"class":2684,"line":1149},[4208],{"type":25,"tag":2682,"props":4209,"children":4210},{"style":2700},[4211],{"type":31,"value":2825},{"type":25,"tag":2682,"props":4213,"children":4214},{"class":2684,"line":1159},[4215],{"type":25,"tag":2682,"props":4216,"children":4217},{"style":3931},[4218],{"type":31,"value":4219},"    // 既に差分がある状態で、差分がなくなった場合\n",{"type":25,"tag":2682,"props":4221,"children":4222},{"class":2684,"line":2758},[4223,4228,4232,4237,4241,4245,4249,4253,4257,4261,4266,4270],{"type":25,"tag":2682,"props":4224,"children":4225},{"style":2688},[4226],{"type":31,"value":4227},"    if",{"type":25,"tag":2682,"props":4229,"children":4230},{"style":2700},[4231],{"type":31,"value":3001},{"type":25,"tag":2682,"props":4233,"children":4234},{"style":2762},[4235],{"type":31,"value":4236},"!",{"type":25,"tag":2682,"props":4238,"children":4239},{"style":3004},[4240],{"type":31,"value":3952},{"type":25,"tag":2682,"props":4242,"children":4243},{"style":2700},[4244],{"type":31,"value":2703},{"type":25,"tag":2682,"props":4246,"children":4247},{"style":2852},[4248],{"type":31,"value":4014},{"type":25,"tag":2682,"props":4250,"children":4251},{"style":2700},[4252],{"type":31,"value":3076},{"type":25,"tag":2682,"props":4254,"children":4255},{"style":3004},[4256],{"type":31,"value":3959},{"type":25,"tag":2682,"props":4258,"children":4259},{"style":2700},[4260],{"type":31,"value":3021},{"type":25,"tag":2682,"props":4262,"children":4263},{"style":2762},[4264],{"type":31,"value":4265}," &&",{"type":25,"tag":2682,"props":4267,"children":4268},{"style":3004},[4269],{"type":31,"value":3693},{"type":25,"tag":2682,"props":4271,"children":4272},{"style":2700},[4273],{"type":31,"value":2978},{"type":25,"tag":2682,"props":4275,"children":4276},{"class":2684,"line":2777},[4277],{"type":25,"tag":2682,"props":4278,"children":4279},{"style":2700},[4280],{"type":31,"value":2987},{"type":25,"tag":2682,"props":4282,"children":4283},{"class":2684,"line":2785},[4284,4289,4293,4297],{"type":25,"tag":2682,"props":4285,"children":4286},{"style":3004},[4287],{"type":31,"value":4288},"        HasDiff",{"type":25,"tag":2682,"props":4290,"children":4291},{"style":2700},[4292],{"type":31,"value":2968},{"type":25,"tag":2682,"props":4294,"children":4295},{"style":2688},[4296],{"type":31,"value":3736},{"type":25,"tag":2682,"props":4298,"children":4299},{"style":2700},[4300],{"type":31,"value":2713},{"type":25,"tag":2682,"props":4302,"children":4303},{"class":2684,"line":2819},[4304,4309,4313,4317,4321,4325,4329,4333,4337],{"type":25,"tag":2682,"props":4305,"children":4306},{"style":3004},[4307],{"type":31,"value":4308},"        DiffStateChanged",{"type":25,"tag":2682,"props":4310,"children":4311},{"style":2762},[4312],{"type":31,"value":2849},{"type":25,"tag":2682,"props":4314,"children":4315},{"style":2700},[4316],{"type":31,"value":2703},{"type":25,"tag":2682,"props":4318,"children":4319},{"style":2852},[4320],{"type":31,"value":3166},{"type":25,"tag":2682,"props":4322,"children":4323},{"style":2700},[4324],{"type":31,"value":3076},{"type":25,"tag":2682,"props":4326,"children":4327},{"style":3004},[4328],{"type":31,"value":3959},{"type":25,"tag":2682,"props":4330,"children":4331},{"style":2700},[4332],{"type":31,"value":2921},{"type":25,"tag":2682,"props":4334,"children":4335},{"style":2688},[4336],{"type":31,"value":3736},{"type":25,"tag":2682,"props":4338,"children":4339},{"style":2700},[4340],{"type":31,"value":3085},{"type":25,"tag":2682,"props":4342,"children":4343},{"class":2684,"line":2828},[4344],{"type":25,"tag":2682,"props":4345,"children":4346},{"style":2700},[4347],{"type":31,"value":3094},{"type":25,"tag":2682,"props":4349,"children":4350},{"class":2684,"line":2862},[4351],{"type":25,"tag":2682,"props":4352,"children":4353},{"style":3931},[4354],{"type":31,"value":4355},"    // 差分がない状態で差分が発生した場合\n",{"type":25,"tag":2682,"props":4357,"children":4358},{"class":2684,"line":2870},[4359,4364,4369,4373,4377,4381,4385,4389,4393,4397,4401,4406,4411],{"type":25,"tag":2682,"props":4360,"children":4361},{"style":2688},[4362],{"type":31,"value":4363},"    else",{"type":25,"tag":2682,"props":4365,"children":4366},{"style":2688},[4367],{"type":31,"value":4368}," if",{"type":25,"tag":2682,"props":4370,"children":4371},{"style":2700},[4372],{"type":31,"value":3001},{"type":25,"tag":2682,"props":4374,"children":4375},{"style":3004},[4376],{"type":31,"value":3952},{"type":25,"tag":2682,"props":4378,"children":4379},{"style":2700},[4380],{"type":31,"value":2703},{"type":25,"tag":2682,"props":4382,"children":4383},{"style":2852},[4384],{"type":31,"value":4014},{"type":25,"tag":2682,"props":4386,"children":4387},{"style":2700},[4388],{"type":31,"value":3076},{"type":25,"tag":2682,"props":4390,"children":4391},{"style":3004},[4392],{"type":31,"value":3959},{"type":25,"tag":2682,"props":4394,"children":4395},{"style":2700},[4396],{"type":31,"value":3021},{"type":25,"tag":2682,"props":4398,"children":4399},{"style":2762},[4400],{"type":31,"value":4265},{"type":25,"tag":2682,"props":4402,"children":4403},{"style":2762},[4404],{"type":31,"value":4405}," !",{"type":25,"tag":2682,"props":4407,"children":4408},{"style":3004},[4409],{"type":31,"value":4410},"HasDiff",{"type":25,"tag":2682,"props":4412,"children":4413},{"style":2700},[4414],{"type":31,"value":2978},{"type":25,"tag":2682,"props":4416,"children":4417},{"class":2684,"line":2981},[4418],{"type":25,"tag":2682,"props":4419,"children":4420},{"style":2700},[4421],{"type":31,"value":2987},{"type":25,"tag":2682,"props":4423,"children":4424},{"class":2684,"line":2990},[4425,4429,4433,4438],{"type":25,"tag":2682,"props":4426,"children":4427},{"style":3004},[4428],{"type":31,"value":4288},{"type":25,"tag":2682,"props":4430,"children":4431},{"style":2700},[4432],{"type":31,"value":2968},{"type":25,"tag":2682,"props":4434,"children":4435},{"style":2688},[4436],{"type":31,"value":4437}," true",{"type":25,"tag":2682,"props":4439,"children":4440},{"style":2700},[4441],{"type":31,"value":2713},{"type":25,"tag":2682,"props":4443,"children":4444},{"class":2684,"line":3044},[4445,4449,4453,4457,4461,4465,4469,4473,4477],{"type":25,"tag":2682,"props":4446,"children":4447},{"style":3004},[4448],{"type":31,"value":4308},{"type":25,"tag":2682,"props":4450,"children":4451},{"style":2762},[4452],{"type":31,"value":2849},{"type":25,"tag":2682,"props":4454,"children":4455},{"style":2700},[4456],{"type":31,"value":2703},{"type":25,"tag":2682,"props":4458,"children":4459},{"style":2852},[4460],{"type":31,"value":3166},{"type":25,"tag":2682,"props":4462,"children":4463},{"style":2700},[4464],{"type":31,"value":3076},{"type":25,"tag":2682,"props":4466,"children":4467},{"style":3004},[4468],{"type":31,"value":3959},{"type":25,"tag":2682,"props":4470,"children":4471},{"style":2700},[4472],{"type":31,"value":2921},{"type":25,"tag":2682,"props":4474,"children":4475},{"style":2688},[4476],{"type":31,"value":4437},{"type":25,"tag":2682,"props":4478,"children":4479},{"style":2700},[4480],{"type":31,"value":3085},{"type":25,"tag":2682,"props":4482,"children":4483},{"class":2684,"line":3065},[4484],{"type":25,"tag":2682,"props":4485,"children":4486},{"style":2700},[4487],{"type":31,"value":3094},{"type":25,"tag":2682,"props":4489,"children":4490},{"class":2684,"line":3088},[4491],{"type":25,"tag":2682,"props":4492,"children":4493},{"style":2700},[4494],{"type":31,"value":3219},{"type":25,"tag":33,"props":4496,"children":4497},{},[4498,4500,4505,4507,4512],{"type":31,"value":4499},"ここで工夫している点は、",{"type":25,"tag":103,"props":4501,"children":4502},{},[4503],{"type":31,"value":4504},"「フラグが変化した瞬間だけ」イベントを発火",{"type":31,"value":4506},"している点です。毎回",{"type":25,"tag":529,"props":4508,"children":4510},{"className":4509},[],[4511],{"type":31,"value":3897},{"type":31,"value":4513},"のたびにイベントを流してしまうと、Blazorのレンダリングループに過剰な負荷がかかります。PICOMの楽譜エディタはピアノロールをドラッグするだけで毎秒数十回のプロパティ変更が走るので、イベントが暴れ出さないように「差分あり↔差分なし」の遷移時だけに絞っています。",{"type":25,"tag":26,"props":4515,"children":4517},{"id":4516},"実際の使われ方保存ボタンの活性判定",[4518],{"type":31,"value":4519},"実際の使われ方：保存ボタンの活性判定",{"type":25,"tag":33,"props":4521,"children":4522},{},[4523],{"type":31,"value":4524},"ViewModel側では次のような感じで使います。",{"type":25,"tag":524,"props":4526,"children":4528},{"className":2674,"code":4527,"language":2676,"meta":8,"style":8},"var initial = LoadFromStorage();          // 初期データ\nvar editable = initial.Clone();           // 編集可能なコピー\n_diff = new DiffDetectableObject\u003CMusic>(initial, editable);\n\n_diff.DiffStateChanged += (music, hasDiff) =>\n{\n    CanSave = hasDiff;\n    TitleBarMark = hasDiff ? \"*\" : \"\";\n    StateHasChanged();\n};\n",[4529],{"type":25,"tag":529,"props":4530,"children":4531},{"__ignoreMap":8},[4532,4564,4601,4651,4658,4704,4711,4731,4779,4791],{"type":25,"tag":2682,"props":4533,"children":4534},{"class":2684,"line":18},[4535,4540,4545,4549,4554,4559],{"type":25,"tag":2682,"props":4536,"children":4537},{"style":2762},[4538],{"type":31,"value":4539},"var",{"type":25,"tag":2682,"props":4541,"children":4542},{"style":2852},[4543],{"type":31,"value":4544}," initial",{"type":25,"tag":2682,"props":4546,"children":4547},{"style":2700},[4548],{"type":31,"value":2968},{"type":25,"tag":2682,"props":4550,"children":4551},{"style":2852},[4552],{"type":31,"value":4553}," LoadFromStorage",{"type":25,"tag":2682,"props":4555,"children":4556},{"style":2700},[4557],{"type":31,"value":4558},"();",{"type":25,"tag":2682,"props":4560,"children":4561},{"style":3931},[4562],{"type":31,"value":4563},"          // 初期データ\n",{"type":25,"tag":2682,"props":4565,"children":4566},{"class":2684,"line":1149},[4567,4571,4575,4579,4583,4587,4592,4596],{"type":25,"tag":2682,"props":4568,"children":4569},{"style":2762},[4570],{"type":31,"value":4539},{"type":25,"tag":2682,"props":4572,"children":4573},{"style":2852},[4574],{"type":31,"value":3628},{"type":25,"tag":2682,"props":4576,"children":4577},{"style":2700},[4578],{"type":31,"value":2968},{"type":25,"tag":2682,"props":4580,"children":4581},{"style":3004},[4582],{"type":31,"value":4544},{"type":25,"tag":2682,"props":4584,"children":4585},{"style":2700},[4586],{"type":31,"value":2703},{"type":25,"tag":2682,"props":4588,"children":4589},{"style":2852},[4590],{"type":31,"value":4591},"Clone",{"type":25,"tag":2682,"props":4593,"children":4594},{"style":2700},[4595],{"type":31,"value":4558},{"type":25,"tag":2682,"props":4597,"children":4598},{"style":3931},[4599],{"type":31,"value":4600},"           // 編集可能なコピー\n",{"type":25,"tag":2682,"props":4602,"children":4603},{"class":2684,"line":1159},[4604,4609,4613,4617,4621,4625,4630,4634,4639,4643,4647],{"type":25,"tag":2682,"props":4605,"children":4606},{"style":3004},[4607],{"type":31,"value":4608},"_diff",{"type":25,"tag":2682,"props":4610,"children":4611},{"style":2700},[4612],{"type":31,"value":2968},{"type":25,"tag":2682,"props":4614,"children":4615},{"style":2762},[4616],{"type":31,"value":3031},{"type":25,"tag":2682,"props":4618,"children":4619},{"style":2694},[4620],{"type":31,"value":3535},{"type":25,"tag":2682,"props":4622,"children":4623},{"style":2700},[4624],{"type":31,"value":2891},{"type":25,"tag":2682,"props":4626,"children":4627},{"style":2694},[4628],{"type":31,"value":4629},"Music",{"type":25,"tag":2682,"props":4631,"children":4632},{"style":2700},[4633],{"type":31,"value":2901},{"type":25,"tag":2682,"props":4635,"children":4636},{"style":3004},[4637],{"type":31,"value":4638},"initial",{"type":25,"tag":2682,"props":4640,"children":4641},{"style":2700},[4642],{"type":31,"value":2921},{"type":25,"tag":2682,"props":4644,"children":4645},{"style":3004},[4646],{"type":31,"value":3628},{"type":25,"tag":2682,"props":4648,"children":4649},{"style":2700},[4650],{"type":31,"value":3085},{"type":25,"tag":2682,"props":4652,"children":4653},{"class":2684,"line":2758},[4654],{"type":25,"tag":2682,"props":4655,"children":4656},{"emptyLinePlaceholder":2752},[4657],{"type":31,"value":2755},{"type":25,"tag":2682,"props":4659,"children":4660},{"class":2684,"line":2777},[4661,4665,4669,4674,4678,4682,4687,4691,4695,4699],{"type":25,"tag":2682,"props":4662,"children":4663},{"style":3004},[4664],{"type":31,"value":4608},{"type":25,"tag":2682,"props":4666,"children":4667},{"style":2700},[4668],{"type":31,"value":2703},{"type":25,"tag":2682,"props":4670,"children":4671},{"style":3004},[4672],{"type":31,"value":4673},"DiffStateChanged",{"type":25,"tag":2682,"props":4675,"children":4676},{"style":2762},[4677],{"type":31,"value":3902},{"type":25,"tag":2682,"props":4679,"children":4680},{"style":2700},[4681],{"type":31,"value":3001},{"type":25,"tag":2682,"props":4683,"children":4684},{"style":2852},[4685],{"type":31,"value":4686},"music",{"type":25,"tag":2682,"props":4688,"children":4689},{"style":2700},[4690],{"type":31,"value":2921},{"type":25,"tag":2682,"props":4692,"children":4693},{"style":2852},[4694],{"type":31,"value":3642},{"type":25,"tag":2682,"props":4696,"children":4697},{"style":2700},[4698],{"type":31,"value":3021},{"type":25,"tag":2682,"props":4700,"children":4701},{"style":2762},[4702],{"type":31,"value":4703}," =>\n",{"type":25,"tag":2682,"props":4705,"children":4706},{"class":2684,"line":2785},[4707],{"type":25,"tag":2682,"props":4708,"children":4709},{"style":2700},[4710],{"type":31,"value":2825},{"type":25,"tag":2682,"props":4712,"children":4713},{"class":2684,"line":2819},[4714,4719,4723,4727],{"type":25,"tag":2682,"props":4715,"children":4716},{"style":3004},[4717],{"type":31,"value":4718},"    CanSave",{"type":25,"tag":2682,"props":4720,"children":4721},{"style":2700},[4722],{"type":31,"value":2968},{"type":25,"tag":2682,"props":4724,"children":4725},{"style":3004},[4726],{"type":31,"value":3642},{"type":25,"tag":2682,"props":4728,"children":4729},{"style":2700},[4730],{"type":31,"value":2713},{"type":25,"tag":2682,"props":4732,"children":4733},{"class":2684,"line":2828},[4734,4739,4743,4747,4752,4757,4762,4767,4771,4775],{"type":25,"tag":2682,"props":4735,"children":4736},{"style":3004},[4737],{"type":31,"value":4738},"    TitleBarMark",{"type":25,"tag":2682,"props":4740,"children":4741},{"style":2700},[4742],{"type":31,"value":2968},{"type":25,"tag":2682,"props":4744,"children":4745},{"style":3004},[4746],{"type":31,"value":3642},{"type":25,"tag":2682,"props":4748,"children":4749},{"style":2762},[4750],{"type":31,"value":4751}," ?",{"type":25,"tag":2682,"props":4753,"children":4754},{"style":3293},[4755],{"type":31,"value":4756}," \"",{"type":25,"tag":2682,"props":4758,"children":4760},{"style":4759},"--shiki-default:#C98A7D",[4761],{"type":31,"value":2582},{"type":25,"tag":2682,"props":4763,"children":4764},{"style":3293},[4765],{"type":31,"value":4766},"\"",{"type":25,"tag":2682,"props":4768,"children":4769},{"style":2762},[4770],{"type":31,"value":2811},{"type":25,"tag":2682,"props":4772,"children":4773},{"style":3293},[4774],{"type":31,"value":3296},{"type":25,"tag":2682,"props":4776,"children":4777},{"style":2700},[4778],{"type":31,"value":2713},{"type":25,"tag":2682,"props":4780,"children":4781},{"class":2684,"line":2862},[4782,4787],{"type":25,"tag":2682,"props":4783,"children":4784},{"style":2852},[4785],{"type":31,"value":4786},"    StateHasChanged",{"type":25,"tag":2682,"props":4788,"children":4789},{"style":2700},[4790],{"type":31,"value":3041},{"type":25,"tag":2682,"props":4792,"children":4793},{"class":2684,"line":2870},[4794],{"type":25,"tag":2682,"props":4795,"children":4796},{"style":2700},[4797],{"type":31,"value":4798},"};\n",{"type":25,"tag":33,"props":4800,"children":4801},{},[4802,4804,4810,4812,4817,4819,4825,4827,4833],{"type":31,"value":4803},"UI側は",{"type":25,"tag":529,"props":4805,"children":4807},{"className":4806},[],[4808],{"type":31,"value":4809},"CanSave",{"type":31,"value":4811},"プロパティをそのままボタンの活性（enabled）にバインドすれば終わりです。ユーザーが何か変更したら",{"type":25,"tag":529,"props":4813,"children":4815},{"className":4814},[],[4816],{"type":31,"value":4410},{"type":31,"value":4818},"が",{"type":25,"tag":529,"props":4820,"children":4822},{"className":4821},[],[4823],{"type":31,"value":4824},"true",{"type":31,"value":4826},"になり、保存後に",{"type":25,"tag":529,"props":4828,"children":4830},{"className":4829},[],[4831],{"type":31,"value":4832},"EditableToInit",{"type":31,"value":4834},"を呼んで初期状態を「今」に巻き戻します。",{"type":25,"tag":524,"props":4836,"children":4838},{"className":2674,"code":4837,"language":2676,"meta":8,"style":8},"public void EditableToInit(T newEditable)\n{\n    _initObj = _editableObj;\n    Value = newEditable;\n    HasDiff = false;\n    DiffStateChanged?.Invoke(_editableObj, false);\n}\n",[4839],{"type":25,"tag":529,"props":4840,"children":4841},{"__ignoreMap":8},[4842,4875,4882,4902,4922,4942,4982],{"type":25,"tag":2682,"props":4843,"children":4844},{"class":2684,"line":18},[4845,4849,4853,4858,4862,4866,4871],{"type":25,"tag":2682,"props":4846,"children":4847},{"style":2762},[4848],{"type":31,"value":2791},{"type":25,"tag":2682,"props":4850,"children":4851},{"style":2688},[4852],{"type":31,"value":2881},{"type":25,"tag":2682,"props":4854,"children":4855},{"style":2852},[4856],{"type":31,"value":4857}," EditableToInit",{"type":25,"tag":2682,"props":4859,"children":4860},{"style":2700},[4861],{"type":31,"value":3076},{"type":25,"tag":2682,"props":4863,"children":4864},{"style":2694},[4865],{"type":31,"value":2896},{"type":25,"tag":2682,"props":4867,"children":4868},{"style":2852},[4869],{"type":31,"value":4870}," newEditable",{"type":25,"tag":2682,"props":4872,"children":4873},{"style":2700},[4874],{"type":31,"value":2978},{"type":25,"tag":2682,"props":4876,"children":4877},{"class":2684,"line":1149},[4878],{"type":25,"tag":2682,"props":4879,"children":4880},{"style":2700},[4881],{"type":31,"value":2825},{"type":25,"tag":2682,"props":4883,"children":4884},{"class":2684,"line":1159},[4885,4890,4894,4898],{"type":25,"tag":2682,"props":4886,"children":4887},{"style":3004},[4888],{"type":31,"value":4889},"    _initObj",{"type":25,"tag":2682,"props":4891,"children":4892},{"style":2700},[4893],{"type":31,"value":2968},{"type":25,"tag":2682,"props":4895,"children":4896},{"style":3004},[4897],{"type":31,"value":3783},{"type":25,"tag":2682,"props":4899,"children":4900},{"style":2700},[4901],{"type":31,"value":2713},{"type":25,"tag":2682,"props":4903,"children":4904},{"class":2684,"line":2758},[4905,4910,4914,4918],{"type":25,"tag":2682,"props":4906,"children":4907},{"style":3004},[4908],{"type":31,"value":4909},"    Value",{"type":25,"tag":2682,"props":4911,"children":4912},{"style":2700},[4913],{"type":31,"value":2968},{"type":25,"tag":2682,"props":4915,"children":4916},{"style":3004},[4917],{"type":31,"value":4870},{"type":25,"tag":2682,"props":4919,"children":4920},{"style":2700},[4921],{"type":31,"value":2713},{"type":25,"tag":2682,"props":4923,"children":4924},{"class":2684,"line":2777},[4925,4930,4934,4938],{"type":25,"tag":2682,"props":4926,"children":4927},{"style":3004},[4928],{"type":31,"value":4929},"    HasDiff",{"type":25,"tag":2682,"props":4931,"children":4932},{"style":2700},[4933],{"type":31,"value":2968},{"type":25,"tag":2682,"props":4935,"children":4936},{"style":2688},[4937],{"type":31,"value":3736},{"type":25,"tag":2682,"props":4939,"children":4940},{"style":2700},[4941],{"type":31,"value":2713},{"type":25,"tag":2682,"props":4943,"children":4944},{"class":2684,"line":2785},[4945,4950,4954,4958,4962,4966,4970,4974,4978],{"type":25,"tag":2682,"props":4946,"children":4947},{"style":3004},[4948],{"type":31,"value":4949},"    DiffStateChanged",{"type":25,"tag":2682,"props":4951,"children":4952},{"style":2762},[4953],{"type":31,"value":2849},{"type":25,"tag":2682,"props":4955,"children":4956},{"style":2700},[4957],{"type":31,"value":2703},{"type":25,"tag":2682,"props":4959,"children":4960},{"style":2852},[4961],{"type":31,"value":3166},{"type":25,"tag":2682,"props":4963,"children":4964},{"style":2700},[4965],{"type":31,"value":3076},{"type":25,"tag":2682,"props":4967,"children":4968},{"style":3004},[4969],{"type":31,"value":3959},{"type":25,"tag":2682,"props":4971,"children":4972},{"style":2700},[4973],{"type":31,"value":2921},{"type":25,"tag":2682,"props":4975,"children":4976},{"style":2688},[4977],{"type":31,"value":3736},{"type":25,"tag":2682,"props":4979,"children":4980},{"style":2700},[4981],{"type":31,"value":3085},{"type":25,"tag":2682,"props":4983,"children":4984},{"class":2684,"line":2819},[4985],{"type":25,"tag":2682,"props":4986,"children":4987},{"style":2700},[4988],{"type":31,"value":3219},{"type":25,"tag":26,"props":4990,"children":4992},{"id":4991},"注意点propertychanged解除忘れでメモリリーク",[4993],{"type":31,"value":4994},"注意点：PropertyChanged解除忘れでメモリリーク",{"type":25,"tag":33,"props":4996,"children":4997},{},[4998,5000,5005],{"type":31,"value":4999},"プロパティに新しいオブジェクトをセットしたときの",{"type":25,"tag":103,"props":5001,"children":5002},{},[5003],{"type":31,"value":5004},"イベント解除",{"type":31,"value":5006},"を忘れると、イベント購読がどんどん溜まる現象が発生します。",{"type":25,"tag":524,"props":5008,"children":5010},{"className":2674,"code":5009,"language":2676,"meta":8,"style":8},"public T Value\n{\n    get => _editableObj;\n    set\n    {\n        _editableObj.PropertyChanged -= Editable_PropertyChanged;  // ← 忘れがち\n        _editableObj = value;\n        _editableObj.PropertyChanged += Editable_PropertyChanged;\n    }\n}\n",[5011],{"type":25,"tag":529,"props":5012,"children":5013},{"__ignoreMap":8},[5014,5030,5037,5057,5065,5072,5105,5124,5151,5158],{"type":25,"tag":2682,"props":5015,"children":5016},{"class":2684,"line":18},[5017,5021,5025],{"type":25,"tag":2682,"props":5018,"children":5019},{"style":2762},[5020],{"type":31,"value":2791},{"type":25,"tag":2682,"props":5022,"children":5023},{"style":3004},[5024],{"type":31,"value":2911},{"type":25,"tag":2682,"props":5026,"children":5027},{"style":3004},[5028],{"type":31,"value":5029}," Value\n",{"type":25,"tag":2682,"props":5031,"children":5032},{"class":2684,"line":1149},[5033],{"type":25,"tag":2682,"props":5034,"children":5035},{"style":2700},[5036],{"type":31,"value":2825},{"type":25,"tag":2682,"props":5038,"children":5039},{"class":2684,"line":1159},[5040,5045,5049,5053],{"type":25,"tag":2682,"props":5041,"children":5042},{"style":2852},[5043],{"type":31,"value":5044},"    get",{"type":25,"tag":2682,"props":5046,"children":5047},{"style":2762},[5048],{"type":31,"value":3336},{"type":25,"tag":2682,"props":5050,"children":5051},{"style":3004},[5052],{"type":31,"value":3783},{"type":25,"tag":2682,"props":5054,"children":5055},{"style":2700},[5056],{"type":31,"value":2713},{"type":25,"tag":2682,"props":5058,"children":5059},{"class":2684,"line":2758},[5060],{"type":25,"tag":2682,"props":5061,"children":5062},{"style":3004},[5063],{"type":31,"value":5064},"    set\n",{"type":25,"tag":2682,"props":5066,"children":5067},{"class":2684,"line":2777},[5068],{"type":25,"tag":2682,"props":5069,"children":5070},{"style":2700},[5071],{"type":31,"value":2987},{"type":25,"tag":2682,"props":5073,"children":5074},{"class":2684,"line":2785},[5075,5079,5083,5087,5092,5096,5100],{"type":25,"tag":2682,"props":5076,"children":5077},{"style":3004},[5078],{"type":31,"value":3869},{"type":25,"tag":2682,"props":5080,"children":5081},{"style":2700},[5082],{"type":31,"value":2703},{"type":25,"tag":2682,"props":5084,"children":5085},{"style":3004},[5086],{"type":31,"value":3897},{"type":25,"tag":2682,"props":5088,"children":5089},{"style":2762},[5090],{"type":31,"value":5091}," -=",{"type":25,"tag":2682,"props":5093,"children":5094},{"style":3004},[5095],{"type":31,"value":3907},{"type":25,"tag":2682,"props":5097,"children":5098},{"style":2700},[5099],{"type":31,"value":3708},{"type":25,"tag":2682,"props":5101,"children":5102},{"style":3931},[5103],{"type":31,"value":5104},"  // ← 忘れがち\n",{"type":25,"tag":2682,"props":5106,"children":5107},{"class":2684,"line":2819},[5108,5112,5116,5120],{"type":25,"tag":2682,"props":5109,"children":5110},{"style":3004},[5111],{"type":31,"value":3869},{"type":25,"tag":2682,"props":5113,"children":5114},{"style":2700},[5115],{"type":31,"value":2968},{"type":25,"tag":2682,"props":5117,"children":5118},{"style":3004},[5119],{"type":31,"value":2930},{"type":25,"tag":2682,"props":5121,"children":5122},{"style":2700},[5123],{"type":31,"value":2713},{"type":25,"tag":2682,"props":5125,"children":5126},{"class":2684,"line":2828},[5127,5131,5135,5139,5143,5147],{"type":25,"tag":2682,"props":5128,"children":5129},{"style":3004},[5130],{"type":31,"value":3869},{"type":25,"tag":2682,"props":5132,"children":5133},{"style":2700},[5134],{"type":31,"value":2703},{"type":25,"tag":2682,"props":5136,"children":5137},{"style":3004},[5138],{"type":31,"value":3897},{"type":25,"tag":2682,"props":5140,"children":5141},{"style":2762},[5142],{"type":31,"value":3902},{"type":25,"tag":2682,"props":5144,"children":5145},{"style":3004},[5146],{"type":31,"value":3907},{"type":25,"tag":2682,"props":5148,"children":5149},{"style":2700},[5150],{"type":31,"value":2713},{"type":25,"tag":2682,"props":5152,"children":5153},{"class":2684,"line":2862},[5154],{"type":25,"tag":2682,"props":5155,"children":5156},{"style":2700},[5157],{"type":31,"value":3094},{"type":25,"tag":2682,"props":5159,"children":5160},{"class":2684,"line":2870},[5161],{"type":25,"tag":2682,"props":5162,"children":5163},{"style":2700},[5164],{"type":31,"value":3219},{"type":25,"tag":33,"props":5166,"children":5167},{},[5168,5173],{"type":25,"tag":529,"props":5169,"children":5171},{"className":5170},[],[5172],{"type":31,"value":3897},{"type":31,"value":5174},"はC#のイベントの中でも特にリークしやすいパターンで、「古いインスタンスを参照しっぱなし」の状態を作りやすいです。Blazor WASMはアプリ全体で1つのプロセスなので、ここでリークするとセッション中にずっと残ります。",{"type":25,"tag":659,"props":5176,"children":5177},{},[5178],{"type":25,"tag":33,"props":5179,"children":5180},{},[5181,5187,5189,5194],{"type":25,"tag":529,"props":5182,"children":5184},{"className":5183},[],[5185],{"type":31,"value":5186},"IDisposable",{"type":31,"value":5188},"の実装も本来は検討すべきですが、PICOMではViewModelの寿命がアプリ起動時〜終了時と長いため、デタッチはすべてセッター内で完結させて簡潔にしました。よりシビアな環境（コンポーネントが頻繁に作り直されるケース）では、",{"type":25,"tag":529,"props":5190,"children":5192},{"className":5191},[],[5193],{"type":31,"value":5186},{"type":31,"value":5195},"を明示的に実装した方が安全です。",{"type":25,"tag":26,"props":5197,"children":5198},{"id":1746},[5199],{"type":31,"value":1746},{"type":25,"tag":53,"props":5201,"children":5202},{},[5203,5213,5223,5228],{"type":25,"tag":57,"props":5204,"children":5205},{},[5206,5211],{"type":25,"tag":529,"props":5207,"children":5209},{"className":5208},[],[5210],{"type":31,"value":2602},{"type":31,"value":5212},"は20行程度で書けるので、小規模アプリならMVVM Toolkitを避けて自前で書くのも選択肢",{"type":25,"tag":57,"props":5214,"children":5215},{},[5216,5221],{"type":25,"tag":529,"props":5217,"children":5219},{"className":5218},[],[5220],{"type":31,"value":2609},{"type":31,"value":5222},"で初期スナップショットと編集中オブジェクトの差分をイベント通知に変換できる",{"type":25,"tag":57,"props":5224,"children":5225},{},[5226],{"type":31,"value":5227},"イベントは「差分フラグが変化した瞬間だけ」に絞ると、レンダリングループに優しい",{"type":25,"tag":57,"props":5229,"children":5230},{},[5231,5236],{"type":25,"tag":529,"props":5232,"children":5234},{"className":5233},[],[5235],{"type":31,"value":3897},{"type":31,"value":5237},"の購読解除は徹底する。忘れるとメモリリークの温床になる",{"type":25,"tag":5239,"props":5240,"children":5241},"style",{},[5242],{"type":31,"value":5243},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"title":8,"searchDepth":1149,"depth":1149,"links":5245},[5246,5247,5250,5253,5254,5255],{"id":28,"depth":1149,"text":28},{"id":2649,"depth":1149,"text":2652,"children":5248},[5249],{"id":3401,"depth":1159,"text":3404},{"id":3490,"depth":1149,"text":3493,"children":5251},[5252],{"id":4138,"depth":1159,"text":4138},{"id":4516,"depth":1149,"text":4519},{"id":4991,"depth":1149,"text":4994},{"id":1746,"depth":1149,"text":1746},"content:articles:tech:blazor:diff-detectable-object.md","articles/tech/blazor/diff-detectable-object.md","articles/tech/blazor/diff-detectable-object",{"_path":5260,"_dir":2548,"_draft":7,"_partial":7,"_locale":8,"title":5261,"description":5262,"date":5263,"tags":5264,"rowTypeId":18,"sitemap":5268,"body":5269,"_type":1180,"_id":8180,"_source":1182,"_file":8181,"_stem":8182,"_extension":1185},"/articles/tech/blazor/messagepack-pim-format","MessagePackで独自バイナリファイル形式（PIM）をバージョン管理対応で設計する","音楽作成アプリPICOMで独自のバイナリファイル形式「PIM」をMessagePackで実装しました。なぜJSONでなくMessagePackを選んだのか、`[Key]`番号の運用ルール、将来のフォーマット進化に備えるVersionフィールド戦略について解説します。","2026-04-15",[2553,5265,5266,5267,2557],"MessagePack","ファイルフォーマット","シリアライズ",{"loc":5260,"lastmod":5263,"priority":18},{"type":22,"children":5270,"toc":8165},[5271,5275,5295,5307,5331,5337,5350,5361,5366,5401,5407,5412,5424,5451,5510,5516,5542,5933,5952,6011,6022,6027,6059,6071,6600,6612,6618,6653,7083,7095,7133,7145,7174,7180,7201,7288,7308,7321,7326,7332,7345,7592,7618,7624,7637,7671,8067,8103,8122,8126,8161],{"type":25,"tag":26,"props":5272,"children":5273},{"id":28},[5274],{"type":31,"value":28},{"type":25,"tag":33,"props":5276,"children":5277},{},[5278,5280,5285,5287,5293],{"type":31,"value":5279},"PICOMというチップチューン向け音楽作成Webアプリを開発する中で、「楽譜データをどのフォーマットで保存するか」を決める必要がありました。最終的に選んだのが",{"type":25,"tag":633,"props":5281,"children":5283},{"content":5282},"バイナリ形式のシリアライズフォーマット。JSONより小さくて速い",[5284],{"type":31,"value":5265},{"type":31,"value":5286},"で、これに",{"type":25,"tag":529,"props":5288,"children":5290},{"className":5289},[],[5291],{"type":31,"value":5292},".pim",{"type":31,"value":5294},"という独自拡張子を付けて「PIMフォーマット」と呼んでいます。",{"type":25,"tag":33,"props":5296,"children":5297},{},[5298,5300,5305],{"type":31,"value":5299},"この記事では、JSONではなくMessagePackを選んだ理由と、将来のフォーマット変更に備えた",{"type":25,"tag":103,"props":5301,"children":5302},{},[5303],{"type":31,"value":5304},"バージョン管理の設計",{"type":31,"value":5306},"、そして実装で気をつけた点を書きます。「自作アプリのセーブデータをどう設計するか」で悩んでいる方の参考になれば嬉しいです。",{"type":25,"tag":39,"props":5308,"children":5309},{},[5310],{"type":25,"tag":33,"props":5311,"children":5312},{},[5313,5315,5321,5323,5329],{"type":31,"value":5314},"PICOMのセーブデータ形式「PIM」はMessagePackベースで、バイナリの軽量さとフォーマット拡張への耐性を両立させています。モデルクラスに",{"type":25,"tag":529,"props":5316,"children":5318},{"className":5317},[],[5319],{"type":31,"value":5320},"Version",{"type":31,"value":5322},"プロパティを埋め込むことで将来のフォーマット変更に備え、",{"type":25,"tag":529,"props":5324,"children":5326},{"className":5325},[],[5327],{"type":31,"value":5328},"[Key]",{"type":31,"value":5330},"番号の管理ルールやReadResult enumでデシリアライズ失敗を型で表現する設計も紹介します。",{"type":25,"tag":26,"props":5332,"children":5334},{"id":5333},"なぜjsonではなくmessagepackを選んだのか",[5335],{"type":31,"value":5336},"なぜJSONではなくMessagePackを選んだのか",{"type":25,"tag":33,"props":5338,"children":5339},{},[5340,5342,5348],{"type":31,"value":5341},"一番最初はJSONで実装するつもりでした。デバッグしやすいですし、",{"type":25,"tag":529,"props":5343,"children":5345},{"className":5344},[],[5346],{"type":31,"value":5347},"System.Text.Json",{"type":31,"value":5349},"は.NET標準で追加の依存がいりません。ただ、PICOMで扱うデータの性質を考えた結果、MessagePackに切り替えました。",{"type":25,"tag":33,"props":5351,"children":5352},{},[5353,5355,5360],{"type":31,"value":5354},"決め手を一言で言うと、",{"type":25,"tag":103,"props":5356,"children":5357},{},[5358],{"type":31,"value":5359},"バイナリフォーマットの利点が欲しかったから",{"type":31,"value":2515},{"type":25,"tag":313,"props":5362,"children":5364},{"id":5363},"サイズが小さい",[5365],{"type":31,"value":5363},{"type":25,"tag":33,"props":5367,"children":5368},{},[5369,5371,5377,5379,5385,5386,5392,5394,5399],{"type":31,"value":5370},"PICOMの1曲は数十〜数百の音符と、各音符にエフェクト情報が付きます。JSONで書くとキー名（",{"type":25,"tag":529,"props":5372,"children":5374},{"className":5373},[],[5375],{"type":31,"value":5376},"\"scale\"",{"type":31,"value":5378},", ",{"type":25,"tag":529,"props":5380,"children":5382},{"className":5381},[],[5383],{"type":31,"value":5384},"\"scaleNumber\"",{"type":31,"value":5378},{"type":25,"tag":529,"props":5387,"children":5389},{"className":5388},[],[5390],{"type":31,"value":5391},"\"length\"",{"type":31,"value":5393},"...）が毎音符ごとに繰り返され、中身より外側が重くなりがちです。MessagePackはキーを整数に置き換えられるので、同じデータで比較すると",{"type":25,"tag":103,"props":5395,"children":5396},{},[5397],{"type":31,"value":5398},"ざっくり半分以下",{"type":31,"value":5400},"になります。ブラウザのIndexedDBに保存する以上、軽いに越したことはありません。",{"type":25,"tag":313,"props":5402,"children":5404},{"id":5403},"indexeddbとの相性が良い",[5405],{"type":31,"value":5406},"IndexedDBとの相性が良い",{"type":25,"tag":33,"props":5408,"children":5409},{},[5410],{"type":31,"value":5411},"最終的にIndexedDBに入れるのはバイト列なので、中間でJSON文字列を経由するよりも、最初からバイナリで扱える方がシンプルになります。余計な文字列⇔バイト列の変換が挟まらない分、保存・読み込み周りのコードの見通しも良くなります。",{"type":25,"tag":33,"props":5413,"children":5414},{},[5415,5417,5422],{"type":31,"value":5416},"ただし、バイナリなら何でも良かったわけではありません。独自のバイナリ形式を自力で設計すると、",{"type":25,"tag":103,"props":5418,"children":5419},{},[5420],{"type":31,"value":5421},"フィールドを1つ追加しただけで既存のセーブデータが一切読めなくなる",{"type":31,"value":5423}," という事態に容易に陥ります。PICOMの楽譜データには、今後も新しいフィールドを追加していく可能性が残っており、拡張に耐えるフォーマットであることは必須条件でした。",{"type":25,"tag":33,"props":5425,"children":5426},{},[5427,5429,5435,5437,5442,5444,5449],{"type":31,"value":5428},"MessagePackを選んだのは、この問題への答えを持っていたからです。",{"type":25,"tag":529,"props":5430,"children":5432},{"className":5431},[],[5433],{"type":31,"value":5434},"[Key(n)]",{"type":31,"value":5436}," の番号運用ルールさえ守れば、",{"type":25,"tag":103,"props":5438,"children":5439},{},[5440],{"type":31,"value":5441},"新しいフィールドを追加しても古いデータが壊れず、逆に古いコードで新しいデータを読んでも余分なフィールドが無視されるだけで済みます",{"type":31,"value":5443},"。どう拡張に耐えるかは後述の「Versionフィールドで将来のフォーマット変更に備える」で具体的に扱いますが、",{"type":25,"tag":103,"props":5445,"children":5446},{},[5447],{"type":31,"value":5448},"「バイナリの扱いやすさ」と「フォーマット拡張への耐性」が両立している",{"type":31,"value":5450}," 点こそが、PICOMが最終的にMessagePackを選んだ決定打でした。",{"type":25,"tag":346,"props":5452,"children":5453},{"cons-label":3432,"pros-label":3433},[5454,5480],{"type":25,"tag":352,"props":5455,"children":5456},{"v-slot:pros":8},[5457],{"type":25,"tag":53,"props":5458,"children":5459},{},[5460,5465,5470],{"type":25,"tag":57,"props":5461,"children":5462},{},[5463],{"type":31,"value":5464},"JSONより小さい（同じデータで半分程度）",{"type":25,"tag":57,"props":5466,"children":5467},{},[5468],{"type":31,"value":5469},"バイナリなのでIndexedDBへの保存時に余計な文字列変換が要らない",{"type":25,"tag":57,"props":5471,"children":5472},{},[5473,5478],{"type":25,"tag":529,"props":5474,"children":5476},{"className":5475},[],[5477],{"type":31,"value":5328},{"type":31,"value":5479},"番号を守ればフィールド追加しても古いデータが壊れない拡張性",{"type":25,"tag":352,"props":5481,"children":5482},{"v-slot:cons":8},[5483,5488,5500],{"type":25,"tag":57,"props":5484,"children":5485},{},[5486],{"type":31,"value":5487},"人間が目視できない（デバッグ時にbase64→MessagePack CLI必須）",{"type":25,"tag":57,"props":5489,"children":5490},{},[5491,5493,5498],{"type":31,"value":5492},"NuGetパッケージ",{"type":25,"tag":529,"props":5494,"children":5496},{"className":5495},[],[5497],{"type":31,"value":5265},{"type":31,"value":5499},"への依存が増える",{"type":25,"tag":57,"props":5501,"children":5502},{},[5503,5508],{"type":25,"tag":529,"props":5504,"children":5506},{"className":5505},[],[5507],{"type":31,"value":5328},{"type":31,"value":5509},"番号を後から変更できない",{"type":25,"tag":26,"props":5511,"children":5513},{"id":5512},"pimフォーマットのデータモデル",[5514],{"type":31,"value":5515},"PIMフォーマットのデータモデル",{"type":25,"tag":33,"props":5517,"children":5518},{},[5519,5521,5527,5528,5534,5535,5540],{"type":31,"value":5520},"セーブデータのトップレベルは次の",{"type":25,"tag":529,"props":5522,"children":5524},{"className":5523},[],[5525],{"type":31,"value":5526},"MusicDataModel",{"type":31,"value":2515},{"type":25,"tag":529,"props":5529,"children":5531},{"className":5530},[],[5532],{"type":31,"value":5533},"[MessagePackObject]",{"type":31,"value":1086},{"type":25,"tag":529,"props":5536,"children":5538},{"className":5537},[],[5539],{"type":31,"value":5434},{"type":31,"value":5541},"の組み合わせでフィールド位置を明示しています。",{"type":25,"tag":524,"props":5543,"children":5545},{"className":2674,"code":5544,"language":2676,"meta":8,"style":8},"[MessagePackObject]\npublic class MusicDataModel\n{\n    [Key(0)]\n    public int Version { get; set; } = 2;\n\n    [Key(1)]\n    public MusicMetadataModel Metadata { get; set; } = new();\n\n    [Key(2)]\n    public MusicSettingsModel Settings { get; set; } = new();\n\n    [Key(3)]\n    public TrackDataModel[] Tracks { get; set; } = [];\n}\n",[5546],{"type":25,"tag":529,"props":5547,"children":5548},{"__ignoreMap":8},[5549,5567,5583,5590,5618,5672,5679,5703,5756,5763,5787,5840,5847,5871,5926],{"type":25,"tag":2682,"props":5550,"children":5551},{"class":2684,"line":18},[5552,5557,5562],{"type":25,"tag":2682,"props":5553,"children":5554},{"style":2700},[5555],{"type":31,"value":5556},"[",{"type":25,"tag":2682,"props":5558,"children":5559},{"style":2694},[5560],{"type":31,"value":5561},"MessagePackObject",{"type":25,"tag":2682,"props":5563,"children":5564},{"style":2700},[5565],{"type":31,"value":5566},"]\n",{"type":25,"tag":2682,"props":5568,"children":5569},{"class":2684,"line":1149},[5570,5574,5578],{"type":25,"tag":2682,"props":5571,"children":5572},{"style":2762},[5573],{"type":31,"value":2791},{"type":25,"tag":2682,"props":5575,"children":5576},{"style":2762},[5577],{"type":31,"value":2801},{"type":25,"tag":2682,"props":5579,"children":5580},{"style":2694},[5581],{"type":31,"value":5582}," MusicDataModel\n",{"type":25,"tag":2682,"props":5584,"children":5585},{"class":2684,"line":1159},[5586],{"type":25,"tag":2682,"props":5587,"children":5588},{"style":2700},[5589],{"type":31,"value":2825},{"type":25,"tag":2682,"props":5591,"children":5592},{"class":2684,"line":2758},[5593,5598,5603,5607,5613],{"type":25,"tag":2682,"props":5594,"children":5595},{"style":2700},[5596],{"type":31,"value":5597},"    [",{"type":25,"tag":2682,"props":5599,"children":5600},{"style":2694},[5601],{"type":31,"value":5602},"Key",{"type":25,"tag":2682,"props":5604,"children":5605},{"style":2700},[5606],{"type":31,"value":3076},{"type":25,"tag":2682,"props":5608,"children":5610},{"style":5609},"--shiki-default:#4C9A91",[5611],{"type":31,"value":5612},"0",{"type":25,"tag":2682,"props":5614,"children":5615},{"style":2700},[5616],{"type":31,"value":5617},")]\n",{"type":25,"tag":2682,"props":5619,"children":5620},{"class":2684,"line":2777},[5621,5625,5630,5635,5639,5643,5647,5651,5655,5659,5663,5668],{"type":25,"tag":2682,"props":5622,"children":5623},{"style":2762},[5624],{"type":31,"value":2834},{"type":25,"tag":2682,"props":5626,"children":5627},{"style":2688},[5628],{"type":31,"value":5629}," int",{"type":25,"tag":2682,"props":5631,"children":5632},{"style":2852},[5633],{"type":31,"value":5634}," Version",{"type":25,"tag":2682,"props":5636,"children":5637},{"style":2700},[5638],{"type":31,"value":3698},{"type":25,"tag":2682,"props":5640,"children":5641},{"style":2762},[5642],{"type":31,"value":3703},{"type":25,"tag":2682,"props":5644,"children":5645},{"style":2700},[5646],{"type":31,"value":3708},{"type":25,"tag":2682,"props":5648,"children":5649},{"style":2762},[5650],{"type":31,"value":3718},{"type":25,"tag":2682,"props":5652,"children":5653},{"style":2700},[5654],{"type":31,"value":3708},{"type":25,"tag":2682,"props":5656,"children":5657},{"style":2700},[5658],{"type":31,"value":3727},{"type":25,"tag":2682,"props":5660,"children":5661},{"style":2700},[5662],{"type":31,"value":2968},{"type":25,"tag":2682,"props":5664,"children":5665},{"style":5609},[5666],{"type":31,"value":5667}," 2",{"type":25,"tag":2682,"props":5669,"children":5670},{"style":2700},[5671],{"type":31,"value":2713},{"type":25,"tag":2682,"props":5673,"children":5674},{"class":2684,"line":2785},[5675],{"type":25,"tag":2682,"props":5676,"children":5677},{"emptyLinePlaceholder":2752},[5678],{"type":31,"value":2755},{"type":25,"tag":2682,"props":5680,"children":5681},{"class":2684,"line":2819},[5682,5686,5690,5694,5699],{"type":25,"tag":2682,"props":5683,"children":5684},{"style":2700},[5685],{"type":31,"value":5597},{"type":25,"tag":2682,"props":5687,"children":5688},{"style":2694},[5689],{"type":31,"value":5602},{"type":25,"tag":2682,"props":5691,"children":5692},{"style":2700},[5693],{"type":31,"value":3076},{"type":25,"tag":2682,"props":5695,"children":5696},{"style":5609},[5697],{"type":31,"value":5698},"1",{"type":25,"tag":2682,"props":5700,"children":5701},{"style":2700},[5702],{"type":31,"value":5617},{"type":25,"tag":2682,"props":5704,"children":5705},{"class":2684,"line":2828},[5706,5710,5715,5720,5724,5728,5732,5736,5740,5744,5748,5752],{"type":25,"tag":2682,"props":5707,"children":5708},{"style":2762},[5709],{"type":31,"value":2834},{"type":25,"tag":2682,"props":5711,"children":5712},{"style":2694},[5713],{"type":31,"value":5714}," MusicMetadataModel",{"type":25,"tag":2682,"props":5716,"children":5717},{"style":2852},[5718],{"type":31,"value":5719}," Metadata",{"type":25,"tag":2682,"props":5721,"children":5722},{"style":2700},[5723],{"type":31,"value":3698},{"type":25,"tag":2682,"props":5725,"children":5726},{"style":2762},[5727],{"type":31,"value":3703},{"type":25,"tag":2682,"props":5729,"children":5730},{"style":2700},[5731],{"type":31,"value":3708},{"type":25,"tag":2682,"props":5733,"children":5734},{"style":2762},[5735],{"type":31,"value":3718},{"type":25,"tag":2682,"props":5737,"children":5738},{"style":2700},[5739],{"type":31,"value":3708},{"type":25,"tag":2682,"props":5741,"children":5742},{"style":2700},[5743],{"type":31,"value":3727},{"type":25,"tag":2682,"props":5745,"children":5746},{"style":2700},[5747],{"type":31,"value":2968},{"type":25,"tag":2682,"props":5749,"children":5750},{"style":2762},[5751],{"type":31,"value":3031},{"type":25,"tag":2682,"props":5753,"children":5754},{"style":2700},[5755],{"type":31,"value":3041},{"type":25,"tag":2682,"props":5757,"children":5758},{"class":2684,"line":2862},[5759],{"type":25,"tag":2682,"props":5760,"children":5761},{"emptyLinePlaceholder":2752},[5762],{"type":31,"value":2755},{"type":25,"tag":2682,"props":5764,"children":5765},{"class":2684,"line":2870},[5766,5770,5774,5778,5783],{"type":25,"tag":2682,"props":5767,"children":5768},{"style":2700},[5769],{"type":31,"value":5597},{"type":25,"tag":2682,"props":5771,"children":5772},{"style":2694},[5773],{"type":31,"value":5602},{"type":25,"tag":2682,"props":5775,"children":5776},{"style":2700},[5777],{"type":31,"value":3076},{"type":25,"tag":2682,"props":5779,"children":5780},{"style":5609},[5781],{"type":31,"value":5782},"2",{"type":25,"tag":2682,"props":5784,"children":5785},{"style":2700},[5786],{"type":31,"value":5617},{"type":25,"tag":2682,"props":5788,"children":5789},{"class":2684,"line":2981},[5790,5794,5799,5804,5808,5812,5816,5820,5824,5828,5832,5836],{"type":25,"tag":2682,"props":5791,"children":5792},{"style":2762},[5793],{"type":31,"value":2834},{"type":25,"tag":2682,"props":5795,"children":5796},{"style":2694},[5797],{"type":31,"value":5798}," MusicSettingsModel",{"type":25,"tag":2682,"props":5800,"children":5801},{"style":2852},[5802],{"type":31,"value":5803}," Settings",{"type":25,"tag":2682,"props":5805,"children":5806},{"style":2700},[5807],{"type":31,"value":3698},{"type":25,"tag":2682,"props":5809,"children":5810},{"style":2762},[5811],{"type":31,"value":3703},{"type":25,"tag":2682,"props":5813,"children":5814},{"style":2700},[5815],{"type":31,"value":3708},{"type":25,"tag":2682,"props":5817,"children":5818},{"style":2762},[5819],{"type":31,"value":3718},{"type":25,"tag":2682,"props":5821,"children":5822},{"style":2700},[5823],{"type":31,"value":3708},{"type":25,"tag":2682,"props":5825,"children":5826},{"style":2700},[5827],{"type":31,"value":3727},{"type":25,"tag":2682,"props":5829,"children":5830},{"style":2700},[5831],{"type":31,"value":2968},{"type":25,"tag":2682,"props":5833,"children":5834},{"style":2762},[5835],{"type":31,"value":3031},{"type":25,"tag":2682,"props":5837,"children":5838},{"style":2700},[5839],{"type":31,"value":3041},{"type":25,"tag":2682,"props":5841,"children":5842},{"class":2684,"line":2990},[5843],{"type":25,"tag":2682,"props":5844,"children":5845},{"emptyLinePlaceholder":2752},[5846],{"type":31,"value":2755},{"type":25,"tag":2682,"props":5848,"children":5849},{"class":2684,"line":3044},[5850,5854,5858,5862,5867],{"type":25,"tag":2682,"props":5851,"children":5852},{"style":2700},[5853],{"type":31,"value":5597},{"type":25,"tag":2682,"props":5855,"children":5856},{"style":2694},[5857],{"type":31,"value":5602},{"type":25,"tag":2682,"props":5859,"children":5860},{"style":2700},[5861],{"type":31,"value":3076},{"type":25,"tag":2682,"props":5863,"children":5864},{"style":5609},[5865],{"type":31,"value":5866},"3",{"type":25,"tag":2682,"props":5868,"children":5869},{"style":2700},[5870],{"type":31,"value":5617},{"type":25,"tag":2682,"props":5872,"children":5873},{"class":2684,"line":3065},[5874,5878,5883,5888,5893,5897,5901,5905,5909,5913,5917,5921],{"type":25,"tag":2682,"props":5875,"children":5876},{"style":2762},[5877],{"type":31,"value":2834},{"type":25,"tag":2682,"props":5879,"children":5880},{"style":2694},[5881],{"type":31,"value":5882}," TrackDataModel",{"type":25,"tag":2682,"props":5884,"children":5885},{"style":2700},[5886],{"type":31,"value":5887},"[]",{"type":25,"tag":2682,"props":5889,"children":5890},{"style":2852},[5891],{"type":31,"value":5892}," Tracks",{"type":25,"tag":2682,"props":5894,"children":5895},{"style":2700},[5896],{"type":31,"value":3698},{"type":25,"tag":2682,"props":5898,"children":5899},{"style":2762},[5900],{"type":31,"value":3703},{"type":25,"tag":2682,"props":5902,"children":5903},{"style":2700},[5904],{"type":31,"value":3708},{"type":25,"tag":2682,"props":5906,"children":5907},{"style":2762},[5908],{"type":31,"value":3718},{"type":25,"tag":2682,"props":5910,"children":5911},{"style":2700},[5912],{"type":31,"value":3708},{"type":25,"tag":2682,"props":5914,"children":5915},{"style":2700},[5916],{"type":31,"value":3727},{"type":25,"tag":2682,"props":5918,"children":5919},{"style":2700},[5920],{"type":31,"value":2968},{"type":25,"tag":2682,"props":5922,"children":5923},{"style":2700},[5924],{"type":31,"value":5925}," [];\n",{"type":25,"tag":2682,"props":5927,"children":5928},{"class":2684,"line":3088},[5929],{"type":25,"tag":2682,"props":5930,"children":5931},{"style":2700},[5932],{"type":31,"value":3219},{"type":25,"tag":33,"props":5934,"children":5935},{},[5936,5938,5943,5945,5950],{"type":31,"value":5937},"ここで一番重要なのが",{"type":25,"tag":529,"props":5939,"children":5941},{"className":5940},[],[5942],{"type":31,"value":5320},{"type":31,"value":5944},"プロパティです。後述しますが、これは",{"type":25,"tag":103,"props":5946,"children":5947},{},[5948],{"type":31,"value":5949},"書き込んだ時のフォーマットバージョン",{"type":31,"value":5951},"を自己申告するためのフィールドで、将来のフォーマット進化の鍵になります。",{"type":25,"tag":33,"props":5953,"children":5954},{},[5955,5957,5963,5965,5971,5973,5979,5980,5986,5987,5993,5995,6001,6003,6009],{"type":31,"value":5956},"Trackの中身はもっとツリーが深く、",{"type":25,"tag":529,"props":5958,"children":5960},{"className":5959},[],[5961],{"type":31,"value":5962},"ComponentDataModel",{"type":31,"value":5964},"を基底にした",{"type":25,"tag":529,"props":5966,"children":5968},{"className":5967},[],[5969],{"type":31,"value":5970},"NoteDataModel",{"type":31,"value":5972}," / ",{"type":25,"tag":529,"props":5974,"children":5976},{"className":5975},[],[5977],{"type":31,"value":5978},"RestDataModel",{"type":31,"value":5972},{"type":25,"tag":529,"props":5981,"children":5983},{"className":5982},[],[5984],{"type":31,"value":5985},"TieDataModel",{"type":31,"value":5972},{"type":25,"tag":529,"props":5988,"children":5990},{"className":5989},[],[5991],{"type":31,"value":5992},"TupletDataModel",{"type":31,"value":5994},"のサブクラスが入っています。MessagePackでは",{"type":25,"tag":529,"props":5996,"children":5998},{"className":5997},[],[5999],{"type":31,"value":6000},"[Union]",{"type":31,"value":6002},"属性で多態を表現する方法と、サブクラスを判別する",{"type":25,"tag":529,"props":6004,"children":6006},{"className":6005},[],[6007],{"type":31,"value":6008},"Type",{"type":31,"value":6010},"フィールドを持たせる方法がありますが、PIMでは前者を使っています。",{"type":25,"tag":26,"props":6012,"children":6014},{"id":6013},"key番号は追記のみ絶対に削除しない",[6015,6020],{"type":25,"tag":529,"props":6016,"children":6018},{"className":6017},[],[6019],{"type":31,"value":5328},{"type":31,"value":6021},"番号は追記のみ、絶対に削除しない",{"type":25,"tag":33,"props":6023,"children":6024},{},[6025],{"type":31,"value":6026},"DBの主キーもそうですが、キーはイミュータブルな運用としています。",{"type":25,"tag":33,"props":6028,"children":6029},{},[6030,6036,6037,6043,6044,6050,6052,6057],{"type":25,"tag":529,"props":6031,"children":6033},{"className":6032},[],[6034],{"type":31,"value":6035},"[Key(0)]",{"type":31,"value":5378},{"type":25,"tag":529,"props":6038,"children":6040},{"className":6039},[],[6041],{"type":31,"value":6042},"[Key(1)]",{"type":31,"value":5378},{"type":25,"tag":529,"props":6045,"children":6047},{"className":6046},[],[6048],{"type":31,"value":6049},"[Key(2)]",{"type":31,"value":6051},"... の番号は、一度付けたら",{"type":25,"tag":103,"props":6053,"children":6054},{},[6055],{"type":31,"value":6056},"絶対に変えてはいけません",{"type":31,"value":6058},"。番号はバイナリ内の物理的な位置そのもので、番号を変えると古いセーブデータが読み込めなくなります。",{"type":25,"tag":33,"props":6060,"children":6061},{},[6062,6064,6069],{"type":31,"value":6063},"フィールドを増やすときはどうするか。次のように常に",{"type":25,"tag":103,"props":6065,"children":6066},{},[6067],{"type":31,"value":6068},"新しい番号を末尾に追加",{"type":31,"value":6070},"します。",{"type":25,"tag":524,"props":6072,"children":6074},{"className":2674,"code":6073,"language":2676,"meta":8,"style":8},"// 悪い例: 既存の番号を詰め直してしまう\n[Key(0)] public int Version { get; set; }\n[Key(1)] public MusicSettingsModel Settings { get; set; }  // Metadataを消して番号を詰めた\n[Key(2)] public TrackDataModel[] Tracks { get; set; }\n\n// 良い例: 既存番号はそのまま、新規は末尾に追加\n[Key(0)] public int Version { get; set; }\n[Key(1)] public MusicMetadataModel Metadata { get; set; }\n[Key(2)] public MusicSettingsModel Settings { get; set; }\n[Key(3)] public TrackDataModel[] Tracks { get; set; }\n[Key(4)] public string? Comment { get; set; }  // 新規追加\n",[6075],{"type":25,"tag":529,"props":6076,"children":6077},{"__ignoreMap":8},[6078,6086,6148,6212,6275,6282,6290,6349,6408,6467,6530],{"type":25,"tag":2682,"props":6079,"children":6080},{"class":2684,"line":18},[6081],{"type":25,"tag":2682,"props":6082,"children":6083},{"style":3931},[6084],{"type":31,"value":6085},"// 悪い例: 既存の番号を詰め直してしまう\n",{"type":25,"tag":2682,"props":6087,"children":6088},{"class":2684,"line":1149},[6089,6093,6097,6101,6105,6110,6115,6119,6123,6127,6131,6135,6139,6143],{"type":25,"tag":2682,"props":6090,"children":6091},{"style":2700},[6092],{"type":31,"value":5556},{"type":25,"tag":2682,"props":6094,"children":6095},{"style":2694},[6096],{"type":31,"value":5602},{"type":25,"tag":2682,"props":6098,"children":6099},{"style":2700},[6100],{"type":31,"value":3076},{"type":25,"tag":2682,"props":6102,"children":6103},{"style":5609},[6104],{"type":31,"value":5612},{"type":25,"tag":2682,"props":6106,"children":6107},{"style":2700},[6108],{"type":31,"value":6109},")]",{"type":25,"tag":2682,"props":6111,"children":6112},{"style":2762},[6113],{"type":31,"value":6114}," public",{"type":25,"tag":2682,"props":6116,"children":6117},{"style":2688},[6118],{"type":31,"value":5629},{"type":25,"tag":2682,"props":6120,"children":6121},{"style":3004},[6122],{"type":31,"value":5634},{"type":25,"tag":2682,"props":6124,"children":6125},{"style":2700},[6126],{"type":31,"value":3698},{"type":25,"tag":2682,"props":6128,"children":6129},{"style":3004},[6130],{"type":31,"value":3703},{"type":25,"tag":2682,"props":6132,"children":6133},{"style":2700},[6134],{"type":31,"value":3708},{"type":25,"tag":2682,"props":6136,"children":6137},{"style":3004},[6138],{"type":31,"value":3718},{"type":25,"tag":2682,"props":6140,"children":6141},{"style":2700},[6142],{"type":31,"value":3708},{"type":25,"tag":2682,"props":6144,"children":6145},{"style":2700},[6146],{"type":31,"value":6147}," }\n",{"type":25,"tag":2682,"props":6149,"children":6150},{"class":2684,"line":1159},[6151,6155,6159,6163,6167,6171,6175,6179,6183,6187,6191,6195,6199,6203,6207],{"type":25,"tag":2682,"props":6152,"children":6153},{"style":2700},[6154],{"type":31,"value":5556},{"type":25,"tag":2682,"props":6156,"children":6157},{"style":2694},[6158],{"type":31,"value":5602},{"type":25,"tag":2682,"props":6160,"children":6161},{"style":2700},[6162],{"type":31,"value":3076},{"type":25,"tag":2682,"props":6164,"children":6165},{"style":5609},[6166],{"type":31,"value":5698},{"type":25,"tag":2682,"props":6168,"children":6169},{"style":2700},[6170],{"type":31,"value":6109},{"type":25,"tag":2682,"props":6172,"children":6173},{"style":2762},[6174],{"type":31,"value":6114},{"type":25,"tag":2682,"props":6176,"children":6177},{"style":3004},[6178],{"type":31,"value":5798},{"type":25,"tag":2682,"props":6180,"children":6181},{"style":3004},[6182],{"type":31,"value":5803},{"type":25,"tag":2682,"props":6184,"children":6185},{"style":2700},[6186],{"type":31,"value":3698},{"type":25,"tag":2682,"props":6188,"children":6189},{"style":3004},[6190],{"type":31,"value":3703},{"type":25,"tag":2682,"props":6192,"children":6193},{"style":2700},[6194],{"type":31,"value":3708},{"type":25,"tag":2682,"props":6196,"children":6197},{"style":3004},[6198],{"type":31,"value":3718},{"type":25,"tag":2682,"props":6200,"children":6201},{"style":2700},[6202],{"type":31,"value":3708},{"type":25,"tag":2682,"props":6204,"children":6205},{"style":2700},[6206],{"type":31,"value":3727},{"type":25,"tag":2682,"props":6208,"children":6209},{"style":3931},[6210],{"type":31,"value":6211},"  // Metadataを消して番号を詰めた\n",{"type":25,"tag":2682,"props":6213,"children":6214},{"class":2684,"line":2758},[6215,6219,6223,6227,6231,6235,6239,6243,6247,6251,6255,6259,6263,6267,6271],{"type":25,"tag":2682,"props":6216,"children":6217},{"style":2700},[6218],{"type":31,"value":5556},{"type":25,"tag":2682,"props":6220,"children":6221},{"style":2694},[6222],{"type":31,"value":5602},{"type":25,"tag":2682,"props":6224,"children":6225},{"style":2700},[6226],{"type":31,"value":3076},{"type":25,"tag":2682,"props":6228,"children":6229},{"style":5609},[6230],{"type":31,"value":5782},{"type":25,"tag":2682,"props":6232,"children":6233},{"style":2700},[6234],{"type":31,"value":6109},{"type":25,"tag":2682,"props":6236,"children":6237},{"style":2762},[6238],{"type":31,"value":6114},{"type":25,"tag":2682,"props":6240,"children":6241},{"style":3004},[6242],{"type":31,"value":5882},{"type":25,"tag":2682,"props":6244,"children":6245},{"style":2700},[6246],{"type":31,"value":5887},{"type":25,"tag":2682,"props":6248,"children":6249},{"style":3004},[6250],{"type":31,"value":5892},{"type":25,"tag":2682,"props":6252,"children":6253},{"style":2700},[6254],{"type":31,"value":3698},{"type":25,"tag":2682,"props":6256,"children":6257},{"style":3004},[6258],{"type":31,"value":3703},{"type":25,"tag":2682,"props":6260,"children":6261},{"style":2700},[6262],{"type":31,"value":3708},{"type":25,"tag":2682,"props":6264,"children":6265},{"style":3004},[6266],{"type":31,"value":3718},{"type":25,"tag":2682,"props":6268,"children":6269},{"style":2700},[6270],{"type":31,"value":3708},{"type":25,"tag":2682,"props":6272,"children":6273},{"style":2700},[6274],{"type":31,"value":6147},{"type":25,"tag":2682,"props":6276,"children":6277},{"class":2684,"line":2777},[6278],{"type":25,"tag":2682,"props":6279,"children":6280},{"emptyLinePlaceholder":2752},[6281],{"type":31,"value":2755},{"type":25,"tag":2682,"props":6283,"children":6284},{"class":2684,"line":2785},[6285],{"type":25,"tag":2682,"props":6286,"children":6287},{"style":3931},[6288],{"type":31,"value":6289},"// 良い例: 既存番号はそのまま、新規は末尾に追加\n",{"type":25,"tag":2682,"props":6291,"children":6292},{"class":2684,"line":2819},[6293,6297,6301,6305,6309,6313,6317,6321,6325,6329,6333,6337,6341,6345],{"type":25,"tag":2682,"props":6294,"children":6295},{"style":2700},[6296],{"type":31,"value":5556},{"type":25,"tag":2682,"props":6298,"children":6299},{"style":2694},[6300],{"type":31,"value":5602},{"type":25,"tag":2682,"props":6302,"children":6303},{"style":2700},[6304],{"type":31,"value":3076},{"type":25,"tag":2682,"props":6306,"children":6307},{"style":5609},[6308],{"type":31,"value":5612},{"type":25,"tag":2682,"props":6310,"children":6311},{"style":2700},[6312],{"type":31,"value":6109},{"type":25,"tag":2682,"props":6314,"children":6315},{"style":2762},[6316],{"type":31,"value":6114},{"type":25,"tag":2682,"props":6318,"children":6319},{"style":2688},[6320],{"type":31,"value":5629},{"type":25,"tag":2682,"props":6322,"children":6323},{"style":3004},[6324],{"type":31,"value":5634},{"type":25,"tag":2682,"props":6326,"children":6327},{"style":2700},[6328],{"type":31,"value":3698},{"type":25,"tag":2682,"props":6330,"children":6331},{"style":3004},[6332],{"type":31,"value":3703},{"type":25,"tag":2682,"props":6334,"children":6335},{"style":2700},[6336],{"type":31,"value":3708},{"type":25,"tag":2682,"props":6338,"children":6339},{"style":3004},[6340],{"type":31,"value":3718},{"type":25,"tag":2682,"props":6342,"children":6343},{"style":2700},[6344],{"type":31,"value":3708},{"type":25,"tag":2682,"props":6346,"children":6347},{"style":2700},[6348],{"type":31,"value":6147},{"type":25,"tag":2682,"props":6350,"children":6351},{"class":2684,"line":2828},[6352,6356,6360,6364,6368,6372,6376,6380,6384,6388,6392,6396,6400,6404],{"type":25,"tag":2682,"props":6353,"children":6354},{"style":2700},[6355],{"type":31,"value":5556},{"type":25,"tag":2682,"props":6357,"children":6358},{"style":2694},[6359],{"type":31,"value":5602},{"type":25,"tag":2682,"props":6361,"children":6362},{"style":2700},[6363],{"type":31,"value":3076},{"type":25,"tag":2682,"props":6365,"children":6366},{"style":5609},[6367],{"type":31,"value":5698},{"type":25,"tag":2682,"props":6369,"children":6370},{"style":2700},[6371],{"type":31,"value":6109},{"type":25,"tag":2682,"props":6373,"children":6374},{"style":2762},[6375],{"type":31,"value":6114},{"type":25,"tag":2682,"props":6377,"children":6378},{"style":3004},[6379],{"type":31,"value":5714},{"type":25,"tag":2682,"props":6381,"children":6382},{"style":3004},[6383],{"type":31,"value":5719},{"type":25,"tag":2682,"props":6385,"children":6386},{"style":2700},[6387],{"type":31,"value":3698},{"type":25,"tag":2682,"props":6389,"children":6390},{"style":3004},[6391],{"type":31,"value":3703},{"type":25,"tag":2682,"props":6393,"children":6394},{"style":2700},[6395],{"type":31,"value":3708},{"type":25,"tag":2682,"props":6397,"children":6398},{"style":3004},[6399],{"type":31,"value":3718},{"type":25,"tag":2682,"props":6401,"children":6402},{"style":2700},[6403],{"type":31,"value":3708},{"type":25,"tag":2682,"props":6405,"children":6406},{"style":2700},[6407],{"type":31,"value":6147},{"type":25,"tag":2682,"props":6409,"children":6410},{"class":2684,"line":2862},[6411,6415,6419,6423,6427,6431,6435,6439,6443,6447,6451,6455,6459,6463],{"type":25,"tag":2682,"props":6412,"children":6413},{"style":2700},[6414],{"type":31,"value":5556},{"type":25,"tag":2682,"props":6416,"children":6417},{"style":2694},[6418],{"type":31,"value":5602},{"type":25,"tag":2682,"props":6420,"children":6421},{"style":2700},[6422],{"type":31,"value":3076},{"type":25,"tag":2682,"props":6424,"children":6425},{"style":5609},[6426],{"type":31,"value":5782},{"type":25,"tag":2682,"props":6428,"children":6429},{"style":2700},[6430],{"type":31,"value":6109},{"type":25,"tag":2682,"props":6432,"children":6433},{"style":2762},[6434],{"type":31,"value":6114},{"type":25,"tag":2682,"props":6436,"children":6437},{"style":3004},[6438],{"type":31,"value":5798},{"type":25,"tag":2682,"props":6440,"children":6441},{"style":3004},[6442],{"type":31,"value":5803},{"type":25,"tag":2682,"props":6444,"children":6445},{"style":2700},[6446],{"type":31,"value":3698},{"type":25,"tag":2682,"props":6448,"children":6449},{"style":3004},[6450],{"type":31,"value":3703},{"type":25,"tag":2682,"props":6452,"children":6453},{"style":2700},[6454],{"type":31,"value":3708},{"type":25,"tag":2682,"props":6456,"children":6457},{"style":3004},[6458],{"type":31,"value":3718},{"type":25,"tag":2682,"props":6460,"children":6461},{"style":2700},[6462],{"type":31,"value":3708},{"type":25,"tag":2682,"props":6464,"children":6465},{"style":2700},[6466],{"type":31,"value":6147},{"type":25,"tag":2682,"props":6468,"children":6469},{"class":2684,"line":2870},[6470,6474,6478,6482,6486,6490,6494,6498,6502,6506,6510,6514,6518,6522,6526],{"type":25,"tag":2682,"props":6471,"children":6472},{"style":2700},[6473],{"type":31,"value":5556},{"type":25,"tag":2682,"props":6475,"children":6476},{"style":2694},[6477],{"type":31,"value":5602},{"type":25,"tag":2682,"props":6479,"children":6480},{"style":2700},[6481],{"type":31,"value":3076},{"type":25,"tag":2682,"props":6483,"children":6484},{"style":5609},[6485],{"type":31,"value":5866},{"type":25,"tag":2682,"props":6487,"children":6488},{"style":2700},[6489],{"type":31,"value":6109},{"type":25,"tag":2682,"props":6491,"children":6492},{"style":2762},[6493],{"type":31,"value":6114},{"type":25,"tag":2682,"props":6495,"children":6496},{"style":3004},[6497],{"type":31,"value":5882},{"type":25,"tag":2682,"props":6499,"children":6500},{"style":2700},[6501],{"type":31,"value":5887},{"type":25,"tag":2682,"props":6503,"children":6504},{"style":3004},[6505],{"type":31,"value":5892},{"type":25,"tag":2682,"props":6507,"children":6508},{"style":2700},[6509],{"type":31,"value":3698},{"type":25,"tag":2682,"props":6511,"children":6512},{"style":3004},[6513],{"type":31,"value":3703},{"type":25,"tag":2682,"props":6515,"children":6516},{"style":2700},[6517],{"type":31,"value":3708},{"type":25,"tag":2682,"props":6519,"children":6520},{"style":3004},[6521],{"type":31,"value":3718},{"type":25,"tag":2682,"props":6523,"children":6524},{"style":2700},[6525],{"type":31,"value":3708},{"type":25,"tag":2682,"props":6527,"children":6528},{"style":2700},[6529],{"type":31,"value":6147},{"type":25,"tag":2682,"props":6531,"children":6532},{"class":2684,"line":2981},[6533,6537,6541,6545,6550,6554,6558,6562,6566,6571,6575,6579,6583,6587,6591,6595],{"type":25,"tag":2682,"props":6534,"children":6535},{"style":2700},[6536],{"type":31,"value":5556},{"type":25,"tag":2682,"props":6538,"children":6539},{"style":2694},[6540],{"type":31,"value":5602},{"type":25,"tag":2682,"props":6542,"children":6543},{"style":2700},[6544],{"type":31,"value":3076},{"type":25,"tag":2682,"props":6546,"children":6547},{"style":5609},[6548],{"type":31,"value":6549},"4",{"type":25,"tag":2682,"props":6551,"children":6552},{"style":2700},[6553],{"type":31,"value":6109},{"type":25,"tag":2682,"props":6555,"children":6556},{"style":2762},[6557],{"type":31,"value":6114},{"type":25,"tag":2682,"props":6559,"children":6560},{"style":2688},[6561],{"type":31,"value":2954},{"type":25,"tag":2682,"props":6563,"children":6564},{"style":2762},[6565],{"type":31,"value":2849},{"type":25,"tag":2682,"props":6567,"children":6568},{"style":3004},[6569],{"type":31,"value":6570}," Comment",{"type":25,"tag":2682,"props":6572,"children":6573},{"style":2700},[6574],{"type":31,"value":3698},{"type":25,"tag":2682,"props":6576,"children":6577},{"style":3004},[6578],{"type":31,"value":3703},{"type":25,"tag":2682,"props":6580,"children":6581},{"style":2700},[6582],{"type":31,"value":3708},{"type":25,"tag":2682,"props":6584,"children":6585},{"style":3004},[6586],{"type":31,"value":3718},{"type":25,"tag":2682,"props":6588,"children":6589},{"style":2700},[6590],{"type":31,"value":3708},{"type":25,"tag":2682,"props":6592,"children":6593},{"style":2700},[6594],{"type":31,"value":3727},{"type":25,"tag":2682,"props":6596,"children":6597},{"style":3931},[6598],{"type":31,"value":6599},"  // 新規追加\n",{"type":25,"tag":33,"props":6601,"children":6602},{},[6603,6605,6610],{"type":31,"value":6604},"削除したいフィールドがあっても、番号は",{"type":25,"tag":103,"props":6606,"children":6607},{},[6608],{"type":31,"value":6609},"空き番として予約",{"type":31,"value":6611},"しておき、コメントで「deprecated」と書いておくのが安全です。",{"type":25,"tag":26,"props":6613,"children":6615},{"id":6614},"versionフィールドで将来のフォーマット変更に備える",[6616],{"type":31,"value":6617},"Versionフィールドで将来のフォーマット変更に備える",{"type":25,"tag":33,"props":6619,"children":6620},{},[6621,6623,6629,6631,6637,6639,6644,6646,6651],{"type":31,"value":6622},"たとえば「トラックに色を付けたい」という要件が来たとします。単純に",{"type":25,"tag":529,"props":6624,"children":6626},{"className":6625},[],[6627],{"type":31,"value":6628},"TrackDataModel",{"type":31,"value":6630},"に",{"type":25,"tag":529,"props":6632,"children":6634},{"className":6633},[],[6635],{"type":31,"value":6636},"[Key(n)] int ColorHex",{"type":31,"value":6638},"を足すだけでも新しい番号なら動きますが、PICOMでは",{"type":25,"tag":103,"props":6640,"children":6641},{},[6642],{"type":31,"value":6643},"モデル自体が大きく変わる将来",{"type":31,"value":6645},"にも備えて",{"type":25,"tag":529,"props":6647,"children":6649},{"className":6648},[],[6650],{"type":31,"value":5320},{"type":31,"value":6652},"を数値で持たせています。",{"type":25,"tag":524,"props":6654,"children":6656},{"className":2674,"code":6655,"language":2676,"meta":8,"style":8},"public (ReadResult resultType, Music? music) Read(byte[] data, int musicId)\n{\n    MusicDataModel model;\n    try\n    {\n        model = MessagePackSerializer.Deserialize\u003CMusicDataModel>(data);\n        Console.WriteLine($\"[PIM] Read: deserialized OK, version = {model.Version}, tracks = {model.Tracks.Length}\");\n    }\n    catch (Exception ex)\n    {\n        Console.WriteLine($\"[PIM] Read: deserialize error: {ex.Message}\");\n        return (ReadResult.FormatError, null);\n    }\n    // ... model.Version に応じて分岐する余地がある\n}\n",[6657],{"type":25,"tag":529,"props":6658,"children":6659},{"__ignoreMap":8},[6660,6743,6750,6767,6775,6782,6829,6926,6933,6959,6966,7024,7061,7068,7076],{"type":25,"tag":2682,"props":6661,"children":6662},{"class":2684,"line":18},[6663,6667,6671,6676,6681,6685,6690,6694,6699,6703,6708,6712,6717,6721,6726,6730,6734,6739],{"type":25,"tag":2682,"props":6664,"children":6665},{"style":2762},[6666],{"type":31,"value":2791},{"type":25,"tag":2682,"props":6668,"children":6669},{"style":2700},[6670],{"type":31,"value":3001},{"type":25,"tag":2682,"props":6672,"children":6673},{"style":2694},[6674],{"type":31,"value":6675},"ReadResult",{"type":25,"tag":2682,"props":6677,"children":6678},{"style":2852},[6679],{"type":31,"value":6680}," resultType",{"type":25,"tag":2682,"props":6682,"children":6683},{"style":2700},[6684],{"type":31,"value":2921},{"type":25,"tag":2682,"props":6686,"children":6687},{"style":2694},[6688],{"type":31,"value":6689}," Music",{"type":25,"tag":2682,"props":6691,"children":6692},{"style":2700},[6693],{"type":31,"value":2849},{"type":25,"tag":2682,"props":6695,"children":6696},{"style":2852},[6697],{"type":31,"value":6698}," music",{"type":25,"tag":2682,"props":6700,"children":6701},{"style":2700},[6702],{"type":31,"value":3021},{"type":25,"tag":2682,"props":6704,"children":6705},{"style":2852},[6706],{"type":31,"value":6707}," Read",{"type":25,"tag":2682,"props":6709,"children":6710},{"style":2700},[6711],{"type":31,"value":3076},{"type":25,"tag":2682,"props":6713,"children":6714},{"style":2688},[6715],{"type":31,"value":6716},"byte",{"type":25,"tag":2682,"props":6718,"children":6719},{"style":2700},[6720],{"type":31,"value":5887},{"type":25,"tag":2682,"props":6722,"children":6723},{"style":2852},[6724],{"type":31,"value":6725}," data",{"type":25,"tag":2682,"props":6727,"children":6728},{"style":2700},[6729],{"type":31,"value":2921},{"type":25,"tag":2682,"props":6731,"children":6732},{"style":2688},[6733],{"type":31,"value":5629},{"type":25,"tag":2682,"props":6735,"children":6736},{"style":2852},[6737],{"type":31,"value":6738}," musicId",{"type":25,"tag":2682,"props":6740,"children":6741},{"style":2700},[6742],{"type":31,"value":2978},{"type":25,"tag":2682,"props":6744,"children":6745},{"class":2684,"line":1149},[6746],{"type":25,"tag":2682,"props":6747,"children":6748},{"style":2700},[6749],{"type":31,"value":2825},{"type":25,"tag":2682,"props":6751,"children":6752},{"class":2684,"line":1159},[6753,6758,6763],{"type":25,"tag":2682,"props":6754,"children":6755},{"style":2694},[6756],{"type":31,"value":6757},"    MusicDataModel",{"type":25,"tag":2682,"props":6759,"children":6760},{"style":2852},[6761],{"type":31,"value":6762}," model",{"type":25,"tag":2682,"props":6764,"children":6765},{"style":2700},[6766],{"type":31,"value":2713},{"type":25,"tag":2682,"props":6768,"children":6769},{"class":2684,"line":2758},[6770],{"type":25,"tag":2682,"props":6771,"children":6772},{"style":2688},[6773],{"type":31,"value":6774},"    try\n",{"type":25,"tag":2682,"props":6776,"children":6777},{"class":2684,"line":2777},[6778],{"type":25,"tag":2682,"props":6779,"children":6780},{"style":2700},[6781],{"type":31,"value":2987},{"type":25,"tag":2682,"props":6783,"children":6784},{"class":2684,"line":2785},[6785,6790,6794,6799,6803,6808,6812,6816,6820,6825],{"type":25,"tag":2682,"props":6786,"children":6787},{"style":3004},[6788],{"type":31,"value":6789},"        model",{"type":25,"tag":2682,"props":6791,"children":6792},{"style":2700},[6793],{"type":31,"value":2968},{"type":25,"tag":2682,"props":6795,"children":6796},{"style":3004},[6797],{"type":31,"value":6798}," MessagePackSerializer",{"type":25,"tag":2682,"props":6800,"children":6801},{"style":2700},[6802],{"type":31,"value":2703},{"type":25,"tag":2682,"props":6804,"children":6805},{"style":2852},[6806],{"type":31,"value":6807},"Deserialize",{"type":25,"tag":2682,"props":6809,"children":6810},{"style":2700},[6811],{"type":31,"value":2891},{"type":25,"tag":2682,"props":6813,"children":6814},{"style":2694},[6815],{"type":31,"value":5526},{"type":25,"tag":2682,"props":6817,"children":6818},{"style":2700},[6819],{"type":31,"value":2901},{"type":25,"tag":2682,"props":6821,"children":6822},{"style":3004},[6823],{"type":31,"value":6824},"data",{"type":25,"tag":2682,"props":6826,"children":6827},{"style":2700},[6828],{"type":31,"value":3085},{"type":25,"tag":2682,"props":6830,"children":6831},{"class":2684,"line":2819},[6832,6837,6841,6846,6850,6855,6860,6865,6870,6874,6878,6883,6888,6892,6896,6900,6905,6909,6914,6918,6922],{"type":25,"tag":2682,"props":6833,"children":6834},{"style":3004},[6835],{"type":31,"value":6836},"        Console",{"type":25,"tag":2682,"props":6838,"children":6839},{"style":2700},[6840],{"type":31,"value":2703},{"type":25,"tag":2682,"props":6842,"children":6843},{"style":2852},[6844],{"type":31,"value":6845},"WriteLine",{"type":25,"tag":2682,"props":6847,"children":6848},{"style":2700},[6849],{"type":31,"value":3076},{"type":25,"tag":2682,"props":6851,"children":6852},{"style":3293},[6853],{"type":31,"value":6854},"$\"",{"type":25,"tag":2682,"props":6856,"children":6857},{"style":4759},[6858],{"type":31,"value":6859},"[PIM] Read: deserialized OK, version = ",{"type":25,"tag":2682,"props":6861,"children":6862},{"style":2700},[6863],{"type":31,"value":6864},"{",{"type":25,"tag":2682,"props":6866,"children":6867},{"style":4759},[6868],{"type":31,"value":6869},"model",{"type":25,"tag":2682,"props":6871,"children":6872},{"style":2700},[6873],{"type":31,"value":2703},{"type":25,"tag":2682,"props":6875,"children":6876},{"style":4759},[6877],{"type":31,"value":5320},{"type":25,"tag":2682,"props":6879,"children":6880},{"style":2700},[6881],{"type":31,"value":6882},"}",{"type":25,"tag":2682,"props":6884,"children":6885},{"style":4759},[6886],{"type":31,"value":6887},", tracks = ",{"type":25,"tag":2682,"props":6889,"children":6890},{"style":2700},[6891],{"type":31,"value":6864},{"type":25,"tag":2682,"props":6893,"children":6894},{"style":4759},[6895],{"type":31,"value":6869},{"type":25,"tag":2682,"props":6897,"children":6898},{"style":2700},[6899],{"type":31,"value":2703},{"type":25,"tag":2682,"props":6901,"children":6902},{"style":4759},[6903],{"type":31,"value":6904},"Tracks",{"type":25,"tag":2682,"props":6906,"children":6907},{"style":2700},[6908],{"type":31,"value":2703},{"type":25,"tag":2682,"props":6910,"children":6911},{"style":4759},[6912],{"type":31,"value":6913},"Length",{"type":25,"tag":2682,"props":6915,"children":6916},{"style":2700},[6917],{"type":31,"value":6882},{"type":25,"tag":2682,"props":6919,"children":6920},{"style":3293},[6921],{"type":31,"value":4766},{"type":25,"tag":2682,"props":6923,"children":6924},{"style":2700},[6925],{"type":31,"value":3085},{"type":25,"tag":2682,"props":6927,"children":6928},{"class":2684,"line":2828},[6929],{"type":25,"tag":2682,"props":6930,"children":6931},{"style":2700},[6932],{"type":31,"value":3094},{"type":25,"tag":2682,"props":6934,"children":6935},{"class":2684,"line":2862},[6936,6941,6945,6950,6955],{"type":25,"tag":2682,"props":6937,"children":6938},{"style":2688},[6939],{"type":31,"value":6940},"    catch",{"type":25,"tag":2682,"props":6942,"children":6943},{"style":2700},[6944],{"type":31,"value":3001},{"type":25,"tag":2682,"props":6946,"children":6947},{"style":2694},[6948],{"type":31,"value":6949},"Exception",{"type":25,"tag":2682,"props":6951,"children":6952},{"style":2852},[6953],{"type":31,"value":6954}," ex",{"type":25,"tag":2682,"props":6956,"children":6957},{"style":2700},[6958],{"type":31,"value":2978},{"type":25,"tag":2682,"props":6960,"children":6961},{"class":2684,"line":2870},[6962],{"type":25,"tag":2682,"props":6963,"children":6964},{"style":2700},[6965],{"type":31,"value":2987},{"type":25,"tag":2682,"props":6967,"children":6968},{"class":2684,"line":2981},[6969,6973,6977,6981,6985,6989,6994,6998,7003,7007,7012,7016,7020],{"type":25,"tag":2682,"props":6970,"children":6971},{"style":3004},[6972],{"type":31,"value":6836},{"type":25,"tag":2682,"props":6974,"children":6975},{"style":2700},[6976],{"type":31,"value":2703},{"type":25,"tag":2682,"props":6978,"children":6979},{"style":2852},[6980],{"type":31,"value":6845},{"type":25,"tag":2682,"props":6982,"children":6983},{"style":2700},[6984],{"type":31,"value":3076},{"type":25,"tag":2682,"props":6986,"children":6987},{"style":3293},[6988],{"type":31,"value":6854},{"type":25,"tag":2682,"props":6990,"children":6991},{"style":4759},[6992],{"type":31,"value":6993},"[PIM] Read: deserialize error: ",{"type":25,"tag":2682,"props":6995,"children":6996},{"style":2700},[6997],{"type":31,"value":6864},{"type":25,"tag":2682,"props":6999,"children":7000},{"style":4759},[7001],{"type":31,"value":7002},"ex",{"type":25,"tag":2682,"props":7004,"children":7005},{"style":2700},[7006],{"type":31,"value":2703},{"type":25,"tag":2682,"props":7008,"children":7009},{"style":4759},[7010],{"type":31,"value":7011},"Message",{"type":25,"tag":2682,"props":7013,"children":7014},{"style":2700},[7015],{"type":31,"value":6882},{"type":25,"tag":2682,"props":7017,"children":7018},{"style":3293},[7019],{"type":31,"value":4766},{"type":25,"tag":2682,"props":7021,"children":7022},{"style":2700},[7023],{"type":31,"value":3085},{"type":25,"tag":2682,"props":7025,"children":7026},{"class":2684,"line":2990},[7027,7032,7036,7040,7044,7049,7053,7057],{"type":25,"tag":2682,"props":7028,"children":7029},{"style":2688},[7030],{"type":31,"value":7031},"        return",{"type":25,"tag":2682,"props":7033,"children":7034},{"style":2700},[7035],{"type":31,"value":3001},{"type":25,"tag":2682,"props":7037,"children":7038},{"style":3004},[7039],{"type":31,"value":6675},{"type":25,"tag":2682,"props":7041,"children":7042},{"style":2700},[7043],{"type":31,"value":2703},{"type":25,"tag":2682,"props":7045,"children":7046},{"style":3004},[7047],{"type":31,"value":7048},"FormatError",{"type":25,"tag":2682,"props":7050,"children":7051},{"style":2700},[7052],{"type":31,"value":2921},{"type":25,"tag":2682,"props":7054,"children":7055},{"style":2762},[7056],{"type":31,"value":2973},{"type":25,"tag":2682,"props":7058,"children":7059},{"style":2700},[7060],{"type":31,"value":3085},{"type":25,"tag":2682,"props":7062,"children":7063},{"class":2684,"line":3044},[7064],{"type":25,"tag":2682,"props":7065,"children":7066},{"style":2700},[7067],{"type":31,"value":3094},{"type":25,"tag":2682,"props":7069,"children":7070},{"class":2684,"line":3065},[7071],{"type":25,"tag":2682,"props":7072,"children":7073},{"style":3931},[7074],{"type":31,"value":7075},"    // ... model.Version に応じて分岐する余地がある\n",{"type":25,"tag":2682,"props":7077,"children":7078},{"class":2684,"line":3088},[7079],{"type":25,"tag":2682,"props":7080,"children":7081},{"style":2700},[7082],{"type":31,"value":3219},{"type":25,"tag":33,"props":7084,"children":7085},{},[7086,7088,7093],{"type":31,"value":7087},"現状PIMはv2で統一されていますが、",{"type":25,"tag":529,"props":7089,"children":7091},{"className":7090},[],[7092],{"type":31,"value":5320},{"type":31,"value":7094},"フィールドを持たせているおかげで、いずれv3が必要になったら次のような選択肢を取れます。",{"type":25,"tag":198,"props":7096,"children":7097},{},[7098,7123],{"type":25,"tag":57,"props":7099,"children":7100},{},[7101,7107,7108,7114,7116,7121],{"type":25,"tag":529,"props":7102,"children":7104},{"className":7103},[],[7105],{"type":31,"value":7106},"MusicDataModelV2",{"type":31,"value":1086},{"type":25,"tag":529,"props":7109,"children":7111},{"className":7110},[],[7112],{"type":31,"value":7113},"MusicDataModelV3",{"type":31,"value":7115},"を別クラスで定義し、読み込み時に",{"type":25,"tag":529,"props":7117,"children":7119},{"className":7118},[],[7120],{"type":31,"value":5320},{"type":31,"value":7122},"を見て振り分ける",{"type":25,"tag":57,"props":7124,"children":7125},{},[7126,7131],{"type":25,"tag":529,"props":7127,"children":7129},{"className":7128},[],[7130],{"type":31,"value":5526},{"type":31,"value":7132},"を大胆に拡張しつつ、v2データは古い部分だけを読み込むフォールバックを用意する",{"type":25,"tag":33,"props":7134,"children":7135},{},[7136,7138,7143],{"type":31,"value":7137},"ここで効いてくるのが、「",{"type":25,"tag":103,"props":7139,"children":7140},{},[7141],{"type":31,"value":7142},"JSONではなくMessagePackであることの副次効果",{"type":31,"value":7144},"」です。MessagePackは余分なフィールドを自動で無視してくれるので、v3で追加したフィールドを持たないv2データを読んでも壊れません。逆に言うと、v3を書いたものをv2のコードで読むときは、追加フィールドが欠落した状態になります。",{"type":25,"tag":659,"props":7146,"children":7147},{},[7148],{"type":25,"tag":33,"props":7149,"children":7150},{},[7151,7153,7158,7160,7165,7167,7172],{"type":31,"value":7152},"Versionフィールドを",{"type":25,"tag":529,"props":7154,"children":7156},{"className":7155},[],[7157],{"type":31,"value":6035},{"type":31,"value":7159},"に置くのは意図的です。一番最初に来る数値なので、バイナリの先頭数バイトを見るだけで「これはPIM v2だ」と判定できます。ツール側からの識別が楽になるので、新しく独自フォーマットを作るなら",{"type":25,"tag":529,"props":7161,"children":7163},{"className":7162},[],[7164],{"type":31,"value":5320},{"type":31,"value":7166},"を",{"type":25,"tag":529,"props":7168,"children":7170},{"className":7169},[],[7171],{"type":31,"value":6035},{"type":31,"value":7173},"に置くことを強くお勧めします。",{"type":25,"tag":26,"props":7175,"children":7177},{"id":7176},"readresult-enum-でデシリアライズ失敗を型で表現する",[7178],{"type":31,"value":7179},"ReadResult enum でデシリアライズ失敗を型で表現する",{"type":25,"tag":33,"props":7181,"children":7182},{},[7183,7185,7191,7193,7199],{"type":31,"value":7184},"読み込み結果は単純な",{"type":25,"tag":529,"props":7186,"children":7188},{"className":7187},[],[7189],{"type":31,"value":7190},"Music?",{"type":31,"value":7192},"ではなく、",{"type":25,"tag":529,"props":7194,"children":7196},{"className":7195},[],[7197],{"type":31,"value":7198},"(ReadResult, Music?)",{"type":31,"value":7200},"のタプルで返しています。",{"type":25,"tag":524,"props":7202,"children":7204},{"className":2674,"code":7203,"language":2676,"meta":8,"style":8},"public enum ReadResult\n{\n    Success,\n    NotFound,\n    FormatError,\n    SoundError,\n}\n",[7205],{"type":25,"tag":529,"props":7206,"children":7207},{"__ignoreMap":8},[7208,7225,7232,7245,7257,7269,7281],{"type":25,"tag":2682,"props":7209,"children":7210},{"class":2684,"line":18},[7211,7215,7220],{"type":25,"tag":2682,"props":7212,"children":7213},{"style":2762},[7214],{"type":31,"value":2791},{"type":25,"tag":2682,"props":7216,"children":7217},{"style":2762},[7218],{"type":31,"value":7219}," enum",{"type":25,"tag":2682,"props":7221,"children":7222},{"style":2694},[7223],{"type":31,"value":7224}," ReadResult\n",{"type":25,"tag":2682,"props":7226,"children":7227},{"class":2684,"line":1149},[7228],{"type":25,"tag":2682,"props":7229,"children":7230},{"style":2700},[7231],{"type":31,"value":2825},{"type":25,"tag":2682,"props":7233,"children":7234},{"class":2684,"line":1159},[7235,7240],{"type":25,"tag":2682,"props":7236,"children":7237},{"style":2852},[7238],{"type":31,"value":7239},"    Success",{"type":25,"tag":2682,"props":7241,"children":7242},{"style":2700},[7243],{"type":31,"value":7244},",\n",{"type":25,"tag":2682,"props":7246,"children":7247},{"class":2684,"line":2758},[7248,7253],{"type":25,"tag":2682,"props":7249,"children":7250},{"style":2852},[7251],{"type":31,"value":7252},"    NotFound",{"type":25,"tag":2682,"props":7254,"children":7255},{"style":2700},[7256],{"type":31,"value":7244},{"type":25,"tag":2682,"props":7258,"children":7259},{"class":2684,"line":2777},[7260,7265],{"type":25,"tag":2682,"props":7261,"children":7262},{"style":2852},[7263],{"type":31,"value":7264},"    FormatError",{"type":25,"tag":2682,"props":7266,"children":7267},{"style":2700},[7268],{"type":31,"value":7244},{"type":25,"tag":2682,"props":7270,"children":7271},{"class":2684,"line":2785},[7272,7277],{"type":25,"tag":2682,"props":7273,"children":7274},{"style":2852},[7275],{"type":31,"value":7276},"    SoundError",{"type":25,"tag":2682,"props":7278,"children":7279},{"style":2700},[7280],{"type":31,"value":7244},{"type":25,"tag":2682,"props":7282,"children":7283},{"class":2684,"line":2819},[7284],{"type":25,"tag":2682,"props":7285,"children":7286},{"style":2700},[7287],{"type":31,"value":3219},{"type":25,"tag":33,"props":7289,"children":7290},{},[7291,7293,7299,7301,7306],{"type":31,"value":7292},"最初は",{"type":25,"tag":529,"props":7294,"children":7296},{"className":7295},[],[7297],{"type":31,"value":7298},"try-catch",{"type":31,"value":7300},"で例外を投げる設計だったのですが、 ",{"type":25,"tag":103,"props":7302,"children":7303},{},[7304],{"type":31,"value":7305},"「ユーザーに見せるエラーメッセージを分岐させたい」",{"type":31,"value":7307}," という要件には例外クラスよりenumの方が相性が良いと気付きました。たとえば「フォーマットが壊れている」と「音階の値が不正」では、ユーザーに見せるメッセージが違います。前者は「ファイルが壊れています」で終わりですが、後者は「PICOMのバージョンが古い可能性があります」と案内したい。enumで分岐すればswitch式で網羅チェックも効くので、例外より安全です。",{"type":25,"tag":33,"props":7309,"children":7310},{},[7311,7313,7319],{"type":31,"value":7312},"C#では",{"type":25,"tag":529,"props":7314,"children":7316},{"className":7315},[],[7317],{"type":31,"value":7318},"TryXX",{"type":31,"value":7320},"というメソッドを作ってout引数で結果を返す方法が主流ですが、非同期メソッドでは使えないという問題もあります。PICOMの一部メソッドは非同期で実装されており、できる限りソースコードのパターンを揃えた方が良いという理由もあり、タプル+enumの形で統一しました。",{"type":25,"tag":33,"props":7322,"children":7323},{},[7324],{"type":31,"value":7325},"ただ、この方法にも弱点があり、MusicのNullチェックとReadResultがSuccessかどうかの両方をチェックしないといけないという手間があります。",{"type":25,"tag":26,"props":7327,"children":7329},{"id":7328},"バックアップ機能にも対応複数の曲をまとめて1ファイルに",[7330],{"type":31,"value":7331},"バックアップ機能にも対応！複数の曲をまとめて1ファイルに",{"type":25,"tag":33,"props":7333,"children":7334},{},[7335,7337,7343],{"type":31,"value":7336},"PIMはもう一つ、バックアップ用の",{"type":25,"tag":529,"props":7338,"children":7340},{"className":7339},[],[7341],{"type":31,"value":7342},"BackupDataModel",{"type":31,"value":7344},"を持っています。",{"type":25,"tag":524,"props":7346,"children":7348},{"className":2674,"code":7347,"language":2676,"meta":8,"style":8},"public byte[] WriteBackup(List\u003CMusic> musics)\n{\n    var backup = new BackupDataModel\n    {\n        Version = 1,\n        CreatedAt = DateTime.UtcNow,\n        Musics = musics.Select(ToDataModel).ToArray(),\n    };\n    return MessagePackSerializer.Serialize(backup);\n}\n",[7349],{"type":25,"tag":529,"props":7350,"children":7351},{"__ignoreMap":8},[7352,7403,7410,7436,7443,7464,7494,7543,7551,7585],{"type":25,"tag":2682,"props":7353,"children":7354},{"class":2684,"line":18},[7355,7359,7364,7368,7373,7377,7382,7386,7390,7394,7399],{"type":25,"tag":2682,"props":7356,"children":7357},{"style":2762},[7358],{"type":31,"value":2791},{"type":25,"tag":2682,"props":7360,"children":7361},{"style":2688},[7362],{"type":31,"value":7363}," byte",{"type":25,"tag":2682,"props":7365,"children":7366},{"style":2700},[7367],{"type":31,"value":5887},{"type":25,"tag":2682,"props":7369,"children":7370},{"style":2852},[7371],{"type":31,"value":7372}," WriteBackup",{"type":25,"tag":2682,"props":7374,"children":7375},{"style":2700},[7376],{"type":31,"value":3076},{"type":25,"tag":2682,"props":7378,"children":7379},{"style":2694},[7380],{"type":31,"value":7381},"List",{"type":25,"tag":2682,"props":7383,"children":7384},{"style":2700},[7385],{"type":31,"value":2891},{"type":25,"tag":2682,"props":7387,"children":7388},{"style":2694},[7389],{"type":31,"value":4629},{"type":25,"tag":2682,"props":7391,"children":7392},{"style":2700},[7393],{"type":31,"value":3548},{"type":25,"tag":2682,"props":7395,"children":7396},{"style":2852},[7397],{"type":31,"value":7398}," musics",{"type":25,"tag":2682,"props":7400,"children":7401},{"style":2700},[7402],{"type":31,"value":2978},{"type":25,"tag":2682,"props":7404,"children":7405},{"class":2684,"line":1149},[7406],{"type":25,"tag":2682,"props":7407,"children":7408},{"style":2700},[7409],{"type":31,"value":2825},{"type":25,"tag":2682,"props":7411,"children":7412},{"class":2684,"line":1159},[7413,7418,7423,7427,7431],{"type":25,"tag":2682,"props":7414,"children":7415},{"style":2762},[7416],{"type":31,"value":7417},"    var",{"type":25,"tag":2682,"props":7419,"children":7420},{"style":2852},[7421],{"type":31,"value":7422}," backup",{"type":25,"tag":2682,"props":7424,"children":7425},{"style":2700},[7426],{"type":31,"value":2968},{"type":25,"tag":2682,"props":7428,"children":7429},{"style":2762},[7430],{"type":31,"value":3031},{"type":25,"tag":2682,"props":7432,"children":7433},{"style":2694},[7434],{"type":31,"value":7435}," BackupDataModel\n",{"type":25,"tag":2682,"props":7437,"children":7438},{"class":2684,"line":2758},[7439],{"type":25,"tag":2682,"props":7440,"children":7441},{"style":2700},[7442],{"type":31,"value":2987},{"type":25,"tag":2682,"props":7444,"children":7445},{"class":2684,"line":2777},[7446,7451,7455,7460],{"type":25,"tag":2682,"props":7447,"children":7448},{"style":3004},[7449],{"type":31,"value":7450},"        Version",{"type":25,"tag":2682,"props":7452,"children":7453},{"style":2700},[7454],{"type":31,"value":2968},{"type":25,"tag":2682,"props":7456,"children":7457},{"style":5609},[7458],{"type":31,"value":7459}," 1",{"type":25,"tag":2682,"props":7461,"children":7462},{"style":2700},[7463],{"type":31,"value":7244},{"type":25,"tag":2682,"props":7465,"children":7466},{"class":2684,"line":2785},[7467,7472,7476,7481,7485,7490],{"type":25,"tag":2682,"props":7468,"children":7469},{"style":3004},[7470],{"type":31,"value":7471},"        CreatedAt",{"type":25,"tag":2682,"props":7473,"children":7474},{"style":2700},[7475],{"type":31,"value":2968},{"type":25,"tag":2682,"props":7477,"children":7478},{"style":3004},[7479],{"type":31,"value":7480}," DateTime",{"type":25,"tag":2682,"props":7482,"children":7483},{"style":2700},[7484],{"type":31,"value":2703},{"type":25,"tag":2682,"props":7486,"children":7487},{"style":3004},[7488],{"type":31,"value":7489},"UtcNow",{"type":25,"tag":2682,"props":7491,"children":7492},{"style":2700},[7493],{"type":31,"value":7244},{"type":25,"tag":2682,"props":7495,"children":7496},{"class":2684,"line":2819},[7497,7502,7506,7510,7514,7519,7523,7528,7533,7538],{"type":25,"tag":2682,"props":7498,"children":7499},{"style":3004},[7500],{"type":31,"value":7501},"        Musics",{"type":25,"tag":2682,"props":7503,"children":7504},{"style":2700},[7505],{"type":31,"value":2968},{"type":25,"tag":2682,"props":7507,"children":7508},{"style":3004},[7509],{"type":31,"value":7398},{"type":25,"tag":2682,"props":7511,"children":7512},{"style":2700},[7513],{"type":31,"value":2703},{"type":25,"tag":2682,"props":7515,"children":7516},{"style":2852},[7517],{"type":31,"value":7518},"Select",{"type":25,"tag":2682,"props":7520,"children":7521},{"style":2700},[7522],{"type":31,"value":3076},{"type":25,"tag":2682,"props":7524,"children":7525},{"style":3004},[7526],{"type":31,"value":7527},"ToDataModel",{"type":25,"tag":2682,"props":7529,"children":7530},{"style":2700},[7531],{"type":31,"value":7532},").",{"type":25,"tag":2682,"props":7534,"children":7535},{"style":2852},[7536],{"type":31,"value":7537},"ToArray",{"type":25,"tag":2682,"props":7539,"children":7540},{"style":2700},[7541],{"type":31,"value":7542},"(),\n",{"type":25,"tag":2682,"props":7544,"children":7545},{"class":2684,"line":2828},[7546],{"type":25,"tag":2682,"props":7547,"children":7548},{"style":2700},[7549],{"type":31,"value":7550},"    };\n",{"type":25,"tag":2682,"props":7552,"children":7553},{"class":2684,"line":2862},[7554,7559,7563,7567,7572,7576,7581],{"type":25,"tag":2682,"props":7555,"children":7556},{"style":2688},[7557],{"type":31,"value":7558},"    return",{"type":25,"tag":2682,"props":7560,"children":7561},{"style":3004},[7562],{"type":31,"value":6798},{"type":25,"tag":2682,"props":7564,"children":7565},{"style":2700},[7566],{"type":31,"value":2703},{"type":25,"tag":2682,"props":7568,"children":7569},{"style":2852},[7570],{"type":31,"value":7571},"Serialize",{"type":25,"tag":2682,"props":7573,"children":7574},{"style":2700},[7575],{"type":31,"value":3076},{"type":25,"tag":2682,"props":7577,"children":7578},{"style":3004},[7579],{"type":31,"value":7580},"backup",{"type":25,"tag":2682,"props":7582,"children":7583},{"style":2700},[7584],{"type":31,"value":3085},{"type":25,"tag":2682,"props":7586,"children":7587},{"class":2684,"line":2870},[7588],{"type":25,"tag":2682,"props":7589,"children":7590},{"style":2700},[7591],{"type":31,"value":3219},{"type":25,"tag":33,"props":7593,"children":7594},{},[7595,7597,7602,7604,7609,7611,7616],{"type":31,"value":7596},"このバックアップの",{"type":25,"tag":529,"props":7598,"children":7600},{"className":7599},[],[7601],{"type":31,"value":5320},{"type":31,"value":7603},"は曲単体の",{"type":25,"tag":529,"props":7605,"children":7607},{"className":7606},[],[7608],{"type":31,"value":5320},{"type":31,"value":7610},"とは",{"type":25,"tag":103,"props":7612,"children":7613},{},[7614],{"type":31,"value":7615},"別系統",{"type":31,"value":7617},"で管理しています。最初は同じ番号を使っていたのですが、「バックアップ形式だけ変えたい（曲フォーマットは据え置き）」みたいな要件が出て来る可能性を考えて別系統に分けました。",{"type":25,"tag":26,"props":7619,"children":7621},{"id":7620},"追記実際にv3へ上げました",[7622],{"type":31,"value":7623},"追記：実際にv3へ上げました",{"type":25,"tag":33,"props":7625,"children":7626},{},[7627,7629,7635],{"type":31,"value":7628},"この記事を書いた直後に、本文で触れた「いずれv3が必要になったら」が実際に発生しました。PICOMの実装を眺めていて、楽曲IDが「IndexedDBのキー」と「メモリ上の ",{"type":25,"tag":529,"props":7630,"children":7632},{"className":7631},[],[7633],{"type":31,"value":7634},"Music.Id",{"type":31,"value":7636},"」の2箇所に散っており、PIMバイナリ自体はIDを持たない状態だと気付いたのがきっかけです。",{"type":25,"tag":33,"props":7638,"children":7639},{},[7640,7642,7647,7649,7655,7657,7662,7664,7669],{"type":31,"value":7641},"そこで ",{"type":25,"tag":529,"props":7643,"children":7645},{"className":7644},[],[7646],{"type":31,"value":5526},{"type":31,"value":7648}," に ",{"type":25,"tag":529,"props":7650,"children":7652},{"className":7651},[],[7653],{"type":31,"value":7654},"[Key(4)] public int Id { get; set; }",{"type":31,"value":7656}," を追加し、",{"type":25,"tag":529,"props":7658,"children":7660},{"className":7659},[],[7661],{"type":31,"value":5320},{"type":31,"value":7663}," を ",{"type":25,"tag":529,"props":7665,"children":7667},{"className":7666},[],[7668],{"type":31,"value":5866},{"type":31,"value":7670}," に上げました。",{"type":25,"tag":524,"props":7672,"children":7674},{"className":2674,"code":7673,"language":2676,"meta":8,"style":8},"[MessagePackObject]\npublic class MusicDataModel\n{\n    [Key(0)] public int Version { get; set; } = 3;\n    [Key(1)] public MusicMetadataModel Metadata { get; set; } = new();\n    [Key(2)] public MusicSettingsModel Settings { get; set; } = new();\n    [Key(3)] public TrackDataModel[] Tracks { get; set; } = [];\n    [Key(4)] public int Id { get; set; }\n}\n",[7675],{"type":25,"tag":529,"props":7676,"children":7677},{"__ignoreMap":8},[7678,7693,7708,7715,7787,7858,7929,8000,8060],{"type":25,"tag":2682,"props":7679,"children":7680},{"class":2684,"line":18},[7681,7685,7689],{"type":25,"tag":2682,"props":7682,"children":7683},{"style":2700},[7684],{"type":31,"value":5556},{"type":25,"tag":2682,"props":7686,"children":7687},{"style":2694},[7688],{"type":31,"value":5561},{"type":25,"tag":2682,"props":7690,"children":7691},{"style":2700},[7692],{"type":31,"value":5566},{"type":25,"tag":2682,"props":7694,"children":7695},{"class":2684,"line":1149},[7696,7700,7704],{"type":25,"tag":2682,"props":7697,"children":7698},{"style":2762},[7699],{"type":31,"value":2791},{"type":25,"tag":2682,"props":7701,"children":7702},{"style":2762},[7703],{"type":31,"value":2801},{"type":25,"tag":2682,"props":7705,"children":7706},{"style":2694},[7707],{"type":31,"value":5582},{"type":25,"tag":2682,"props":7709,"children":7710},{"class":2684,"line":1159},[7711],{"type":25,"tag":2682,"props":7712,"children":7713},{"style":2700},[7714],{"type":31,"value":2825},{"type":25,"tag":2682,"props":7716,"children":7717},{"class":2684,"line":2758},[7718,7722,7726,7730,7734,7738,7742,7746,7750,7754,7758,7762,7766,7770,7774,7778,7783],{"type":25,"tag":2682,"props":7719,"children":7720},{"style":2700},[7721],{"type":31,"value":5597},{"type":25,"tag":2682,"props":7723,"children":7724},{"style":2694},[7725],{"type":31,"value":5602},{"type":25,"tag":2682,"props":7727,"children":7728},{"style":2700},[7729],{"type":31,"value":3076},{"type":25,"tag":2682,"props":7731,"children":7732},{"style":5609},[7733],{"type":31,"value":5612},{"type":25,"tag":2682,"props":7735,"children":7736},{"style":2700},[7737],{"type":31,"value":6109},{"type":25,"tag":2682,"props":7739,"children":7740},{"style":2762},[7741],{"type":31,"value":6114},{"type":25,"tag":2682,"props":7743,"children":7744},{"style":2688},[7745],{"type":31,"value":5629},{"type":25,"tag":2682,"props":7747,"children":7748},{"style":2852},[7749],{"type":31,"value":5634},{"type":25,"tag":2682,"props":7751,"children":7752},{"style":2700},[7753],{"type":31,"value":3698},{"type":25,"tag":2682,"props":7755,"children":7756},{"style":2762},[7757],{"type":31,"value":3703},{"type":25,"tag":2682,"props":7759,"children":7760},{"style":2700},[7761],{"type":31,"value":3708},{"type":25,"tag":2682,"props":7763,"children":7764},{"style":2762},[7765],{"type":31,"value":3718},{"type":25,"tag":2682,"props":7767,"children":7768},{"style":2700},[7769],{"type":31,"value":3708},{"type":25,"tag":2682,"props":7771,"children":7772},{"style":2700},[7773],{"type":31,"value":3727},{"type":25,"tag":2682,"props":7775,"children":7776},{"style":2700},[7777],{"type":31,"value":2968},{"type":25,"tag":2682,"props":7779,"children":7780},{"style":5609},[7781],{"type":31,"value":7782}," 3",{"type":25,"tag":2682,"props":7784,"children":7785},{"style":2700},[7786],{"type":31,"value":2713},{"type":25,"tag":2682,"props":7788,"children":7789},{"class":2684,"line":2777},[7790,7794,7798,7802,7806,7810,7814,7818,7822,7826,7830,7834,7838,7842,7846,7850,7854],{"type":25,"tag":2682,"props":7791,"children":7792},{"style":2700},[7793],{"type":31,"value":5597},{"type":25,"tag":2682,"props":7795,"children":7796},{"style":2694},[7797],{"type":31,"value":5602},{"type":25,"tag":2682,"props":7799,"children":7800},{"style":2700},[7801],{"type":31,"value":3076},{"type":25,"tag":2682,"props":7803,"children":7804},{"style":5609},[7805],{"type":31,"value":5698},{"type":25,"tag":2682,"props":7807,"children":7808},{"style":2700},[7809],{"type":31,"value":6109},{"type":25,"tag":2682,"props":7811,"children":7812},{"style":2762},[7813],{"type":31,"value":6114},{"type":25,"tag":2682,"props":7815,"children":7816},{"style":2694},[7817],{"type":31,"value":5714},{"type":25,"tag":2682,"props":7819,"children":7820},{"style":2852},[7821],{"type":31,"value":5719},{"type":25,"tag":2682,"props":7823,"children":7824},{"style":2700},[7825],{"type":31,"value":3698},{"type":25,"tag":2682,"props":7827,"children":7828},{"style":2762},[7829],{"type":31,"value":3703},{"type":25,"tag":2682,"props":7831,"children":7832},{"style":2700},[7833],{"type":31,"value":3708},{"type":25,"tag":2682,"props":7835,"children":7836},{"style":2762},[7837],{"type":31,"value":3718},{"type":25,"tag":2682,"props":7839,"children":7840},{"style":2700},[7841],{"type":31,"value":3708},{"type":25,"tag":2682,"props":7843,"children":7844},{"style":2700},[7845],{"type":31,"value":3727},{"type":25,"tag":2682,"props":7847,"children":7848},{"style":2700},[7849],{"type":31,"value":2968},{"type":25,"tag":2682,"props":7851,"children":7852},{"style":2762},[7853],{"type":31,"value":3031},{"type":25,"tag":2682,"props":7855,"children":7856},{"style":2700},[7857],{"type":31,"value":3041},{"type":25,"tag":2682,"props":7859,"children":7860},{"class":2684,"line":2785},[7861,7865,7869,7873,7877,7881,7885,7889,7893,7897,7901,7905,7909,7913,7917,7921,7925],{"type":25,"tag":2682,"props":7862,"children":7863},{"style":2700},[7864],{"type":31,"value":5597},{"type":25,"tag":2682,"props":7866,"children":7867},{"style":2694},[7868],{"type":31,"value":5602},{"type":25,"tag":2682,"props":7870,"children":7871},{"style":2700},[7872],{"type":31,"value":3076},{"type":25,"tag":2682,"props":7874,"children":7875},{"style":5609},[7876],{"type":31,"value":5782},{"type":25,"tag":2682,"props":7878,"children":7879},{"style":2700},[7880],{"type":31,"value":6109},{"type":25,"tag":2682,"props":7882,"children":7883},{"style":2762},[7884],{"type":31,"value":6114},{"type":25,"tag":2682,"props":7886,"children":7887},{"style":2694},[7888],{"type":31,"value":5798},{"type":25,"tag":2682,"props":7890,"children":7891},{"style":2852},[7892],{"type":31,"value":5803},{"type":25,"tag":2682,"props":7894,"children":7895},{"style":2700},[7896],{"type":31,"value":3698},{"type":25,"tag":2682,"props":7898,"children":7899},{"style":2762},[7900],{"type":31,"value":3703},{"type":25,"tag":2682,"props":7902,"children":7903},{"style":2700},[7904],{"type":31,"value":3708},{"type":25,"tag":2682,"props":7906,"children":7907},{"style":2762},[7908],{"type":31,"value":3718},{"type":25,"tag":2682,"props":7910,"children":7911},{"style":2700},[7912],{"type":31,"value":3708},{"type":25,"tag":2682,"props":7914,"children":7915},{"style":2700},[7916],{"type":31,"value":3727},{"type":25,"tag":2682,"props":7918,"children":7919},{"style":2700},[7920],{"type":31,"value":2968},{"type":25,"tag":2682,"props":7922,"children":7923},{"style":2762},[7924],{"type":31,"value":3031},{"type":25,"tag":2682,"props":7926,"children":7927},{"style":2700},[7928],{"type":31,"value":3041},{"type":25,"tag":2682,"props":7930,"children":7931},{"class":2684,"line":2819},[7932,7936,7940,7944,7948,7952,7956,7960,7964,7968,7972,7976,7980,7984,7988,7992,7996],{"type":25,"tag":2682,"props":7933,"children":7934},{"style":2700},[7935],{"type":31,"value":5597},{"type":25,"tag":2682,"props":7937,"children":7938},{"style":2694},[7939],{"type":31,"value":5602},{"type":25,"tag":2682,"props":7941,"children":7942},{"style":2700},[7943],{"type":31,"value":3076},{"type":25,"tag":2682,"props":7945,"children":7946},{"style":5609},[7947],{"type":31,"value":5866},{"type":25,"tag":2682,"props":7949,"children":7950},{"style":2700},[7951],{"type":31,"value":6109},{"type":25,"tag":2682,"props":7953,"children":7954},{"style":2762},[7955],{"type":31,"value":6114},{"type":25,"tag":2682,"props":7957,"children":7958},{"style":2694},[7959],{"type":31,"value":5882},{"type":25,"tag":2682,"props":7961,"children":7962},{"style":2700},[7963],{"type":31,"value":5887},{"type":25,"tag":2682,"props":7965,"children":7966},{"style":2852},[7967],{"type":31,"value":5892},{"type":25,"tag":2682,"props":7969,"children":7970},{"style":2700},[7971],{"type":31,"value":3698},{"type":25,"tag":2682,"props":7973,"children":7974},{"style":2762},[7975],{"type":31,"value":3703},{"type":25,"tag":2682,"props":7977,"children":7978},{"style":2700},[7979],{"type":31,"value":3708},{"type":25,"tag":2682,"props":7981,"children":7982},{"style":2762},[7983],{"type":31,"value":3718},{"type":25,"tag":2682,"props":7985,"children":7986},{"style":2700},[7987],{"type":31,"value":3708},{"type":25,"tag":2682,"props":7989,"children":7990},{"style":2700},[7991],{"type":31,"value":3727},{"type":25,"tag":2682,"props":7993,"children":7994},{"style":2700},[7995],{"type":31,"value":2968},{"type":25,"tag":2682,"props":7997,"children":7998},{"style":2700},[7999],{"type":31,"value":5925},{"type":25,"tag":2682,"props":8001,"children":8002},{"class":2684,"line":2828},[8003,8007,8011,8015,8019,8023,8027,8031,8036,8040,8044,8048,8052,8056],{"type":25,"tag":2682,"props":8004,"children":8005},{"style":2700},[8006],{"type":31,"value":5597},{"type":25,"tag":2682,"props":8008,"children":8009},{"style":2694},[8010],{"type":31,"value":5602},{"type":25,"tag":2682,"props":8012,"children":8013},{"style":2700},[8014],{"type":31,"value":3076},{"type":25,"tag":2682,"props":8016,"children":8017},{"style":5609},[8018],{"type":31,"value":6549},{"type":25,"tag":2682,"props":8020,"children":8021},{"style":2700},[8022],{"type":31,"value":6109},{"type":25,"tag":2682,"props":8024,"children":8025},{"style":2762},[8026],{"type":31,"value":6114},{"type":25,"tag":2682,"props":8028,"children":8029},{"style":2688},[8030],{"type":31,"value":5629},{"type":25,"tag":2682,"props":8032,"children":8033},{"style":2852},[8034],{"type":31,"value":8035}," Id",{"type":25,"tag":2682,"props":8037,"children":8038},{"style":2700},[8039],{"type":31,"value":3698},{"type":25,"tag":2682,"props":8041,"children":8042},{"style":2762},[8043],{"type":31,"value":3703},{"type":25,"tag":2682,"props":8045,"children":8046},{"style":2700},[8047],{"type":31,"value":3708},{"type":25,"tag":2682,"props":8049,"children":8050},{"style":2762},[8051],{"type":31,"value":3718},{"type":25,"tag":2682,"props":8053,"children":8054},{"style":2700},[8055],{"type":31,"value":3708},{"type":25,"tag":2682,"props":8057,"children":8058},{"style":2700},[8059],{"type":31,"value":6147},{"type":25,"tag":2682,"props":8061,"children":8062},{"class":2684,"line":2862},[8063],{"type":25,"tag":2682,"props":8064,"children":8065},{"style":2700},[8066],{"type":31,"value":3219},{"type":25,"tag":33,"props":8068,"children":8069},{},[8070,8072,8077,8079,8085,8087,8093,8095,8101],{"type":31,"value":8071},"本文で強調した「",{"type":25,"tag":529,"props":8073,"children":8075},{"className":8074},[],[8076],{"type":31,"value":5328},{"type":31,"value":8078},"番号は追記のみ」のルールに従って、既存の ",{"type":25,"tag":529,"props":8080,"children":8082},{"className":8081},[],[8083],{"type":31,"value":8084},"[Key(0)]〜[Key(3)]",{"type":31,"value":8086}," は一切触らず、末尾に ",{"type":25,"tag":529,"props":8088,"children":8090},{"className":8089},[],[8091],{"type":31,"value":8092},"[Key(4)]",{"type":31,"value":8094}," を足すだけで済みました。そして、読み込み時に ",{"type":25,"tag":529,"props":8096,"children":8098},{"className":8097},[],[8099],{"type":31,"value":8100},"model.Version \u003C 3",{"type":31,"value":8102}," かどうかで旧形式かを判定するだけで済み、バイナリ先頭を見れば版が分かる設計のありがたみを感じました。",{"type":25,"tag":33,"props":8104,"children":8105},{},[8106,8108,8113,8115,8120],{"type":31,"value":8107},"記事本文で「",{"type":25,"tag":529,"props":8109,"children":8111},{"className":8110},[],[8112],{"type":31,"value":5320},{"type":31,"value":8114},"フィールドは最初から入れる。後付けすると必ず苦労する」と書きましたが、",{"type":25,"tag":103,"props":8116,"children":8117},{},[8118],{"type":31,"value":8119},"自分で書いた記事の主張に後から自分で救われた",{"type":31,"value":8121},"形になりました。バージョン管理対応の思想を最初から仕込んでおく価値は、実際に次の版を作るまで実感しづらいのですが、こうやって発揮される瞬間が必ず来ます。",{"type":25,"tag":26,"props":8123,"children":8124},{"id":1746},[8125],{"type":31,"value":1746},{"type":25,"tag":53,"props":8127,"children":8128},{},[8129,8134,8144,8156],{"type":25,"tag":57,"props":8130,"children":8131},{},[8132],{"type":31,"value":8133},"デバッグ性よりサイズ・バイナリとしての扱いやすさ・フォーマット拡張への耐性が大事なら、JSONではなくMessagePackを選ぶ価値がある",{"type":25,"tag":57,"props":8135,"children":8136},{},[8137,8142],{"type":25,"tag":529,"props":8138,"children":8140},{"className":8139},[],[8141],{"type":31,"value":5328},{"type":31,"value":8143},"番号は一度付けたら絶対に変えない、削除もしない",{"type":25,"tag":57,"props":8145,"children":8146},{},[8147,8149,8154],{"type":31,"value":8148},"フォーマットの",{"type":25,"tag":529,"props":8150,"children":8152},{"className":8151},[],[8153],{"type":31,"value":5320},{"type":31,"value":8155},"フィールドは最初から入れる。これを後付けすると必ず苦労する",{"type":25,"tag":57,"props":8157,"children":8158},{},[8159],{"type":31,"value":8160},"デシリアライズ結果はenumで分岐できるようにしておくと、ユーザーへのエラーメッセージ設計が楽になる",{"type":25,"tag":5239,"props":8162,"children":8163},{},[8164],{"type":31,"value":5243},{"title":8,"searchDepth":1149,"depth":1149,"links":8166},[8167,8168,8172,8173,8175,8176,8177,8178,8179],{"id":28,"depth":1149,"text":28},{"id":5333,"depth":1149,"text":5336,"children":8169},[8170,8171],{"id":5363,"depth":1159,"text":5363},{"id":5403,"depth":1159,"text":5406},{"id":5512,"depth":1149,"text":5515},{"id":6013,"depth":1149,"text":8174},"[Key]番号は追記のみ、絶対に削除しない",{"id":6614,"depth":1149,"text":6617},{"id":7176,"depth":1149,"text":7179},{"id":7328,"depth":1149,"text":7331},{"id":7620,"depth":1149,"text":7623},{"id":1746,"depth":1149,"text":1746},"content:articles:tech:blazor:messagepack-pim-format.md","articles/tech/blazor/messagepack-pim-format.md","articles/tech/blazor/messagepack-pim-format",{"_path":8184,"_dir":2548,"_draft":7,"_partial":7,"_locale":8,"title":8185,"description":8186,"date":8187,"tags":8188,"rowTypeId":18,"sitemap":8191,"body":8192,"_type":1180,"_id":11282,"_source":1182,"_file":11283,"_stem":11284,"_extension":1185},"/articles/tech/blazor/undo-redo-command-pattern","StackベースのUndo/RedoをC#で実装する｜CompositeCommandで複数操作も一括取り消し","楽譜エディタのUndo/RedoをCommandパターンで実装した実例を紹介します。2本のStackで履歴を管理し、CompositeCommandで複数操作を1手としてまとめ、TrimHistoryで履歴上限を設ける方法を解説します。","2026-04-17",[2553,8189,8190,2555,2557],"デザインパターン","Command",{"loc":8184,"lastmod":8187,"priority":18},{"type":22,"children":8193,"toc":11273},[8194,8198,8203,8216,8246,8252,8257,8269,8275,8280,8501,8552,8557,8990,9015,9021,9047,9942,9947,9967,10001,10007,10012,10025,10313,10339,10403,10424,10430,10457,10462,10993,11005,11010,11201,11206,11210,11269],{"type":25,"tag":26,"props":8195,"children":8196},{"id":28},[8197],{"type":31,"value":28},{"type":25,"tag":33,"props":8199,"children":8200},{},[8201],{"type":31,"value":8202},"楽譜エディタ「PICOM」を作り始めて、Undo/Redoがないと不便だと感じ実装しました。おそらく、多くの編集系UIを持つアプリでは必須機能だと思います。",{"type":25,"tag":33,"props":8204,"children":8205},{},[8206,8208,8214],{"type":31,"value":8207},"そこで、この記事では、PICOMで実装したUndo/Redoの仕組みを紹介します。教科書通りの",{"type":25,"tag":633,"props":8209,"children":8211},{"content":8210},"実行する処理をオブジェクトとしてカプセル化し、履歴管理や取り消しを可能にするデザインパターン",[8212],{"type":31,"value":8213},"Commandパターン",{"type":31,"value":8215},"ですが、実際に使えるものにするには「複数操作を1手としてまとめたい」「履歴が無限に増えるのは困る」といった現実的な課題があります。そのあたりをどう解決したかを具体的に書きます。",{"type":25,"tag":39,"props":8217,"children":8218},{},[8219],{"type":25,"tag":33,"props":8220,"children":8221},{},[8222,8228,8230,8236,8238,8244],{"type":25,"tag":529,"props":8223,"children":8225},{"className":8224},[],[8226],{"type":31,"value":8227},"ICommand",{"type":31,"value":8229},"インターフェースを定義し、",{"type":25,"tag":529,"props":8231,"children":8233},{"className":8232},[],[8234],{"type":31,"value":8235},"UndoRedoManager",{"type":31,"value":8237},"が2本のStackで履歴を管理します。",{"type":25,"tag":529,"props":8239,"children":8241},{"className":8240},[],[8242],{"type":31,"value":8243},"CompositeCommand",{"type":31,"value":8245},"で複数のコマンドを1手にまとめ、ピアノロール上の複数音符を同時に移動するような操作も一発でUndoできます。履歴には上限を設けてメモリリークを防いでいます。",{"type":25,"tag":26,"props":8247,"children":8249},{"id":8248},"undoredoの仕組みを考える",[8250],{"type":31,"value":8251},"Undo/Redoの仕組みを考える",{"type":25,"tag":33,"props":8253,"children":8254},{},[8255],{"type":31,"value":8256},"Undo/Redoを愚直に実装する方法として、操作をするたびに状態を時系列に沿って記憶するという方法が思いつきます。しかし、この方法ではメモリ効率も悪く、状態管理も非常に複雑化します。",{"type":25,"tag":33,"props":8258,"children":8259},{},[8260,8262,8267],{"type":31,"value":8261},"そこで、デザインパターンであるCommandパターンを利用します。Commandパターンでは、状態ではなく ",{"type":25,"tag":103,"props":8263,"children":8264},{},[8265],{"type":31,"value":8266},"操作",{"type":31,"value":8268}," を記憶するため、以前の状態に復元したり、次の状態へ進めたりなどが容易に実現できます。\n続いて、実際の実装を見ていきましょう。",{"type":25,"tag":26,"props":8270,"children":8272},{"id":8271},"icommand最小インターフェースから始める",[8273],{"type":31,"value":8274},"ICommand：最小インターフェースから始める",{"type":25,"tag":33,"props":8276,"children":8277},{},[8278],{"type":31,"value":8279},"Commandパターンの出発点は「実行」と「取り消し」を1ペアで持つインターフェースです。PICOMでは次のように定義しています。",{"type":25,"tag":524,"props":8281,"children":8283},{"className":2674,"code":8282,"language":2676,"meta":8,"style":8},"public interface ICommand\n{\n    /// \u003Csummary>コマンドを実行する\u003C/summary>\n    void Execute();\n\n    /// \u003Csummary>コマンドを取り消す\u003C/summary>\n    void Undo();\n\n    /// \u003Csummary>コマンドの説明（デバッグ/UI表示用）\u003C/summary>\n    string Description { get; }\n}\n",[8284],{"type":25,"tag":529,"props":8285,"children":8286},{"__ignoreMap":8},[8287,8303,8310,8346,8363,8370,8406,8422,8429,8465,8494],{"type":25,"tag":2682,"props":8288,"children":8289},{"class":2684,"line":18},[8290,8294,8298],{"type":25,"tag":2682,"props":8291,"children":8292},{"style":2762},[8293],{"type":31,"value":2791},{"type":25,"tag":2682,"props":8295,"children":8296},{"style":2762},[8297],{"type":31,"value":4035},{"type":25,"tag":2682,"props":8299,"children":8300},{"style":2694},[8301],{"type":31,"value":8302}," ICommand\n",{"type":25,"tag":2682,"props":8304,"children":8305},{"class":2684,"line":1149},[8306],{"type":25,"tag":2682,"props":8307,"children":8308},{"style":2700},[8309],{"type":31,"value":2825},{"type":25,"tag":2682,"props":8311,"children":8312},{"class":2684,"line":1159},[8313,8317,8321,8325,8329,8334,8338,8342],{"type":25,"tag":2682,"props":8314,"children":8315},{"style":3931},[8316],{"type":31,"value":4067},{"type":25,"tag":2682,"props":8318,"children":8319},{"style":2700},[8320],{"type":31,"value":2891},{"type":25,"tag":2682,"props":8322,"children":8323},{"style":2688},[8324],{"type":31,"value":4076},{"type":25,"tag":2682,"props":8326,"children":8327},{"style":2700},[8328],{"type":31,"value":3548},{"type":25,"tag":2682,"props":8330,"children":8331},{"style":3931},[8332],{"type":31,"value":8333},"コマンドを実行する",{"type":25,"tag":2682,"props":8335,"children":8336},{"style":2700},[8337],{"type":31,"value":4090},{"type":25,"tag":2682,"props":8339,"children":8340},{"style":2688},[8341],{"type":31,"value":4076},{"type":25,"tag":2682,"props":8343,"children":8344},{"style":2700},[8345],{"type":31,"value":3587},{"type":25,"tag":2682,"props":8347,"children":8348},{"class":2684,"line":2758},[8349,8354,8359],{"type":25,"tag":2682,"props":8350,"children":8351},{"style":2688},[8352],{"type":31,"value":8353},"    void",{"type":25,"tag":2682,"props":8355,"children":8356},{"style":2852},[8357],{"type":31,"value":8358}," Execute",{"type":25,"tag":2682,"props":8360,"children":8361},{"style":2700},[8362],{"type":31,"value":3041},{"type":25,"tag":2682,"props":8364,"children":8365},{"class":2684,"line":2777},[8366],{"type":25,"tag":2682,"props":8367,"children":8368},{"emptyLinePlaceholder":2752},[8369],{"type":31,"value":2755},{"type":25,"tag":2682,"props":8371,"children":8372},{"class":2684,"line":2785},[8373,8377,8381,8385,8389,8394,8398,8402],{"type":25,"tag":2682,"props":8374,"children":8375},{"style":3931},[8376],{"type":31,"value":4067},{"type":25,"tag":2682,"props":8378,"children":8379},{"style":2700},[8380],{"type":31,"value":2891},{"type":25,"tag":2682,"props":8382,"children":8383},{"style":2688},[8384],{"type":31,"value":4076},{"type":25,"tag":2682,"props":8386,"children":8387},{"style":2700},[8388],{"type":31,"value":3548},{"type":25,"tag":2682,"props":8390,"children":8391},{"style":3931},[8392],{"type":31,"value":8393},"コマンドを取り消す",{"type":25,"tag":2682,"props":8395,"children":8396},{"style":2700},[8397],{"type":31,"value":4090},{"type":25,"tag":2682,"props":8399,"children":8400},{"style":2688},[8401],{"type":31,"value":4076},{"type":25,"tag":2682,"props":8403,"children":8404},{"style":2700},[8405],{"type":31,"value":3587},{"type":25,"tag":2682,"props":8407,"children":8408},{"class":2684,"line":2819},[8409,8413,8418],{"type":25,"tag":2682,"props":8410,"children":8411},{"style":2688},[8412],{"type":31,"value":8353},{"type":25,"tag":2682,"props":8414,"children":8415},{"style":2852},[8416],{"type":31,"value":8417}," Undo",{"type":25,"tag":2682,"props":8419,"children":8420},{"style":2700},[8421],{"type":31,"value":3041},{"type":25,"tag":2682,"props":8423,"children":8424},{"class":2684,"line":2828},[8425],{"type":25,"tag":2682,"props":8426,"children":8427},{"emptyLinePlaceholder":2752},[8428],{"type":31,"value":2755},{"type":25,"tag":2682,"props":8430,"children":8431},{"class":2684,"line":2862},[8432,8436,8440,8444,8448,8453,8457,8461],{"type":25,"tag":2682,"props":8433,"children":8434},{"style":3931},[8435],{"type":31,"value":4067},{"type":25,"tag":2682,"props":8437,"children":8438},{"style":2700},[8439],{"type":31,"value":2891},{"type":25,"tag":2682,"props":8441,"children":8442},{"style":2688},[8443],{"type":31,"value":4076},{"type":25,"tag":2682,"props":8445,"children":8446},{"style":2700},[8447],{"type":31,"value":3548},{"type":25,"tag":2682,"props":8449,"children":8450},{"style":3931},[8451],{"type":31,"value":8452},"コマンドの説明（デバッグ/UI表示用）",{"type":25,"tag":2682,"props":8454,"children":8455},{"style":2700},[8456],{"type":31,"value":4090},{"type":25,"tag":2682,"props":8458,"children":8459},{"style":2688},[8460],{"type":31,"value":4076},{"type":25,"tag":2682,"props":8462,"children":8463},{"style":2700},[8464],{"type":31,"value":3587},{"type":25,"tag":2682,"props":8466,"children":8467},{"class":2684,"line":2870},[8468,8473,8478,8482,8486,8490],{"type":25,"tag":2682,"props":8469,"children":8470},{"style":2688},[8471],{"type":31,"value":8472},"    string",{"type":25,"tag":2682,"props":8474,"children":8475},{"style":2852},[8476],{"type":31,"value":8477}," Description",{"type":25,"tag":2682,"props":8479,"children":8480},{"style":2700},[8481],{"type":31,"value":3698},{"type":25,"tag":2682,"props":8483,"children":8484},{"style":2762},[8485],{"type":31,"value":3703},{"type":25,"tag":2682,"props":8487,"children":8488},{"style":2700},[8489],{"type":31,"value":3708},{"type":25,"tag":2682,"props":8491,"children":8492},{"style":2700},[8493],{"type":31,"value":6147},{"type":25,"tag":2682,"props":8495,"children":8496},{"class":2684,"line":2981},[8497],{"type":25,"tag":2682,"props":8498,"children":8499},{"style":2700},[8500],{"type":31,"value":3219},{"type":25,"tag":33,"props":8502,"children":8503},{},[8504,8506,8512,8514,8520,8522,8527,8529,8535,8537,8543,8545,8550],{"type":31,"value":8505},"ポイントは",{"type":25,"tag":529,"props":8507,"children":8509},{"className":8508},[],[8510],{"type":31,"value":8511},"Description",{"type":31,"value":8513},"を入れていることです。履歴一覧を出したり、デバッグで",{"type":25,"tag":529,"props":8515,"children":8517},{"className":8516},[],[8518],{"type":31,"value":8519},"Console.WriteLine",{"type":31,"value":8521},"する時に、どのコマンドが何をしたのかを文字列で追えると",{"type":25,"tag":103,"props":8523,"children":8524},{},[8525],{"type":31,"value":8526},"デバッグの効率が雲泥の差",{"type":31,"value":8528},"です。.NET標準の",{"type":25,"tag":529,"props":8530,"children":8532},{"className":8531},[],[8533],{"type":31,"value":8534},"System.Windows.Input.ICommand",{"type":31,"value":8536},"とは別物なので、名前が被る場合は名前空間を明示するかリネームしてください。PICOMはBlazor WASMで実装しているため、",{"type":25,"tag":529,"props":8538,"children":8540},{"className":8539},[],[8541],{"type":31,"value":8542},"System.Windows",{"type":31,"value":8544},"の",{"type":25,"tag":529,"props":8546,"children":8548},{"className":8547},[],[8549],{"type":31,"value":8227},{"type":31,"value":8551},"と衝突することはなかったです。",{"type":25,"tag":33,"props":8553,"children":8554},{},[8555],{"type":31,"value":8556},"具体的なコマンドの実装例として、音符追加コマンドを載せておきます。",{"type":25,"tag":524,"props":8558,"children":8560},{"className":2674,"code":8559,"language":2676,"meta":8,"style":8},"public class AddNoteCommand : ICommand\n{\n    private readonly Track _track;\n    private readonly int _position128Note;\n    private readonly SoundComponentBase _component;\n\n    public string Description => $\"ノート追加 at {_position128Note}\";\n\n    public AddNoteCommand(Track track, int position128Note, SoundComponentBase component)\n    {\n        _track = track;\n        _position128Note = position128Note;\n        _component = component;\n    }\n\n    public void Execute() => _track.AddAt(_position128Note, _component);\n    public void Undo() => _track.RemoveAt(_position128Note, _component);\n}\n",[8561],{"type":25,"tag":529,"props":8562,"children":8563},{"__ignoreMap":8},[8564,8588,8595,8621,8645,8670,8677,8727,8734,8789,8796,8816,8836,8856,8863,8870,8927,8983],{"type":25,"tag":2682,"props":8565,"children":8566},{"class":2684,"line":18},[8567,8571,8575,8580,8584],{"type":25,"tag":2682,"props":8568,"children":8569},{"style":2762},[8570],{"type":31,"value":2791},{"type":25,"tag":2682,"props":8572,"children":8573},{"style":2762},[8574],{"type":31,"value":2801},{"type":25,"tag":2682,"props":8576,"children":8577},{"style":2694},[8578],{"type":31,"value":8579}," AddNoteCommand",{"type":25,"tag":2682,"props":8581,"children":8582},{"style":2700},[8583],{"type":31,"value":2811},{"type":25,"tag":2682,"props":8585,"children":8586},{"style":2694},[8587],{"type":31,"value":8302},{"type":25,"tag":2682,"props":8589,"children":8590},{"class":2684,"line":1149},[8591],{"type":25,"tag":2682,"props":8592,"children":8593},{"style":2700},[8594],{"type":31,"value":2825},{"type":25,"tag":2682,"props":8596,"children":8597},{"class":2684,"line":1159},[8598,8602,8607,8612,8617],{"type":25,"tag":2682,"props":8599,"children":8600},{"style":2762},[8601],{"type":31,"value":3277},{"type":25,"tag":2682,"props":8603,"children":8604},{"style":2762},[8605],{"type":31,"value":8606}," readonly",{"type":25,"tag":2682,"props":8608,"children":8609},{"style":2694},[8610],{"type":31,"value":8611}," Track",{"type":25,"tag":2682,"props":8613,"children":8614},{"style":2852},[8615],{"type":31,"value":8616}," _track",{"type":25,"tag":2682,"props":8618,"children":8619},{"style":2700},[8620],{"type":31,"value":2713},{"type":25,"tag":2682,"props":8622,"children":8623},{"class":2684,"line":2758},[8624,8628,8632,8636,8641],{"type":25,"tag":2682,"props":8625,"children":8626},{"style":2762},[8627],{"type":31,"value":3277},{"type":25,"tag":2682,"props":8629,"children":8630},{"style":2762},[8631],{"type":31,"value":8606},{"type":25,"tag":2682,"props":8633,"children":8634},{"style":2688},[8635],{"type":31,"value":5629},{"type":25,"tag":2682,"props":8637,"children":8638},{"style":2852},[8639],{"type":31,"value":8640}," _position128Note",{"type":25,"tag":2682,"props":8642,"children":8643},{"style":2700},[8644],{"type":31,"value":2713},{"type":25,"tag":2682,"props":8646,"children":8647},{"class":2684,"line":2777},[8648,8652,8656,8661,8666],{"type":25,"tag":2682,"props":8649,"children":8650},{"style":2762},[8651],{"type":31,"value":3277},{"type":25,"tag":2682,"props":8653,"children":8654},{"style":2762},[8655],{"type":31,"value":8606},{"type":25,"tag":2682,"props":8657,"children":8658},{"style":2694},[8659],{"type":31,"value":8660}," SoundComponentBase",{"type":25,"tag":2682,"props":8662,"children":8663},{"style":2852},[8664],{"type":31,"value":8665}," _component",{"type":25,"tag":2682,"props":8667,"children":8668},{"style":2700},[8669],{"type":31,"value":2713},{"type":25,"tag":2682,"props":8671,"children":8672},{"class":2684,"line":2785},[8673],{"type":25,"tag":2682,"props":8674,"children":8675},{"emptyLinePlaceholder":2752},[8676],{"type":31,"value":2755},{"type":25,"tag":2682,"props":8678,"children":8679},{"class":2684,"line":2819},[8680,8684,8688,8692,8696,8701,8706,8710,8715,8719,8723],{"type":25,"tag":2682,"props":8681,"children":8682},{"style":2762},[8683],{"type":31,"value":2834},{"type":25,"tag":2682,"props":8685,"children":8686},{"style":2688},[8687],{"type":31,"value":2954},{"type":25,"tag":2682,"props":8689,"children":8690},{"style":2852},[8691],{"type":31,"value":8477},{"type":25,"tag":2682,"props":8693,"children":8694},{"style":2762},[8695],{"type":31,"value":3336},{"type":25,"tag":2682,"props":8697,"children":8698},{"style":3293},[8699],{"type":31,"value":8700}," $\"",{"type":25,"tag":2682,"props":8702,"children":8703},{"style":4759},[8704],{"type":31,"value":8705},"ノート追加 at ",{"type":25,"tag":2682,"props":8707,"children":8708},{"style":2700},[8709],{"type":31,"value":6864},{"type":25,"tag":2682,"props":8711,"children":8712},{"style":4759},[8713],{"type":31,"value":8714},"_position128Note",{"type":25,"tag":2682,"props":8716,"children":8717},{"style":2700},[8718],{"type":31,"value":6882},{"type":25,"tag":2682,"props":8720,"children":8721},{"style":3293},[8722],{"type":31,"value":4766},{"type":25,"tag":2682,"props":8724,"children":8725},{"style":2700},[8726],{"type":31,"value":2713},{"type":25,"tag":2682,"props":8728,"children":8729},{"class":2684,"line":2828},[8730],{"type":25,"tag":2682,"props":8731,"children":8732},{"emptyLinePlaceholder":2752},[8733],{"type":31,"value":2755},{"type":25,"tag":2682,"props":8735,"children":8736},{"class":2684,"line":2862},[8737,8741,8745,8749,8754,8759,8763,8767,8772,8776,8780,8785],{"type":25,"tag":2682,"props":8738,"children":8739},{"style":2762},[8740],{"type":31,"value":2834},{"type":25,"tag":2682,"props":8742,"children":8743},{"style":2852},[8744],{"type":31,"value":8579},{"type":25,"tag":2682,"props":8746,"children":8747},{"style":2700},[8748],{"type":31,"value":3076},{"type":25,"tag":2682,"props":8750,"children":8751},{"style":2694},[8752],{"type":31,"value":8753},"Track",{"type":25,"tag":2682,"props":8755,"children":8756},{"style":2852},[8757],{"type":31,"value":8758}," track",{"type":25,"tag":2682,"props":8760,"children":8761},{"style":2700},[8762],{"type":31,"value":2921},{"type":25,"tag":2682,"props":8764,"children":8765},{"style":2688},[8766],{"type":31,"value":5629},{"type":25,"tag":2682,"props":8768,"children":8769},{"style":2852},[8770],{"type":31,"value":8771}," position128Note",{"type":25,"tag":2682,"props":8773,"children":8774},{"style":2700},[8775],{"type":31,"value":2921},{"type":25,"tag":2682,"props":8777,"children":8778},{"style":2694},[8779],{"type":31,"value":8660},{"type":25,"tag":2682,"props":8781,"children":8782},{"style":2852},[8783],{"type":31,"value":8784}," component",{"type":25,"tag":2682,"props":8786,"children":8787},{"style":2700},[8788],{"type":31,"value":2978},{"type":25,"tag":2682,"props":8790,"children":8791},{"class":2684,"line":2870},[8792],{"type":25,"tag":2682,"props":8793,"children":8794},{"style":2700},[8795],{"type":31,"value":2987},{"type":25,"tag":2682,"props":8797,"children":8798},{"class":2684,"line":2981},[8799,8804,8808,8812],{"type":25,"tag":2682,"props":8800,"children":8801},{"style":3004},[8802],{"type":31,"value":8803},"        _track",{"type":25,"tag":2682,"props":8805,"children":8806},{"style":2700},[8807],{"type":31,"value":2968},{"type":25,"tag":2682,"props":8809,"children":8810},{"style":3004},[8811],{"type":31,"value":8758},{"type":25,"tag":2682,"props":8813,"children":8814},{"style":2700},[8815],{"type":31,"value":2713},{"type":25,"tag":2682,"props":8817,"children":8818},{"class":2684,"line":2990},[8819,8824,8828,8832],{"type":25,"tag":2682,"props":8820,"children":8821},{"style":3004},[8822],{"type":31,"value":8823},"        _position128Note",{"type":25,"tag":2682,"props":8825,"children":8826},{"style":2700},[8827],{"type":31,"value":2968},{"type":25,"tag":2682,"props":8829,"children":8830},{"style":3004},[8831],{"type":31,"value":8771},{"type":25,"tag":2682,"props":8833,"children":8834},{"style":2700},[8835],{"type":31,"value":2713},{"type":25,"tag":2682,"props":8837,"children":8838},{"class":2684,"line":3044},[8839,8844,8848,8852],{"type":25,"tag":2682,"props":8840,"children":8841},{"style":3004},[8842],{"type":31,"value":8843},"        _component",{"type":25,"tag":2682,"props":8845,"children":8846},{"style":2700},[8847],{"type":31,"value":2968},{"type":25,"tag":2682,"props":8849,"children":8850},{"style":3004},[8851],{"type":31,"value":8784},{"type":25,"tag":2682,"props":8853,"children":8854},{"style":2700},[8855],{"type":31,"value":2713},{"type":25,"tag":2682,"props":8857,"children":8858},{"class":2684,"line":3065},[8859],{"type":25,"tag":2682,"props":8860,"children":8861},{"style":2700},[8862],{"type":31,"value":3094},{"type":25,"tag":2682,"props":8864,"children":8865},{"class":2684,"line":3088},[8866],{"type":25,"tag":2682,"props":8867,"children":8868},{"emptyLinePlaceholder":2752},[8869],{"type":31,"value":2755},{"type":25,"tag":2682,"props":8871,"children":8872},{"class":2684,"line":3097},[8873,8877,8881,8885,8890,8894,8898,8902,8907,8911,8915,8919,8923],{"type":25,"tag":2682,"props":8874,"children":8875},{"style":2762},[8876],{"type":31,"value":2834},{"type":25,"tag":2682,"props":8878,"children":8879},{"style":2688},[8880],{"type":31,"value":2881},{"type":25,"tag":2682,"props":8882,"children":8883},{"style":2852},[8884],{"type":31,"value":8358},{"type":25,"tag":2682,"props":8886,"children":8887},{"style":2700},[8888],{"type":31,"value":8889},"()",{"type":25,"tag":2682,"props":8891,"children":8892},{"style":2762},[8893],{"type":31,"value":3336},{"type":25,"tag":2682,"props":8895,"children":8896},{"style":3004},[8897],{"type":31,"value":8616},{"type":25,"tag":2682,"props":8899,"children":8900},{"style":2700},[8901],{"type":31,"value":2703},{"type":25,"tag":2682,"props":8903,"children":8904},{"style":2852},[8905],{"type":31,"value":8906},"AddAt",{"type":25,"tag":2682,"props":8908,"children":8909},{"style":2700},[8910],{"type":31,"value":3076},{"type":25,"tag":2682,"props":8912,"children":8913},{"style":3004},[8914],{"type":31,"value":8714},{"type":25,"tag":2682,"props":8916,"children":8917},{"style":2700},[8918],{"type":31,"value":2921},{"type":25,"tag":2682,"props":8920,"children":8921},{"style":3004},[8922],{"type":31,"value":8665},{"type":25,"tag":2682,"props":8924,"children":8925},{"style":2700},[8926],{"type":31,"value":3085},{"type":25,"tag":2682,"props":8928,"children":8929},{"class":2684,"line":3105},[8930,8934,8938,8942,8946,8950,8954,8958,8963,8967,8971,8975,8979],{"type":25,"tag":2682,"props":8931,"children":8932},{"style":2762},[8933],{"type":31,"value":2834},{"type":25,"tag":2682,"props":8935,"children":8936},{"style":2688},[8937],{"type":31,"value":2881},{"type":25,"tag":2682,"props":8939,"children":8940},{"style":2852},[8941],{"type":31,"value":8417},{"type":25,"tag":2682,"props":8943,"children":8944},{"style":2700},[8945],{"type":31,"value":8889},{"type":25,"tag":2682,"props":8947,"children":8948},{"style":2762},[8949],{"type":31,"value":3336},{"type":25,"tag":2682,"props":8951,"children":8952},{"style":3004},[8953],{"type":31,"value":8616},{"type":25,"tag":2682,"props":8955,"children":8956},{"style":2700},[8957],{"type":31,"value":2703},{"type":25,"tag":2682,"props":8959,"children":8960},{"style":2852},[8961],{"type":31,"value":8962},"RemoveAt",{"type":25,"tag":2682,"props":8964,"children":8965},{"style":2700},[8966],{"type":31,"value":3076},{"type":25,"tag":2682,"props":8968,"children":8969},{"style":3004},[8970],{"type":31,"value":8714},{"type":25,"tag":2682,"props":8972,"children":8973},{"style":2700},[8974],{"type":31,"value":2921},{"type":25,"tag":2682,"props":8976,"children":8977},{"style":3004},[8978],{"type":31,"value":8665},{"type":25,"tag":2682,"props":8980,"children":8981},{"style":2700},[8982],{"type":31,"value":3085},{"type":25,"tag":2682,"props":8984,"children":8985},{"class":2684,"line":3139},[8986],{"type":25,"tag":2682,"props":8987,"children":8988},{"style":2700},[8989],{"type":31,"value":3219},{"type":25,"tag":33,"props":8991,"children":8992},{},[8993,8999,9000,9006,9008,9013],{"type":25,"tag":529,"props":8994,"children":8996},{"className":8995},[],[8997],{"type":31,"value":8998},"Execute",{"type":31,"value":1086},{"type":25,"tag":529,"props":9001,"children":9003},{"className":9002},[],[9004],{"type":31,"value":9005},"Undo",{"type":31,"value":9007},"が対称になっているのがCommandパターンの気持ちよさです。",{"type":25,"tag":529,"props":9009,"children":9011},{"className":9010},[],[9012],{"type":31,"value":9005},{"type":31,"value":9014},"で失敗するような操作は原則として作らない、というのが運用上の鉄則です。",{"type":25,"tag":26,"props":9016,"children":9018},{"id":9017},"undoredomanager2本のstackで履歴を管理",[9019],{"type":31,"value":9020},"UndoRedoManager：2本のStackで履歴を管理",{"type":25,"tag":33,"props":9022,"children":9023},{},[9024,9026,9031,9032,9038,9039,9045],{"type":31,"value":9025},"履歴の管理本体は",{"type":25,"tag":529,"props":9027,"children":9029},{"className":9028},[],[9030],{"type":31,"value":8235},{"type":31,"value":2515},{"type":25,"tag":529,"props":9033,"children":9035},{"className":9034},[],[9036],{"type":31,"value":9037},"_undoStack",{"type":31,"value":1086},{"type":25,"tag":529,"props":9040,"children":9042},{"className":9041},[],[9043],{"type":31,"value":9044},"_redoStack",{"type":31,"value":9046},"の2本で押したり引いたりします。",{"type":25,"tag":524,"props":9048,"children":9050},{"className":2674,"code":9049,"language":2676,"meta":8,"style":8},"public class UndoRedoManager\n{\n    private readonly Stack\u003CICommand> _undoStack = new();\n    private readonly Stack\u003CICommand> _redoStack = new();\n    private readonly int _maxHistorySize;\n\n    public event Action? StateChanged;\n\n    public UndoRedoManager(int maxHistorySize = 100)\n    {\n        _maxHistorySize = maxHistorySize;\n    }\n\n    public bool CanUndo => _undoStack.Count > 0;\n    public bool CanRedo => _redoStack.Count > 0;\n\n    public void Execute(ICommand command)\n    {\n        command.Execute();\n        _undoStack.Push(command);\n        _redoStack.Clear();\n        TrimHistory();\n        StateChanged?.Invoke();\n    }\n\n    public void Undo()\n    {\n        if (!CanUndo) return;\n        var command = _undoStack.Pop();\n        command.Undo();\n        _redoStack.Push(command);\n        StateChanged?.Invoke();\n    }\n\n    public void Redo()\n    {\n        if (!CanRedo) return;\n        var command = _redoStack.Pop();\n        command.Execute();\n        _undoStack.Push(command);\n        StateChanged?.Invoke();\n    }\n}\n",[9051],{"type":25,"tag":529,"props":9052,"children":9053},{"__ignoreMap":8},[9054,9070,9077,9122,9166,9190,9197,9226,9233,9272,9279,9299,9306,9313,9360,9404,9411,9443,9450,9470,9500,9521,9534,9559,9567,9575,9596,9604,9638,9672,9692,9720,9744,9752,9760,9781,9789,9822,9854,9874,9902,9926,9934],{"type":25,"tag":2682,"props":9055,"children":9056},{"class":2684,"line":18},[9057,9061,9065],{"type":25,"tag":2682,"props":9058,"children":9059},{"style":2762},[9060],{"type":31,"value":2791},{"type":25,"tag":2682,"props":9062,"children":9063},{"style":2762},[9064],{"type":31,"value":2801},{"type":25,"tag":2682,"props":9066,"children":9067},{"style":2694},[9068],{"type":31,"value":9069}," UndoRedoManager\n",{"type":25,"tag":2682,"props":9071,"children":9072},{"class":2684,"line":1149},[9073],{"type":25,"tag":2682,"props":9074,"children":9075},{"style":2700},[9076],{"type":31,"value":2825},{"type":25,"tag":2682,"props":9078,"children":9079},{"class":2684,"line":1159},[9080,9084,9088,9093,9097,9101,9105,9110,9114,9118],{"type":25,"tag":2682,"props":9081,"children":9082},{"style":2762},[9083],{"type":31,"value":3277},{"type":25,"tag":2682,"props":9085,"children":9086},{"style":2762},[9087],{"type":31,"value":8606},{"type":25,"tag":2682,"props":9089,"children":9090},{"style":2694},[9091],{"type":31,"value":9092}," Stack",{"type":25,"tag":2682,"props":9094,"children":9095},{"style":2700},[9096],{"type":31,"value":2891},{"type":25,"tag":2682,"props":9098,"children":9099},{"style":2694},[9100],{"type":31,"value":8227},{"type":25,"tag":2682,"props":9102,"children":9103},{"style":2700},[9104],{"type":31,"value":3548},{"type":25,"tag":2682,"props":9106,"children":9107},{"style":2852},[9108],{"type":31,"value":9109}," _undoStack",{"type":25,"tag":2682,"props":9111,"children":9112},{"style":2700},[9113],{"type":31,"value":2968},{"type":25,"tag":2682,"props":9115,"children":9116},{"style":2762},[9117],{"type":31,"value":3031},{"type":25,"tag":2682,"props":9119,"children":9120},{"style":2700},[9121],{"type":31,"value":3041},{"type":25,"tag":2682,"props":9123,"children":9124},{"class":2684,"line":2758},[9125,9129,9133,9137,9141,9145,9149,9154,9158,9162],{"type":25,"tag":2682,"props":9126,"children":9127},{"style":2762},[9128],{"type":31,"value":3277},{"type":25,"tag":2682,"props":9130,"children":9131},{"style":2762},[9132],{"type":31,"value":8606},{"type":25,"tag":2682,"props":9134,"children":9135},{"style":2694},[9136],{"type":31,"value":9092},{"type":25,"tag":2682,"props":9138,"children":9139},{"style":2700},[9140],{"type":31,"value":2891},{"type":25,"tag":2682,"props":9142,"children":9143},{"style":2694},[9144],{"type":31,"value":8227},{"type":25,"tag":2682,"props":9146,"children":9147},{"style":2700},[9148],{"type":31,"value":3548},{"type":25,"tag":2682,"props":9150,"children":9151},{"style":2852},[9152],{"type":31,"value":9153}," _redoStack",{"type":25,"tag":2682,"props":9155,"children":9156},{"style":2700},[9157],{"type":31,"value":2968},{"type":25,"tag":2682,"props":9159,"children":9160},{"style":2762},[9161],{"type":31,"value":3031},{"type":25,"tag":2682,"props":9163,"children":9164},{"style":2700},[9165],{"type":31,"value":3041},{"type":25,"tag":2682,"props":9167,"children":9168},{"class":2684,"line":2777},[9169,9173,9177,9181,9186],{"type":25,"tag":2682,"props":9170,"children":9171},{"style":2762},[9172],{"type":31,"value":3277},{"type":25,"tag":2682,"props":9174,"children":9175},{"style":2762},[9176],{"type":31,"value":8606},{"type":25,"tag":2682,"props":9178,"children":9179},{"style":2688},[9180],{"type":31,"value":5629},{"type":25,"tag":2682,"props":9182,"children":9183},{"style":2852},[9184],{"type":31,"value":9185}," _maxHistorySize",{"type":25,"tag":2682,"props":9187,"children":9188},{"style":2700},[9189],{"type":31,"value":2713},{"type":25,"tag":2682,"props":9191,"children":9192},{"class":2684,"line":2785},[9193],{"type":25,"tag":2682,"props":9194,"children":9195},{"emptyLinePlaceholder":2752},[9196],{"type":31,"value":2755},{"type":25,"tag":2682,"props":9198,"children":9199},{"class":2684,"line":2819},[9200,9204,9208,9213,9217,9222],{"type":25,"tag":2682,"props":9201,"children":9202},{"style":2762},[9203],{"type":31,"value":2834},{"type":25,"tag":2682,"props":9205,"children":9206},{"style":2762},[9207],{"type":31,"value":2839},{"type":25,"tag":2682,"props":9209,"children":9210},{"style":2694},[9211],{"type":31,"value":9212}," Action",{"type":25,"tag":2682,"props":9214,"children":9215},{"style":2700},[9216],{"type":31,"value":2849},{"type":25,"tag":2682,"props":9218,"children":9219},{"style":2852},[9220],{"type":31,"value":9221}," StateChanged",{"type":25,"tag":2682,"props":9223,"children":9224},{"style":2700},[9225],{"type":31,"value":2713},{"type":25,"tag":2682,"props":9227,"children":9228},{"class":2684,"line":2828},[9229],{"type":25,"tag":2682,"props":9230,"children":9231},{"emptyLinePlaceholder":2752},[9232],{"type":31,"value":2755},{"type":25,"tag":2682,"props":9234,"children":9235},{"class":2684,"line":2862},[9236,9240,9245,9249,9254,9259,9263,9268],{"type":25,"tag":2682,"props":9237,"children":9238},{"style":2762},[9239],{"type":31,"value":2834},{"type":25,"tag":2682,"props":9241,"children":9242},{"style":2852},[9243],{"type":31,"value":9244}," UndoRedoManager",{"type":25,"tag":2682,"props":9246,"children":9247},{"style":2700},[9248],{"type":31,"value":3076},{"type":25,"tag":2682,"props":9250,"children":9251},{"style":2688},[9252],{"type":31,"value":9253},"int",{"type":25,"tag":2682,"props":9255,"children":9256},{"style":2852},[9257],{"type":31,"value":9258}," maxHistorySize",{"type":25,"tag":2682,"props":9260,"children":9261},{"style":2700},[9262],{"type":31,"value":2968},{"type":25,"tag":2682,"props":9264,"children":9265},{"style":5609},[9266],{"type":31,"value":9267}," 100",{"type":25,"tag":2682,"props":9269,"children":9270},{"style":2700},[9271],{"type":31,"value":2978},{"type":25,"tag":2682,"props":9273,"children":9274},{"class":2684,"line":2870},[9275],{"type":25,"tag":2682,"props":9276,"children":9277},{"style":2700},[9278],{"type":31,"value":2987},{"type":25,"tag":2682,"props":9280,"children":9281},{"class":2684,"line":2981},[9282,9287,9291,9295],{"type":25,"tag":2682,"props":9283,"children":9284},{"style":3004},[9285],{"type":31,"value":9286},"        _maxHistorySize",{"type":25,"tag":2682,"props":9288,"children":9289},{"style":2700},[9290],{"type":31,"value":2968},{"type":25,"tag":2682,"props":9292,"children":9293},{"style":3004},[9294],{"type":31,"value":9258},{"type":25,"tag":2682,"props":9296,"children":9297},{"style":2700},[9298],{"type":31,"value":2713},{"type":25,"tag":2682,"props":9300,"children":9301},{"class":2684,"line":2990},[9302],{"type":25,"tag":2682,"props":9303,"children":9304},{"style":2700},[9305],{"type":31,"value":3094},{"type":25,"tag":2682,"props":9307,"children":9308},{"class":2684,"line":3044},[9309],{"type":25,"tag":2682,"props":9310,"children":9311},{"emptyLinePlaceholder":2752},[9312],{"type":31,"value":2755},{"type":25,"tag":2682,"props":9314,"children":9315},{"class":2684,"line":3065},[9316,9320,9324,9329,9333,9337,9341,9346,9351,9356],{"type":25,"tag":2682,"props":9317,"children":9318},{"style":2762},[9319],{"type":31,"value":2834},{"type":25,"tag":2682,"props":9321,"children":9322},{"style":2688},[9323],{"type":31,"value":3637},{"type":25,"tag":2682,"props":9325,"children":9326},{"style":2852},[9327],{"type":31,"value":9328}," CanUndo",{"type":25,"tag":2682,"props":9330,"children":9331},{"style":2762},[9332],{"type":31,"value":3336},{"type":25,"tag":2682,"props":9334,"children":9335},{"style":3004},[9336],{"type":31,"value":9109},{"type":25,"tag":2682,"props":9338,"children":9339},{"style":2700},[9340],{"type":31,"value":2703},{"type":25,"tag":2682,"props":9342,"children":9343},{"style":3004},[9344],{"type":31,"value":9345},"Count",{"type":25,"tag":2682,"props":9347,"children":9348},{"style":2700},[9349],{"type":31,"value":9350}," >",{"type":25,"tag":2682,"props":9352,"children":9353},{"style":5609},[9354],{"type":31,"value":9355}," 0",{"type":25,"tag":2682,"props":9357,"children":9358},{"style":2700},[9359],{"type":31,"value":2713},{"type":25,"tag":2682,"props":9361,"children":9362},{"class":2684,"line":3088},[9363,9367,9371,9376,9380,9384,9388,9392,9396,9400],{"type":25,"tag":2682,"props":9364,"children":9365},{"style":2762},[9366],{"type":31,"value":2834},{"type":25,"tag":2682,"props":9368,"children":9369},{"style":2688},[9370],{"type":31,"value":3637},{"type":25,"tag":2682,"props":9372,"children":9373},{"style":2852},[9374],{"type":31,"value":9375}," CanRedo",{"type":25,"tag":2682,"props":9377,"children":9378},{"style":2762},[9379],{"type":31,"value":3336},{"type":25,"tag":2682,"props":9381,"children":9382},{"style":3004},[9383],{"type":31,"value":9153},{"type":25,"tag":2682,"props":9385,"children":9386},{"style":2700},[9387],{"type":31,"value":2703},{"type":25,"tag":2682,"props":9389,"children":9390},{"style":3004},[9391],{"type":31,"value":9345},{"type":25,"tag":2682,"props":9393,"children":9394},{"style":2700},[9395],{"type":31,"value":9350},{"type":25,"tag":2682,"props":9397,"children":9398},{"style":5609},[9399],{"type":31,"value":9355},{"type":25,"tag":2682,"props":9401,"children":9402},{"style":2700},[9403],{"type":31,"value":2713},{"type":25,"tag":2682,"props":9405,"children":9406},{"class":2684,"line":3097},[9407],{"type":25,"tag":2682,"props":9408,"children":9409},{"emptyLinePlaceholder":2752},[9410],{"type":31,"value":2755},{"type":25,"tag":2682,"props":9412,"children":9413},{"class":2684,"line":3105},[9414,9418,9422,9426,9430,9434,9439],{"type":25,"tag":2682,"props":9415,"children":9416},{"style":2762},[9417],{"type":31,"value":2834},{"type":25,"tag":2682,"props":9419,"children":9420},{"style":2688},[9421],{"type":31,"value":2881},{"type":25,"tag":2682,"props":9423,"children":9424},{"style":2852},[9425],{"type":31,"value":8358},{"type":25,"tag":2682,"props":9427,"children":9428},{"style":2700},[9429],{"type":31,"value":3076},{"type":25,"tag":2682,"props":9431,"children":9432},{"style":2694},[9433],{"type":31,"value":8227},{"type":25,"tag":2682,"props":9435,"children":9436},{"style":2852},[9437],{"type":31,"value":9438}," command",{"type":25,"tag":2682,"props":9440,"children":9441},{"style":2700},[9442],{"type":31,"value":2978},{"type":25,"tag":2682,"props":9444,"children":9445},{"class":2684,"line":3139},[9446],{"type":25,"tag":2682,"props":9447,"children":9448},{"style":2700},[9449],{"type":31,"value":2987},{"type":25,"tag":2682,"props":9451,"children":9452},{"class":2684,"line":3147},[9453,9458,9462,9466],{"type":25,"tag":2682,"props":9454,"children":9455},{"style":3004},[9456],{"type":31,"value":9457},"        command",{"type":25,"tag":2682,"props":9459,"children":9460},{"style":2700},[9461],{"type":31,"value":2703},{"type":25,"tag":2682,"props":9463,"children":9464},{"style":2852},[9465],{"type":31,"value":8998},{"type":25,"tag":2682,"props":9467,"children":9468},{"style":2700},[9469],{"type":31,"value":3041},{"type":25,"tag":2682,"props":9471,"children":9472},{"class":2684,"line":3205},[9473,9478,9482,9487,9491,9496],{"type":25,"tag":2682,"props":9474,"children":9475},{"style":3004},[9476],{"type":31,"value":9477},"        _undoStack",{"type":25,"tag":2682,"props":9479,"children":9480},{"style":2700},[9481],{"type":31,"value":2703},{"type":25,"tag":2682,"props":9483,"children":9484},{"style":2852},[9485],{"type":31,"value":9486},"Push",{"type":25,"tag":2682,"props":9488,"children":9489},{"style":2700},[9490],{"type":31,"value":3076},{"type":25,"tag":2682,"props":9492,"children":9493},{"style":3004},[9494],{"type":31,"value":9495},"command",{"type":25,"tag":2682,"props":9497,"children":9498},{"style":2700},[9499],{"type":31,"value":3085},{"type":25,"tag":2682,"props":9501,"children":9502},{"class":2684,"line":3213},[9503,9508,9512,9517],{"type":25,"tag":2682,"props":9504,"children":9505},{"style":3004},[9506],{"type":31,"value":9507},"        _redoStack",{"type":25,"tag":2682,"props":9509,"children":9510},{"style":2700},[9511],{"type":31,"value":2703},{"type":25,"tag":2682,"props":9513,"children":9514},{"style":2852},[9515],{"type":31,"value":9516},"Clear",{"type":25,"tag":2682,"props":9518,"children":9519},{"style":2700},[9520],{"type":31,"value":3041},{"type":25,"tag":2682,"props":9522,"children":9524},{"class":2684,"line":9523},22,[9525,9530],{"type":25,"tag":2682,"props":9526,"children":9527},{"style":2852},[9528],{"type":31,"value":9529},"        TrimHistory",{"type":25,"tag":2682,"props":9531,"children":9532},{"style":2700},[9533],{"type":31,"value":3041},{"type":25,"tag":2682,"props":9535,"children":9537},{"class":2684,"line":9536},23,[9538,9543,9547,9551,9555],{"type":25,"tag":2682,"props":9539,"children":9540},{"style":3004},[9541],{"type":31,"value":9542},"        StateChanged",{"type":25,"tag":2682,"props":9544,"children":9545},{"style":2762},[9546],{"type":31,"value":2849},{"type":25,"tag":2682,"props":9548,"children":9549},{"style":2700},[9550],{"type":31,"value":2703},{"type":25,"tag":2682,"props":9552,"children":9553},{"style":2852},[9554],{"type":31,"value":3166},{"type":25,"tag":2682,"props":9556,"children":9557},{"style":2700},[9558],{"type":31,"value":3041},{"type":25,"tag":2682,"props":9560,"children":9562},{"class":2684,"line":9561},24,[9563],{"type":25,"tag":2682,"props":9564,"children":9565},{"style":2700},[9566],{"type":31,"value":3094},{"type":25,"tag":2682,"props":9568,"children":9570},{"class":2684,"line":9569},25,[9571],{"type":25,"tag":2682,"props":9572,"children":9573},{"emptyLinePlaceholder":2752},[9574],{"type":31,"value":2755},{"type":25,"tag":2682,"props":9576,"children":9578},{"class":2684,"line":9577},26,[9579,9583,9587,9591],{"type":25,"tag":2682,"props":9580,"children":9581},{"style":2762},[9582],{"type":31,"value":2834},{"type":25,"tag":2682,"props":9584,"children":9585},{"style":2688},[9586],{"type":31,"value":2881},{"type":25,"tag":2682,"props":9588,"children":9589},{"style":2852},[9590],{"type":31,"value":8417},{"type":25,"tag":2682,"props":9592,"children":9593},{"style":2700},[9594],{"type":31,"value":9595},"()\n",{"type":25,"tag":2682,"props":9597,"children":9599},{"class":2684,"line":9598},27,[9600],{"type":25,"tag":2682,"props":9601,"children":9602},{"style":2700},[9603],{"type":31,"value":2987},{"type":25,"tag":2682,"props":9605,"children":9607},{"class":2684,"line":9606},28,[9608,9612,9616,9620,9625,9629,9634],{"type":25,"tag":2682,"props":9609,"children":9610},{"style":2688},[9611],{"type":31,"value":2996},{"type":25,"tag":2682,"props":9613,"children":9614},{"style":2700},[9615],{"type":31,"value":3001},{"type":25,"tag":2682,"props":9617,"children":9618},{"style":2762},[9619],{"type":31,"value":4236},{"type":25,"tag":2682,"props":9621,"children":9622},{"style":3004},[9623],{"type":31,"value":9624},"CanUndo",{"type":25,"tag":2682,"props":9626,"children":9627},{"style":2700},[9628],{"type":31,"value":3021},{"type":25,"tag":2682,"props":9630,"children":9631},{"style":2688},[9632],{"type":31,"value":9633}," return",{"type":25,"tag":2682,"props":9635,"children":9636},{"style":2700},[9637],{"type":31,"value":2713},{"type":25,"tag":2682,"props":9639,"children":9641},{"class":2684,"line":9640},29,[9642,9647,9651,9655,9659,9663,9668],{"type":25,"tag":2682,"props":9643,"children":9644},{"style":2762},[9645],{"type":31,"value":9646},"        var",{"type":25,"tag":2682,"props":9648,"children":9649},{"style":2852},[9650],{"type":31,"value":9438},{"type":25,"tag":2682,"props":9652,"children":9653},{"style":2700},[9654],{"type":31,"value":2968},{"type":25,"tag":2682,"props":9656,"children":9657},{"style":3004},[9658],{"type":31,"value":9109},{"type":25,"tag":2682,"props":9660,"children":9661},{"style":2700},[9662],{"type":31,"value":2703},{"type":25,"tag":2682,"props":9664,"children":9665},{"style":2852},[9666],{"type":31,"value":9667},"Pop",{"type":25,"tag":2682,"props":9669,"children":9670},{"style":2700},[9671],{"type":31,"value":3041},{"type":25,"tag":2682,"props":9673,"children":9675},{"class":2684,"line":9674},30,[9676,9680,9684,9688],{"type":25,"tag":2682,"props":9677,"children":9678},{"style":3004},[9679],{"type":31,"value":9457},{"type":25,"tag":2682,"props":9681,"children":9682},{"style":2700},[9683],{"type":31,"value":2703},{"type":25,"tag":2682,"props":9685,"children":9686},{"style":2852},[9687],{"type":31,"value":9005},{"type":25,"tag":2682,"props":9689,"children":9690},{"style":2700},[9691],{"type":31,"value":3041},{"type":25,"tag":2682,"props":9693,"children":9695},{"class":2684,"line":9694},31,[9696,9700,9704,9708,9712,9716],{"type":25,"tag":2682,"props":9697,"children":9698},{"style":3004},[9699],{"type":31,"value":9507},{"type":25,"tag":2682,"props":9701,"children":9702},{"style":2700},[9703],{"type":31,"value":2703},{"type":25,"tag":2682,"props":9705,"children":9706},{"style":2852},[9707],{"type":31,"value":9486},{"type":25,"tag":2682,"props":9709,"children":9710},{"style":2700},[9711],{"type":31,"value":3076},{"type":25,"tag":2682,"props":9713,"children":9714},{"style":3004},[9715],{"type":31,"value":9495},{"type":25,"tag":2682,"props":9717,"children":9718},{"style":2700},[9719],{"type":31,"value":3085},{"type":25,"tag":2682,"props":9721,"children":9723},{"class":2684,"line":9722},32,[9724,9728,9732,9736,9740],{"type":25,"tag":2682,"props":9725,"children":9726},{"style":3004},[9727],{"type":31,"value":9542},{"type":25,"tag":2682,"props":9729,"children":9730},{"style":2762},[9731],{"type":31,"value":2849},{"type":25,"tag":2682,"props":9733,"children":9734},{"style":2700},[9735],{"type":31,"value":2703},{"type":25,"tag":2682,"props":9737,"children":9738},{"style":2852},[9739],{"type":31,"value":3166},{"type":25,"tag":2682,"props":9741,"children":9742},{"style":2700},[9743],{"type":31,"value":3041},{"type":25,"tag":2682,"props":9745,"children":9747},{"class":2684,"line":9746},33,[9748],{"type":25,"tag":2682,"props":9749,"children":9750},{"style":2700},[9751],{"type":31,"value":3094},{"type":25,"tag":2682,"props":9753,"children":9755},{"class":2684,"line":9754},34,[9756],{"type":25,"tag":2682,"props":9757,"children":9758},{"emptyLinePlaceholder":2752},[9759],{"type":31,"value":2755},{"type":25,"tag":2682,"props":9761,"children":9763},{"class":2684,"line":9762},35,[9764,9768,9772,9777],{"type":25,"tag":2682,"props":9765,"children":9766},{"style":2762},[9767],{"type":31,"value":2834},{"type":25,"tag":2682,"props":9769,"children":9770},{"style":2688},[9771],{"type":31,"value":2881},{"type":25,"tag":2682,"props":9773,"children":9774},{"style":2852},[9775],{"type":31,"value":9776}," Redo",{"type":25,"tag":2682,"props":9778,"children":9779},{"style":2700},[9780],{"type":31,"value":9595},{"type":25,"tag":2682,"props":9782,"children":9784},{"class":2684,"line":9783},36,[9785],{"type":25,"tag":2682,"props":9786,"children":9787},{"style":2700},[9788],{"type":31,"value":2987},{"type":25,"tag":2682,"props":9790,"children":9792},{"class":2684,"line":9791},37,[9793,9797,9801,9805,9810,9814,9818],{"type":25,"tag":2682,"props":9794,"children":9795},{"style":2688},[9796],{"type":31,"value":2996},{"type":25,"tag":2682,"props":9798,"children":9799},{"style":2700},[9800],{"type":31,"value":3001},{"type":25,"tag":2682,"props":9802,"children":9803},{"style":2762},[9804],{"type":31,"value":4236},{"type":25,"tag":2682,"props":9806,"children":9807},{"style":3004},[9808],{"type":31,"value":9809},"CanRedo",{"type":25,"tag":2682,"props":9811,"children":9812},{"style":2700},[9813],{"type":31,"value":3021},{"type":25,"tag":2682,"props":9815,"children":9816},{"style":2688},[9817],{"type":31,"value":9633},{"type":25,"tag":2682,"props":9819,"children":9820},{"style":2700},[9821],{"type":31,"value":2713},{"type":25,"tag":2682,"props":9823,"children":9825},{"class":2684,"line":9824},38,[9826,9830,9834,9838,9842,9846,9850],{"type":25,"tag":2682,"props":9827,"children":9828},{"style":2762},[9829],{"type":31,"value":9646},{"type":25,"tag":2682,"props":9831,"children":9832},{"style":2852},[9833],{"type":31,"value":9438},{"type":25,"tag":2682,"props":9835,"children":9836},{"style":2700},[9837],{"type":31,"value":2968},{"type":25,"tag":2682,"props":9839,"children":9840},{"style":3004},[9841],{"type":31,"value":9153},{"type":25,"tag":2682,"props":9843,"children":9844},{"style":2700},[9845],{"type":31,"value":2703},{"type":25,"tag":2682,"props":9847,"children":9848},{"style":2852},[9849],{"type":31,"value":9667},{"type":25,"tag":2682,"props":9851,"children":9852},{"style":2700},[9853],{"type":31,"value":3041},{"type":25,"tag":2682,"props":9855,"children":9857},{"class":2684,"line":9856},39,[9858,9862,9866,9870],{"type":25,"tag":2682,"props":9859,"children":9860},{"style":3004},[9861],{"type":31,"value":9457},{"type":25,"tag":2682,"props":9863,"children":9864},{"style":2700},[9865],{"type":31,"value":2703},{"type":25,"tag":2682,"props":9867,"children":9868},{"style":2852},[9869],{"type":31,"value":8998},{"type":25,"tag":2682,"props":9871,"children":9872},{"style":2700},[9873],{"type":31,"value":3041},{"type":25,"tag":2682,"props":9875,"children":9877},{"class":2684,"line":9876},40,[9878,9882,9886,9890,9894,9898],{"type":25,"tag":2682,"props":9879,"children":9880},{"style":3004},[9881],{"type":31,"value":9477},{"type":25,"tag":2682,"props":9883,"children":9884},{"style":2700},[9885],{"type":31,"value":2703},{"type":25,"tag":2682,"props":9887,"children":9888},{"style":2852},[9889],{"type":31,"value":9486},{"type":25,"tag":2682,"props":9891,"children":9892},{"style":2700},[9893],{"type":31,"value":3076},{"type":25,"tag":2682,"props":9895,"children":9896},{"style":3004},[9897],{"type":31,"value":9495},{"type":25,"tag":2682,"props":9899,"children":9900},{"style":2700},[9901],{"type":31,"value":3085},{"type":25,"tag":2682,"props":9903,"children":9905},{"class":2684,"line":9904},41,[9906,9910,9914,9918,9922],{"type":25,"tag":2682,"props":9907,"children":9908},{"style":3004},[9909],{"type":31,"value":9542},{"type":25,"tag":2682,"props":9911,"children":9912},{"style":2762},[9913],{"type":31,"value":2849},{"type":25,"tag":2682,"props":9915,"children":9916},{"style":2700},[9917],{"type":31,"value":2703},{"type":25,"tag":2682,"props":9919,"children":9920},{"style":2852},[9921],{"type":31,"value":3166},{"type":25,"tag":2682,"props":9923,"children":9924},{"style":2700},[9925],{"type":31,"value":3041},{"type":25,"tag":2682,"props":9927,"children":9929},{"class":2684,"line":9928},42,[9930],{"type":25,"tag":2682,"props":9931,"children":9932},{"style":2700},[9933],{"type":31,"value":3094},{"type":25,"tag":2682,"props":9935,"children":9937},{"class":2684,"line":9936},43,[9938],{"type":25,"tag":2682,"props":9939,"children":9940},{"style":2700},[9941],{"type":31,"value":3219},{"type":25,"tag":33,"props":9943,"children":9944},{},[9945],{"type":31,"value":9946},"ここで注目ポイントが2つあります。",{"type":25,"tag":33,"props":9948,"children":9949},{},[9950,9952,9957,9959,9965],{"type":31,"value":9951},"1つ目は",{"type":25,"tag":529,"props":9953,"children":9955},{"className":9954},[],[9956],{"type":31,"value":8998},{"type":31,"value":9958},"の中で",{"type":25,"tag":529,"props":9960,"children":9962},{"className":9961},[],[9963],{"type":31,"value":9964},"_redoStack.Clear()",{"type":31,"value":9966},"を呼んでいる点です。Undoした後に新しい操作を実行したら、「未来の枝」（Redo可能だった履歴）は全部捨てるのが一般的な挙動です。Adobe系のアプリでUndo→別の操作をするとRedoが効かなくなるのと同じ挙動で、ユーザーが違和感なく使えます。",{"type":25,"tag":33,"props":9968,"children":9969},{},[9970,9972,9978,9980,9986,9988,9993,9994,9999],{"type":31,"value":9971},"2つ目は",{"type":25,"tag":529,"props":9973,"children":9975},{"className":9974},[],[9976],{"type":31,"value":9977},"StateChanged",{"type":31,"value":9979},"イベントです。Blazor側ではこのイベントに購読して、UndoボタンとRedoボタンの",{"type":25,"tag":529,"props":9981,"children":9983},{"className":9982},[],[9984],{"type":31,"value":9985},"disabled",{"type":31,"value":9987},"属性を",{"type":25,"tag":529,"props":9989,"children":9991},{"className":9990},[],[9992],{"type":31,"value":9624},{"type":31,"value":5972},{"type":25,"tag":529,"props":9995,"children":9997},{"className":9996},[],[9998],{"type":31,"value":9809},{"type":31,"value":10000},"に連動させます。イベント駆動にしておけば、どこからコマンドを実行してもUIが自動で追従します。",{"type":25,"tag":26,"props":10002,"children":10004},{"id":10003},"trimhistory履歴上限でメモリリークを防ぐ",[10005],{"type":31,"value":10006},"TrimHistory：履歴上限でメモリリークを防ぐ",{"type":25,"tag":33,"props":10008,"children":10009},{},[10010],{"type":31,"value":10011},"長時間編集していると履歴はどんどん積み上がります。PICOMの楽譜エディタでは、音符の配置・削除・移動だけでも1時間の編集で数百件を超えるので、履歴を無制限に持っているとメモリが膨れ上がる恐れがあります。",{"type":25,"tag":33,"props":10013,"children":10014},{},[10015,10017,10023],{"type":31,"value":10016},"そこで、最大履歴数を超えたら古いものから捨てる",{"type":25,"tag":529,"props":10018,"children":10020},{"className":10019},[],[10021],{"type":31,"value":10022},"TrimHistory",{"type":31,"value":10024},"を実装しました。",{"type":25,"tag":524,"props":10026,"children":10028},{"className":2674,"code":10027,"language":2676,"meta":8,"style":8},"private void TrimHistory()\n{\n    while (_undoStack.Count > _maxHistorySize)\n    {\n        var list = _undoStack.ToList();\n        list.RemoveAt(list.Count - 1);\n        _undoStack.Clear();\n        foreach (var item in list.AsEnumerable().Reverse())\n        {\n            _undoStack.Push(item);\n        }\n    }\n}\n",[10029],{"type":25,"tag":529,"props":10030,"children":10031},{"__ignoreMap":8},[10032,10052,10059,10095,10102,10135,10181,10200,10254,10262,10291,10299,10306],{"type":25,"tag":2682,"props":10033,"children":10034},{"class":2684,"line":18},[10035,10039,10043,10048],{"type":25,"tag":2682,"props":10036,"children":10037},{"style":2762},[10038],{"type":31,"value":4165},{"type":25,"tag":2682,"props":10040,"children":10041},{"style":2688},[10042],{"type":31,"value":2881},{"type":25,"tag":2682,"props":10044,"children":10045},{"style":2852},[10046],{"type":31,"value":10047}," TrimHistory",{"type":25,"tag":2682,"props":10049,"children":10050},{"style":2700},[10051],{"type":31,"value":9595},{"type":25,"tag":2682,"props":10053,"children":10054},{"class":2684,"line":1149},[10055],{"type":25,"tag":2682,"props":10056,"children":10057},{"style":2700},[10058],{"type":31,"value":2825},{"type":25,"tag":2682,"props":10060,"children":10061},{"class":2684,"line":1159},[10062,10067,10071,10075,10079,10083,10087,10091],{"type":25,"tag":2682,"props":10063,"children":10064},{"style":2688},[10065],{"type":31,"value":10066},"    while",{"type":25,"tag":2682,"props":10068,"children":10069},{"style":2700},[10070],{"type":31,"value":3001},{"type":25,"tag":2682,"props":10072,"children":10073},{"style":3004},[10074],{"type":31,"value":9037},{"type":25,"tag":2682,"props":10076,"children":10077},{"style":2700},[10078],{"type":31,"value":2703},{"type":25,"tag":2682,"props":10080,"children":10081},{"style":3004},[10082],{"type":31,"value":9345},{"type":25,"tag":2682,"props":10084,"children":10085},{"style":2700},[10086],{"type":31,"value":9350},{"type":25,"tag":2682,"props":10088,"children":10089},{"style":3004},[10090],{"type":31,"value":9185},{"type":25,"tag":2682,"props":10092,"children":10093},{"style":2700},[10094],{"type":31,"value":2978},{"type":25,"tag":2682,"props":10096,"children":10097},{"class":2684,"line":2758},[10098],{"type":25,"tag":2682,"props":10099,"children":10100},{"style":2700},[10101],{"type":31,"value":2987},{"type":25,"tag":2682,"props":10103,"children":10104},{"class":2684,"line":2777},[10105,10109,10114,10118,10122,10126,10131],{"type":25,"tag":2682,"props":10106,"children":10107},{"style":2762},[10108],{"type":31,"value":9646},{"type":25,"tag":2682,"props":10110,"children":10111},{"style":2852},[10112],{"type":31,"value":10113}," list",{"type":25,"tag":2682,"props":10115,"children":10116},{"style":2700},[10117],{"type":31,"value":2968},{"type":25,"tag":2682,"props":10119,"children":10120},{"style":3004},[10121],{"type":31,"value":9109},{"type":25,"tag":2682,"props":10123,"children":10124},{"style":2700},[10125],{"type":31,"value":2703},{"type":25,"tag":2682,"props":10127,"children":10128},{"style":2852},[10129],{"type":31,"value":10130},"ToList",{"type":25,"tag":2682,"props":10132,"children":10133},{"style":2700},[10134],{"type":31,"value":3041},{"type":25,"tag":2682,"props":10136,"children":10137},{"class":2684,"line":2785},[10138,10143,10147,10151,10155,10160,10164,10168,10173,10177],{"type":25,"tag":2682,"props":10139,"children":10140},{"style":3004},[10141],{"type":31,"value":10142},"        list",{"type":25,"tag":2682,"props":10144,"children":10145},{"style":2700},[10146],{"type":31,"value":2703},{"type":25,"tag":2682,"props":10148,"children":10149},{"style":2852},[10150],{"type":31,"value":8962},{"type":25,"tag":2682,"props":10152,"children":10153},{"style":2700},[10154],{"type":31,"value":3076},{"type":25,"tag":2682,"props":10156,"children":10157},{"style":3004},[10158],{"type":31,"value":10159},"list",{"type":25,"tag":2682,"props":10161,"children":10162},{"style":2700},[10163],{"type":31,"value":2703},{"type":25,"tag":2682,"props":10165,"children":10166},{"style":3004},[10167],{"type":31,"value":9345},{"type":25,"tag":2682,"props":10169,"children":10170},{"style":2762},[10171],{"type":31,"value":10172}," -",{"type":25,"tag":2682,"props":10174,"children":10175},{"style":5609},[10176],{"type":31,"value":7459},{"type":25,"tag":2682,"props":10178,"children":10179},{"style":2700},[10180],{"type":31,"value":3085},{"type":25,"tag":2682,"props":10182,"children":10183},{"class":2684,"line":2819},[10184,10188,10192,10196],{"type":25,"tag":2682,"props":10185,"children":10186},{"style":3004},[10187],{"type":31,"value":9477},{"type":25,"tag":2682,"props":10189,"children":10190},{"style":2700},[10191],{"type":31,"value":2703},{"type":25,"tag":2682,"props":10193,"children":10194},{"style":2852},[10195],{"type":31,"value":9516},{"type":25,"tag":2682,"props":10197,"children":10198},{"style":2700},[10199],{"type":31,"value":3041},{"type":25,"tag":2682,"props":10201,"children":10202},{"class":2684,"line":2828},[10203,10208,10212,10216,10221,10226,10230,10234,10239,10244,10249],{"type":25,"tag":2682,"props":10204,"children":10205},{"style":2688},[10206],{"type":31,"value":10207},"        foreach",{"type":25,"tag":2682,"props":10209,"children":10210},{"style":2700},[10211],{"type":31,"value":3001},{"type":25,"tag":2682,"props":10213,"children":10214},{"style":2762},[10215],{"type":31,"value":4539},{"type":25,"tag":2682,"props":10217,"children":10218},{"style":2852},[10219],{"type":31,"value":10220}," item",{"type":25,"tag":2682,"props":10222,"children":10223},{"style":2688},[10224],{"type":31,"value":10225}," in",{"type":25,"tag":2682,"props":10227,"children":10228},{"style":3004},[10229],{"type":31,"value":10113},{"type":25,"tag":2682,"props":10231,"children":10232},{"style":2700},[10233],{"type":31,"value":2703},{"type":25,"tag":2682,"props":10235,"children":10236},{"style":2852},[10237],{"type":31,"value":10238},"AsEnumerable",{"type":25,"tag":2682,"props":10240,"children":10241},{"style":2700},[10242],{"type":31,"value":10243},"().",{"type":25,"tag":2682,"props":10245,"children":10246},{"style":2852},[10247],{"type":31,"value":10248},"Reverse",{"type":25,"tag":2682,"props":10250,"children":10251},{"style":2700},[10252],{"type":31,"value":10253},"())\n",{"type":25,"tag":2682,"props":10255,"children":10256},{"class":2684,"line":2862},[10257],{"type":25,"tag":2682,"props":10258,"children":10259},{"style":2700},[10260],{"type":31,"value":10261},"        {\n",{"type":25,"tag":2682,"props":10263,"children":10264},{"class":2684,"line":2870},[10265,10270,10274,10278,10282,10287],{"type":25,"tag":2682,"props":10266,"children":10267},{"style":3004},[10268],{"type":31,"value":10269},"            _undoStack",{"type":25,"tag":2682,"props":10271,"children":10272},{"style":2700},[10273],{"type":31,"value":2703},{"type":25,"tag":2682,"props":10275,"children":10276},{"style":2852},[10277],{"type":31,"value":9486},{"type":25,"tag":2682,"props":10279,"children":10280},{"style":2700},[10281],{"type":31,"value":3076},{"type":25,"tag":2682,"props":10283,"children":10284},{"style":3004},[10285],{"type":31,"value":10286},"item",{"type":25,"tag":2682,"props":10288,"children":10289},{"style":2700},[10290],{"type":31,"value":3085},{"type":25,"tag":2682,"props":10292,"children":10293},{"class":2684,"line":2981},[10294],{"type":25,"tag":2682,"props":10295,"children":10296},{"style":2700},[10297],{"type":31,"value":10298},"        }\n",{"type":25,"tag":2682,"props":10300,"children":10301},{"class":2684,"line":2990},[10302],{"type":25,"tag":2682,"props":10303,"children":10304},{"style":2700},[10305],{"type":31,"value":3094},{"type":25,"tag":2682,"props":10307,"children":10308},{"class":2684,"line":3044},[10309],{"type":25,"tag":2682,"props":10310,"children":10311},{"style":2700},[10312],{"type":31,"value":3219},{"type":25,"tag":33,"props":10314,"children":10315},{},[10316,10322,10324,10329,10331,10337],{"type":25,"tag":529,"props":10317,"children":10319},{"className":10318},[],[10320],{"type":31,"value":10321},"Stack\u003CT>",{"type":31,"value":10323},"は最古要素を直接削除するAPIを持っていないので、一度",{"type":25,"tag":529,"props":10325,"children":10327},{"className":10326},[],[10328],{"type":31,"value":7381},{"type":31,"value":10330},"に展開して最後（最古）を削り、再度Pushし直しています。愚直ですが確実で、",{"type":25,"tag":529,"props":10332,"children":10334},{"className":10333},[],[10335],{"type":31,"value":10336},"_maxHistorySize = 100",{"type":31,"value":10338},"程度なら実行コストは無視できる範囲です。",{"type":25,"tag":346,"props":10340,"children":10341},{"cons-label":3432,"pros-label":3433},[10342,10382],{"type":25,"tag":352,"props":10343,"children":10344},{"v-slot:pros":8},[10345],{"type":25,"tag":53,"props":10346,"children":10347},{},[10348,10361,10377],{"type":25,"tag":57,"props":10349,"children":10350},{},[10351,10353,10359],{"type":31,"value":10352},"Undo/Redoの挙動が",{"type":25,"tag":529,"props":10354,"children":10356},{"className":10355},[],[10357],{"type":31,"value":10358},"Push/Pop",{"type":31,"value":10360},"と綺麗に一致して読みやすい",{"type":25,"tag":57,"props":10362,"children":10363},{},[10364,10369,10370,10375],{"type":25,"tag":529,"props":10365,"children":10367},{"className":10366},[],[10368],{"type":31,"value":9516},{"type":31,"value":5972},{"type":25,"tag":529,"props":10371,"children":10373},{"className":10372},[],[10374],{"type":31,"value":9345},{"type":31,"value":10376},"などの基本APIで十分足りる",{"type":25,"tag":57,"props":10378,"children":10379},{},[10380],{"type":31,"value":10381},"履歴構造の意図が型から自明",{"type":25,"tag":352,"props":10383,"children":10384},{"v-slot:cons":8},[10385,10398],{"type":25,"tag":57,"props":10386,"children":10387},{},[10388,10390,10396],{"type":31,"value":10389},"最古要素の削除に展開→詰め直しが必要（",{"type":25,"tag":529,"props":10391,"children":10393},{"className":10392},[],[10394],{"type":31,"value":10395},"LinkedList",{"type":31,"value":10397},"ならO(1)）",{"type":25,"tag":57,"props":10399,"children":10400},{},[10401],{"type":31,"value":10402},"双方向からの参照が欲しくなった時に不便",{"type":25,"tag":33,"props":10404,"children":10405},{},[10406,10408,10414,10416,10422],{"type":31,"value":10407},"もし履歴数が数万を想定する場合は",{"type":25,"tag":529,"props":10409,"children":10411},{"className":10410},[],[10412],{"type":31,"value":10413},"LinkedList\u003CICommand>",{"type":31,"value":10415},"や",{"type":25,"tag":529,"props":10417,"children":10419},{"className":10418},[],[10420],{"type":31,"value":10421},"Deque",{"type":31,"value":10423},"的な構造を使った方が良いのですが、エディタのUndo履歴は100件前後で十分なので、可読性を優先してStackにしています。",{"type":25,"tag":26,"props":10425,"children":10427},{"id":10426},"compositecommand複数操作を1手にまとめる",[10428],{"type":31,"value":10429},"CompositeCommand：複数操作を1手にまとめる",{"type":25,"tag":33,"props":10431,"children":10432},{},[10433,10435,10440,10442,10447,10449,10455],{"type":31,"value":10434},"実装していて一番「これがないとまずい」と感じたのが",{"type":25,"tag":529,"props":10436,"children":10438},{"className":10437},[],[10439],{"type":31,"value":8243},{"type":31,"value":10441},"です。たとえばピアノロール上で",{"type":25,"tag":103,"props":10443,"children":10444},{},[10445],{"type":31,"value":10446},"複数の音符を選択して一括で下に動かす",{"type":31,"value":10448},"操作を考えると、これを1つ1つ",{"type":25,"tag":529,"props":10450,"children":10452},{"className":10451},[],[10453],{"type":31,"value":10454},"MoveNotesCommand",{"type":31,"value":10456},"として履歴に積んだら、Undoを何回も押さないと元の状態に戻りません。",{"type":25,"tag":33,"props":10458,"children":10459},{},[10460],{"type":31,"value":10461},"解決策は「中に複数のコマンドを持ち、Execute/Undoで全部を順番に回すコマンド」を作ることです。",{"type":25,"tag":524,"props":10463,"children":10465},{"className":2674,"code":10464,"language":2676,"meta":8,"style":8},"public class CompositeCommand : ICommand\n{\n    private readonly List\u003CICommand> _commands;\n    private readonly string _description;\n\n    public string Description => _description;\n\n    public CompositeCommand(string description, IEnumerable\u003CICommand> commands)\n    {\n        _description = description;\n        _commands = commands.ToList();\n    }\n\n    public void Execute()\n    {\n        foreach (var command in _commands)\n        {\n            command.Execute();\n        }\n    }\n\n    public void Undo()\n    {\n        for (int i = _commands.Count - 1; i >= 0; i--)\n        {\n            _commands[i].Undo();\n        }\n    }\n}\n",[10466],{"type":25,"tag":529,"props":10467,"children":10468},{"__ignoreMap":8},[10469,10493,10500,10537,10561,10568,10595,10602,10656,10663,10683,10711,10718,10725,10744,10751,10782,10789,10809,10816,10823,10830,10849,10856,10935,10942,10972,10979,10986],{"type":25,"tag":2682,"props":10470,"children":10471},{"class":2684,"line":18},[10472,10476,10480,10485,10489],{"type":25,"tag":2682,"props":10473,"children":10474},{"style":2762},[10475],{"type":31,"value":2791},{"type":25,"tag":2682,"props":10477,"children":10478},{"style":2762},[10479],{"type":31,"value":2801},{"type":25,"tag":2682,"props":10481,"children":10482},{"style":2694},[10483],{"type":31,"value":10484}," CompositeCommand",{"type":25,"tag":2682,"props":10486,"children":10487},{"style":2700},[10488],{"type":31,"value":2811},{"type":25,"tag":2682,"props":10490,"children":10491},{"style":2694},[10492],{"type":31,"value":8302},{"type":25,"tag":2682,"props":10494,"children":10495},{"class":2684,"line":1149},[10496],{"type":25,"tag":2682,"props":10497,"children":10498},{"style":2700},[10499],{"type":31,"value":2825},{"type":25,"tag":2682,"props":10501,"children":10502},{"class":2684,"line":1159},[10503,10507,10511,10516,10520,10524,10528,10533],{"type":25,"tag":2682,"props":10504,"children":10505},{"style":2762},[10506],{"type":31,"value":3277},{"type":25,"tag":2682,"props":10508,"children":10509},{"style":2762},[10510],{"type":31,"value":8606},{"type":25,"tag":2682,"props":10512,"children":10513},{"style":2694},[10514],{"type":31,"value":10515}," List",{"type":25,"tag":2682,"props":10517,"children":10518},{"style":2700},[10519],{"type":31,"value":2891},{"type":25,"tag":2682,"props":10521,"children":10522},{"style":2694},[10523],{"type":31,"value":8227},{"type":25,"tag":2682,"props":10525,"children":10526},{"style":2700},[10527],{"type":31,"value":3548},{"type":25,"tag":2682,"props":10529,"children":10530},{"style":2852},[10531],{"type":31,"value":10532}," _commands",{"type":25,"tag":2682,"props":10534,"children":10535},{"style":2700},[10536],{"type":31,"value":2713},{"type":25,"tag":2682,"props":10538,"children":10539},{"class":2684,"line":2758},[10540,10544,10548,10552,10557],{"type":25,"tag":2682,"props":10541,"children":10542},{"style":2762},[10543],{"type":31,"value":3277},{"type":25,"tag":2682,"props":10545,"children":10546},{"style":2762},[10547],{"type":31,"value":8606},{"type":25,"tag":2682,"props":10549,"children":10550},{"style":2688},[10551],{"type":31,"value":2954},{"type":25,"tag":2682,"props":10553,"children":10554},{"style":2852},[10555],{"type":31,"value":10556}," _description",{"type":25,"tag":2682,"props":10558,"children":10559},{"style":2700},[10560],{"type":31,"value":2713},{"type":25,"tag":2682,"props":10562,"children":10563},{"class":2684,"line":2777},[10564],{"type":25,"tag":2682,"props":10565,"children":10566},{"emptyLinePlaceholder":2752},[10567],{"type":31,"value":2755},{"type":25,"tag":2682,"props":10569,"children":10570},{"class":2684,"line":2785},[10571,10575,10579,10583,10587,10591],{"type":25,"tag":2682,"props":10572,"children":10573},{"style":2762},[10574],{"type":31,"value":2834},{"type":25,"tag":2682,"props":10576,"children":10577},{"style":2688},[10578],{"type":31,"value":2954},{"type":25,"tag":2682,"props":10580,"children":10581},{"style":2852},[10582],{"type":31,"value":8477},{"type":25,"tag":2682,"props":10584,"children":10585},{"style":2762},[10586],{"type":31,"value":3336},{"type":25,"tag":2682,"props":10588,"children":10589},{"style":3004},[10590],{"type":31,"value":10556},{"type":25,"tag":2682,"props":10592,"children":10593},{"style":2700},[10594],{"type":31,"value":2713},{"type":25,"tag":2682,"props":10596,"children":10597},{"class":2684,"line":2819},[10598],{"type":25,"tag":2682,"props":10599,"children":10600},{"emptyLinePlaceholder":2752},[10601],{"type":31,"value":2755},{"type":25,"tag":2682,"props":10603,"children":10604},{"class":2684,"line":2828},[10605,10609,10613,10617,10621,10626,10630,10635,10639,10643,10647,10652],{"type":25,"tag":2682,"props":10606,"children":10607},{"style":2762},[10608],{"type":31,"value":2834},{"type":25,"tag":2682,"props":10610,"children":10611},{"style":2852},[10612],{"type":31,"value":10484},{"type":25,"tag":2682,"props":10614,"children":10615},{"style":2700},[10616],{"type":31,"value":3076},{"type":25,"tag":2682,"props":10618,"children":10619},{"style":2688},[10620],{"type":31,"value":3128},{"type":25,"tag":2682,"props":10622,"children":10623},{"style":2852},[10624],{"type":31,"value":10625}," description",{"type":25,"tag":2682,"props":10627,"children":10628},{"style":2700},[10629],{"type":31,"value":2921},{"type":25,"tag":2682,"props":10631,"children":10632},{"style":2694},[10633],{"type":31,"value":10634}," IEnumerable",{"type":25,"tag":2682,"props":10636,"children":10637},{"style":2700},[10638],{"type":31,"value":2891},{"type":25,"tag":2682,"props":10640,"children":10641},{"style":2694},[10642],{"type":31,"value":8227},{"type":25,"tag":2682,"props":10644,"children":10645},{"style":2700},[10646],{"type":31,"value":3548},{"type":25,"tag":2682,"props":10648,"children":10649},{"style":2852},[10650],{"type":31,"value":10651}," commands",{"type":25,"tag":2682,"props":10653,"children":10654},{"style":2700},[10655],{"type":31,"value":2978},{"type":25,"tag":2682,"props":10657,"children":10658},{"class":2684,"line":2862},[10659],{"type":25,"tag":2682,"props":10660,"children":10661},{"style":2700},[10662],{"type":31,"value":2987},{"type":25,"tag":2682,"props":10664,"children":10665},{"class":2684,"line":2870},[10666,10671,10675,10679],{"type":25,"tag":2682,"props":10667,"children":10668},{"style":3004},[10669],{"type":31,"value":10670},"        _description",{"type":25,"tag":2682,"props":10672,"children":10673},{"style":2700},[10674],{"type":31,"value":2968},{"type":25,"tag":2682,"props":10676,"children":10677},{"style":3004},[10678],{"type":31,"value":10625},{"type":25,"tag":2682,"props":10680,"children":10681},{"style":2700},[10682],{"type":31,"value":2713},{"type":25,"tag":2682,"props":10684,"children":10685},{"class":2684,"line":2981},[10686,10691,10695,10699,10703,10707],{"type":25,"tag":2682,"props":10687,"children":10688},{"style":3004},[10689],{"type":31,"value":10690},"        _commands",{"type":25,"tag":2682,"props":10692,"children":10693},{"style":2700},[10694],{"type":31,"value":2968},{"type":25,"tag":2682,"props":10696,"children":10697},{"style":3004},[10698],{"type":31,"value":10651},{"type":25,"tag":2682,"props":10700,"children":10701},{"style":2700},[10702],{"type":31,"value":2703},{"type":25,"tag":2682,"props":10704,"children":10705},{"style":2852},[10706],{"type":31,"value":10130},{"type":25,"tag":2682,"props":10708,"children":10709},{"style":2700},[10710],{"type":31,"value":3041},{"type":25,"tag":2682,"props":10712,"children":10713},{"class":2684,"line":2990},[10714],{"type":25,"tag":2682,"props":10715,"children":10716},{"style":2700},[10717],{"type":31,"value":3094},{"type":25,"tag":2682,"props":10719,"children":10720},{"class":2684,"line":3044},[10721],{"type":25,"tag":2682,"props":10722,"children":10723},{"emptyLinePlaceholder":2752},[10724],{"type":31,"value":2755},{"type":25,"tag":2682,"props":10726,"children":10727},{"class":2684,"line":3065},[10728,10732,10736,10740],{"type":25,"tag":2682,"props":10729,"children":10730},{"style":2762},[10731],{"type":31,"value":2834},{"type":25,"tag":2682,"props":10733,"children":10734},{"style":2688},[10735],{"type":31,"value":2881},{"type":25,"tag":2682,"props":10737,"children":10738},{"style":2852},[10739],{"type":31,"value":8358},{"type":25,"tag":2682,"props":10741,"children":10742},{"style":2700},[10743],{"type":31,"value":9595},{"type":25,"tag":2682,"props":10745,"children":10746},{"class":2684,"line":3088},[10747],{"type":25,"tag":2682,"props":10748,"children":10749},{"style":2700},[10750],{"type":31,"value":2987},{"type":25,"tag":2682,"props":10752,"children":10753},{"class":2684,"line":3097},[10754,10758,10762,10766,10770,10774,10778],{"type":25,"tag":2682,"props":10755,"children":10756},{"style":2688},[10757],{"type":31,"value":10207},{"type":25,"tag":2682,"props":10759,"children":10760},{"style":2700},[10761],{"type":31,"value":3001},{"type":25,"tag":2682,"props":10763,"children":10764},{"style":2762},[10765],{"type":31,"value":4539},{"type":25,"tag":2682,"props":10767,"children":10768},{"style":2852},[10769],{"type":31,"value":9438},{"type":25,"tag":2682,"props":10771,"children":10772},{"style":2688},[10773],{"type":31,"value":10225},{"type":25,"tag":2682,"props":10775,"children":10776},{"style":3004},[10777],{"type":31,"value":10532},{"type":25,"tag":2682,"props":10779,"children":10780},{"style":2700},[10781],{"type":31,"value":2978},{"type":25,"tag":2682,"props":10783,"children":10784},{"class":2684,"line":3105},[10785],{"type":25,"tag":2682,"props":10786,"children":10787},{"style":2700},[10788],{"type":31,"value":10261},{"type":25,"tag":2682,"props":10790,"children":10791},{"class":2684,"line":3139},[10792,10797,10801,10805],{"type":25,"tag":2682,"props":10793,"children":10794},{"style":3004},[10795],{"type":31,"value":10796},"            command",{"type":25,"tag":2682,"props":10798,"children":10799},{"style":2700},[10800],{"type":31,"value":2703},{"type":25,"tag":2682,"props":10802,"children":10803},{"style":2852},[10804],{"type":31,"value":8998},{"type":25,"tag":2682,"props":10806,"children":10807},{"style":2700},[10808],{"type":31,"value":3041},{"type":25,"tag":2682,"props":10810,"children":10811},{"class":2684,"line":3147},[10812],{"type":25,"tag":2682,"props":10813,"children":10814},{"style":2700},[10815],{"type":31,"value":10298},{"type":25,"tag":2682,"props":10817,"children":10818},{"class":2684,"line":3205},[10819],{"type":25,"tag":2682,"props":10820,"children":10821},{"style":2700},[10822],{"type":31,"value":3094},{"type":25,"tag":2682,"props":10824,"children":10825},{"class":2684,"line":3213},[10826],{"type":25,"tag":2682,"props":10827,"children":10828},{"emptyLinePlaceholder":2752},[10829],{"type":31,"value":2755},{"type":25,"tag":2682,"props":10831,"children":10832},{"class":2684,"line":9523},[10833,10837,10841,10845],{"type":25,"tag":2682,"props":10834,"children":10835},{"style":2762},[10836],{"type":31,"value":2834},{"type":25,"tag":2682,"props":10838,"children":10839},{"style":2688},[10840],{"type":31,"value":2881},{"type":25,"tag":2682,"props":10842,"children":10843},{"style":2852},[10844],{"type":31,"value":8417},{"type":25,"tag":2682,"props":10846,"children":10847},{"style":2700},[10848],{"type":31,"value":9595},{"type":25,"tag":2682,"props":10850,"children":10851},{"class":2684,"line":9536},[10852],{"type":25,"tag":2682,"props":10853,"children":10854},{"style":2700},[10855],{"type":31,"value":2987},{"type":25,"tag":2682,"props":10857,"children":10858},{"class":2684,"line":9561},[10859,10864,10868,10872,10877,10881,10885,10889,10893,10897,10901,10905,10909,10914,10918,10922,10926,10931],{"type":25,"tag":2682,"props":10860,"children":10861},{"style":2688},[10862],{"type":31,"value":10863},"        for",{"type":25,"tag":2682,"props":10865,"children":10866},{"style":2700},[10867],{"type":31,"value":3001},{"type":25,"tag":2682,"props":10869,"children":10870},{"style":2688},[10871],{"type":31,"value":9253},{"type":25,"tag":2682,"props":10873,"children":10874},{"style":2852},[10875],{"type":31,"value":10876}," i",{"type":25,"tag":2682,"props":10878,"children":10879},{"style":2700},[10880],{"type":31,"value":2968},{"type":25,"tag":2682,"props":10882,"children":10883},{"style":3004},[10884],{"type":31,"value":10532},{"type":25,"tag":2682,"props":10886,"children":10887},{"style":2700},[10888],{"type":31,"value":2703},{"type":25,"tag":2682,"props":10890,"children":10891},{"style":3004},[10892],{"type":31,"value":9345},{"type":25,"tag":2682,"props":10894,"children":10895},{"style":2762},[10896],{"type":31,"value":10172},{"type":25,"tag":2682,"props":10898,"children":10899},{"style":5609},[10900],{"type":31,"value":7459},{"type":25,"tag":2682,"props":10902,"children":10903},{"style":2700},[10904],{"type":31,"value":3708},{"type":25,"tag":2682,"props":10906,"children":10907},{"style":3004},[10908],{"type":31,"value":10876},{"type":25,"tag":2682,"props":10910,"children":10911},{"style":2700},[10912],{"type":31,"value":10913}," >=",{"type":25,"tag":2682,"props":10915,"children":10916},{"style":5609},[10917],{"type":31,"value":9355},{"type":25,"tag":2682,"props":10919,"children":10920},{"style":2700},[10921],{"type":31,"value":3708},{"type":25,"tag":2682,"props":10923,"children":10924},{"style":3004},[10925],{"type":31,"value":10876},{"type":25,"tag":2682,"props":10927,"children":10928},{"style":2762},[10929],{"type":31,"value":10930},"--",{"type":25,"tag":2682,"props":10932,"children":10933},{"style":2700},[10934],{"type":31,"value":2978},{"type":25,"tag":2682,"props":10936,"children":10937},{"class":2684,"line":9569},[10938],{"type":25,"tag":2682,"props":10939,"children":10940},{"style":2700},[10941],{"type":31,"value":10261},{"type":25,"tag":2682,"props":10943,"children":10944},{"class":2684,"line":9577},[10945,10950,10954,10959,10964,10968],{"type":25,"tag":2682,"props":10946,"children":10947},{"style":3004},[10948],{"type":31,"value":10949},"            _commands",{"type":25,"tag":2682,"props":10951,"children":10952},{"style":2700},[10953],{"type":31,"value":5556},{"type":25,"tag":2682,"props":10955,"children":10956},{"style":3004},[10957],{"type":31,"value":10958},"i",{"type":25,"tag":2682,"props":10960,"children":10961},{"style":2700},[10962],{"type":31,"value":10963},"].",{"type":25,"tag":2682,"props":10965,"children":10966},{"style":2852},[10967],{"type":31,"value":9005},{"type":25,"tag":2682,"props":10969,"children":10970},{"style":2700},[10971],{"type":31,"value":3041},{"type":25,"tag":2682,"props":10973,"children":10974},{"class":2684,"line":9598},[10975],{"type":25,"tag":2682,"props":10976,"children":10977},{"style":2700},[10978],{"type":31,"value":10298},{"type":25,"tag":2682,"props":10980,"children":10981},{"class":2684,"line":9606},[10982],{"type":25,"tag":2682,"props":10983,"children":10984},{"style":2700},[10985],{"type":31,"value":3094},{"type":25,"tag":2682,"props":10987,"children":10988},{"class":2684,"line":9640},[10989],{"type":25,"tag":2682,"props":10990,"children":10991},{"style":2700},[10992],{"type":31,"value":3219},{"type":25,"tag":33,"props":10994,"children":10995},{},[10996,10998,11003],{"type":31,"value":10997},"地味に大事なのが",{"type":25,"tag":103,"props":10999,"children":11000},{},[11001],{"type":31,"value":11002},"Undo時は逆順で回す",{"type":31,"value":11004},"ところです。「AをしてからBをした」なら、戻す時は「Bを戻してからAを戻す」のが正しい順序です。コマンド間に副作用の依存があるとこの順序を間違えた時にバグるので、注意ポイントです。",{"type":25,"tag":33,"props":11006,"children":11007},{},[11008],{"type":31,"value":11009},"使い方はこんな感じです。",{"type":25,"tag":524,"props":11011,"children":11013},{"className":2674,"code":11012,"language":2676,"meta":8,"style":8},"var commands = selectedNotes.Select(note => new MoveNoteCommand(track, note, delta));\nvar composite = new CompositeCommand($\"{selectedNotes.Count}個の音符を移動\", commands);\n_undoRedoManager.Execute(composite);\n",[11014],{"type":25,"tag":529,"props":11015,"children":11016},{"__ignoreMap":8},[11017,11098,11172],{"type":25,"tag":2682,"props":11018,"children":11019},{"class":2684,"line":18},[11020,11024,11028,11032,11037,11041,11045,11049,11054,11058,11062,11067,11071,11076,11080,11085,11089,11094],{"type":25,"tag":2682,"props":11021,"children":11022},{"style":2762},[11023],{"type":31,"value":4539},{"type":25,"tag":2682,"props":11025,"children":11026},{"style":2852},[11027],{"type":31,"value":10651},{"type":25,"tag":2682,"props":11029,"children":11030},{"style":2700},[11031],{"type":31,"value":2968},{"type":25,"tag":2682,"props":11033,"children":11034},{"style":3004},[11035],{"type":31,"value":11036}," selectedNotes",{"type":25,"tag":2682,"props":11038,"children":11039},{"style":2700},[11040],{"type":31,"value":2703},{"type":25,"tag":2682,"props":11042,"children":11043},{"style":2852},[11044],{"type":31,"value":7518},{"type":25,"tag":2682,"props":11046,"children":11047},{"style":2700},[11048],{"type":31,"value":3076},{"type":25,"tag":2682,"props":11050,"children":11051},{"style":2852},[11052],{"type":31,"value":11053},"note",{"type":25,"tag":2682,"props":11055,"children":11056},{"style":2762},[11057],{"type":31,"value":3336},{"type":25,"tag":2682,"props":11059,"children":11060},{"style":2762},[11061],{"type":31,"value":3031},{"type":25,"tag":2682,"props":11063,"children":11064},{"style":2694},[11065],{"type":31,"value":11066}," MoveNoteCommand",{"type":25,"tag":2682,"props":11068,"children":11069},{"style":2700},[11070],{"type":31,"value":3076},{"type":25,"tag":2682,"props":11072,"children":11073},{"style":3004},[11074],{"type":31,"value":11075},"track",{"type":25,"tag":2682,"props":11077,"children":11078},{"style":2700},[11079],{"type":31,"value":2921},{"type":25,"tag":2682,"props":11081,"children":11082},{"style":3004},[11083],{"type":31,"value":11084}," note",{"type":25,"tag":2682,"props":11086,"children":11087},{"style":2700},[11088],{"type":31,"value":2921},{"type":25,"tag":2682,"props":11090,"children":11091},{"style":3004},[11092],{"type":31,"value":11093}," delta",{"type":25,"tag":2682,"props":11095,"children":11096},{"style":2700},[11097],{"type":31,"value":3202},{"type":25,"tag":2682,"props":11099,"children":11100},{"class":2684,"line":1149},[11101,11105,11110,11114,11118,11122,11126,11130,11134,11139,11143,11147,11151,11156,11160,11164,11168],{"type":25,"tag":2682,"props":11102,"children":11103},{"style":2762},[11104],{"type":31,"value":4539},{"type":25,"tag":2682,"props":11106,"children":11107},{"style":2852},[11108],{"type":31,"value":11109}," composite",{"type":25,"tag":2682,"props":11111,"children":11112},{"style":2700},[11113],{"type":31,"value":2968},{"type":25,"tag":2682,"props":11115,"children":11116},{"style":2762},[11117],{"type":31,"value":3031},{"type":25,"tag":2682,"props":11119,"children":11120},{"style":2694},[11121],{"type":31,"value":10484},{"type":25,"tag":2682,"props":11123,"children":11124},{"style":2700},[11125],{"type":31,"value":3076},{"type":25,"tag":2682,"props":11127,"children":11128},{"style":3293},[11129],{"type":31,"value":6854},{"type":25,"tag":2682,"props":11131,"children":11132},{"style":2700},[11133],{"type":31,"value":6864},{"type":25,"tag":2682,"props":11135,"children":11136},{"style":4759},[11137],{"type":31,"value":11138},"selectedNotes",{"type":25,"tag":2682,"props":11140,"children":11141},{"style":2700},[11142],{"type":31,"value":2703},{"type":25,"tag":2682,"props":11144,"children":11145},{"style":4759},[11146],{"type":31,"value":9345},{"type":25,"tag":2682,"props":11148,"children":11149},{"style":2700},[11150],{"type":31,"value":6882},{"type":25,"tag":2682,"props":11152,"children":11153},{"style":4759},[11154],{"type":31,"value":11155},"個の音符を移動",{"type":25,"tag":2682,"props":11157,"children":11158},{"style":3293},[11159],{"type":31,"value":4766},{"type":25,"tag":2682,"props":11161,"children":11162},{"style":2700},[11163],{"type":31,"value":2921},{"type":25,"tag":2682,"props":11165,"children":11166},{"style":3004},[11167],{"type":31,"value":10651},{"type":25,"tag":2682,"props":11169,"children":11170},{"style":2700},[11171],{"type":31,"value":3085},{"type":25,"tag":2682,"props":11173,"children":11174},{"class":2684,"line":1159},[11175,11180,11184,11188,11192,11197],{"type":25,"tag":2682,"props":11176,"children":11177},{"style":3004},[11178],{"type":31,"value":11179},"_undoRedoManager",{"type":25,"tag":2682,"props":11181,"children":11182},{"style":2700},[11183],{"type":31,"value":2703},{"type":25,"tag":2682,"props":11185,"children":11186},{"style":2852},[11187],{"type":31,"value":8998},{"type":25,"tag":2682,"props":11189,"children":11190},{"style":2700},[11191],{"type":31,"value":3076},{"type":25,"tag":2682,"props":11193,"children":11194},{"style":3004},[11195],{"type":31,"value":11196},"composite",{"type":25,"tag":2682,"props":11198,"children":11199},{"style":2700},[11200],{"type":31,"value":3085},{"type":25,"tag":33,"props":11202,"children":11203},{},[11204],{"type":31,"value":11205},"ユーザー視点では「選択した音符を全部動かす」を1手としてUndoできるので、快適に使えます。",{"type":25,"tag":26,"props":11207,"children":11208},{"id":1746},[11209],{"type":31,"value":1746},{"type":25,"tag":53,"props":11211,"children":11212},{},[11213,11242,11252,11257],{"type":25,"tag":57,"props":11214,"children":11215},{},[11216,11221,11223,11228,11229,11234,11235,11240],{"type":25,"tag":529,"props":11217,"children":11219},{"className":11218},[],[11220],{"type":31,"value":8227},{"type":31,"value":11222},"は",{"type":25,"tag":529,"props":11224,"children":11226},{"className":11225},[],[11227],{"type":31,"value":8998},{"type":31,"value":5972},{"type":25,"tag":529,"props":11230,"children":11232},{"className":11231},[],[11233],{"type":31,"value":9005},{"type":31,"value":5972},{"type":25,"tag":529,"props":11236,"children":11238},{"className":11237},[],[11239],{"type":31,"value":8511},{"type":31,"value":11241},"の3点セットで十分",{"type":25,"tag":57,"props":11243,"children":11244},{},[11245,11250],{"type":25,"tag":529,"props":11246,"children":11248},{"className":11247},[],[11249],{"type":31,"value":8235},{"type":31,"value":11251},"は2本のStackで管理、Redoスタックは新規Executeで破棄",{"type":25,"tag":57,"props":11253,"children":11254},{},[11255],{"type":31,"value":11256},"履歴上限を入れないとメモリ使用量が膨らむことに注意",{"type":25,"tag":57,"props":11258,"children":11259},{},[11260,11262,11267],{"type":31,"value":11261},"複数操作を一括でする",{"type":25,"tag":529,"props":11263,"children":11265},{"className":11264},[],[11266],{"type":31,"value":8243},{"type":31,"value":11268},"は必須レベルで便利。Undo時は逆順に回す",{"type":25,"tag":5239,"props":11270,"children":11271},{},[11272],{"type":31,"value":5243},{"title":8,"searchDepth":1149,"depth":1149,"links":11274},[11275,11276,11277,11278,11279,11280,11281],{"id":28,"depth":1149,"text":28},{"id":8248,"depth":1149,"text":8251},{"id":8271,"depth":1149,"text":8274},{"id":9017,"depth":1149,"text":9020},{"id":10003,"depth":1149,"text":10006},{"id":10426,"depth":1149,"text":10429},{"id":1746,"depth":1149,"text":1746},"content:articles:tech:blazor:undo-redo-command-pattern.md","articles/tech/blazor/undo-redo-command-pattern.md","articles/tech/blazor/undo-redo-command-pattern",{"_path":11286,"_dir":11287,"_draft":7,"_partial":7,"_locale":8,"title":11288,"description":11289,"date":11290,"tags":11291,"rowTypeId":18,"sitemap":11297,"body":11298,"_type":1180,"_id":11659,"_source":1182,"_file":11660,"_stem":11661,"_extension":1185},"/articles/tech/development/adsense-azure-swa","development","Azure Static Web Apps × Google AdSenseで所有権確認を通すまでにハマったこと","Nuxt SSGで構築したAzure Static Web AppsのサイトにGoogle AdSenseを導入しようとして、所有権確認でハマったポイントと解決策をまとめます。","2026-03-22",[11292,11293,11294,11295,11296],"Azure Static Web Apps","Google AdSense","Nuxt","DNS","Cloudflare",{"loc":11286,"lastmod":11290,"priority":18},{"type":22,"children":11299,"toc":11648},[11300,11304,11309,11317,11322,11400,11406,11411,11446,11451,11470,11476,11481,11487,11499,11504,11585,11589,11594,11643],{"type":25,"tag":26,"props":11301,"children":11302},{"id":28},[11303],{"type":31,"value":28},{"type":25,"tag":33,"props":11305,"children":11306},{},[11307],{"type":31,"value":11308},"Nuxt（SSG）+ Azure Static Web Apps（以下ASWA）で運用しているサイトにGoogle AdSenseを導入しようとしたところ、所有権確認がなかなか通らず苦労しました。\nこの記事では、ハマったポイントとその解決策をまとめます。同じ構成で困っている方の参考になれば幸いです。",{"type":25,"tag":39,"props":11310,"children":11311},{},[11312],{"type":25,"tag":33,"props":11313,"children":11314},{},[11315],{"type":31,"value":11316},"Azure Static Web Apps + Nuxt SSGの構成でAdSense所有権確認を通すために、ads.txtの配信とルートドメインのDNS設定（Cloudflare）のハマりポイントを乗り越えた記録です。",{"type":25,"tag":26,"props":11318,"children":11320},{"id":11319},"環境",[11321],{"type":31,"value":11319},{"type":25,"tag":825,"props":11323,"children":11324},{},[11325,11340],{"type":25,"tag":829,"props":11326,"children":11327},{},[11328],{"type":25,"tag":833,"props":11329,"children":11330},{},[11331,11335],{"type":25,"tag":837,"props":11332,"children":11333},{},[11334],{"type":31,"value":841},{"type":25,"tag":837,"props":11336,"children":11337},{},[11338],{"type":31,"value":11339},"技術",{"type":25,"tag":848,"props":11341,"children":11342},{},[11343,11356,11368,11380],{"type":25,"tag":833,"props":11344,"children":11345},{},[11346,11351],{"type":25,"tag":855,"props":11347,"children":11348},{},[11349],{"type":31,"value":11350},"フレームワーク",{"type":25,"tag":855,"props":11352,"children":11353},{},[11354],{"type":31,"value":11355},"Nuxt 4（SSG）",{"type":25,"tag":833,"props":11357,"children":11358},{},[11359,11364],{"type":25,"tag":855,"props":11360,"children":11361},{},[11362],{"type":31,"value":11363},"ホスティング",{"type":25,"tag":855,"props":11365,"children":11366},{},[11367],{"type":31,"value":11292},{"type":25,"tag":833,"props":11369,"children":11370},{},[11371,11375],{"type":25,"tag":855,"props":11372,"children":11373},{},[11374],{"type":31,"value":11295},{"type":25,"tag":855,"props":11376,"children":11377},{},[11378],{"type":31,"value":11379},"お名前.com → Cloudflare に移行",{"type":25,"tag":833,"props":11381,"children":11382},{},[11383,11388],{"type":25,"tag":855,"props":11384,"children":11385},{},[11386],{"type":31,"value":11387},"ドメイン",{"type":25,"tag":855,"props":11389,"children":11390},{},[11391,11393],{"type":31,"value":11392},"akizorasoft.com / ",{"type":25,"tag":1354,"props":11394,"children":11397},{"href":11395,"rel":11396},"http://www.akizorasoft.com",[1358],[11398],{"type":31,"value":11399},"www.akizorasoft.com",{"type":25,"tag":26,"props":11401,"children":11403},{"id":11402},"ハマりポイント-ルートドメインにアクセスできない",[11404],{"type":31,"value":11405},"ハマりポイント: ルートドメインにアクセスできない",{"type":25,"tag":313,"props":11407,"children":11409},{"id":11408},"問題",[11410],{"type":31,"value":11408},{"type":25,"tag":33,"props":11412,"children":11413},{},[11414,11416,11422,11424,11429,11431,11436,11438,11444],{"type":31,"value":11415},"AdSenseに登録できるのは",{"type":25,"tag":529,"props":11417,"children":11419},{"className":11418},[],[11420],{"type":31,"value":11421},"akizorasoft.com",{"type":31,"value":11423},"（ルートドメイン）のみで、",{"type":25,"tag":529,"props":11425,"children":11427},{"className":11426},[],[11428],{"type":31,"value":11399},{"type":31,"value":11430},"は登録できない。しかし、",{"type":25,"tag":529,"props":11432,"children":11434},{"className":11433},[],[11435],{"type":31,"value":11421},{"type":31,"value":11437},"にはDNSレコードが設定されておらず、Googleのクローラーがサイトにアクセスできなかった。\nこれにより、",{"type":25,"tag":529,"props":11439,"children":11441},{"className":11440},[],[11442],{"type":31,"value":11443},"ads.txt",{"type":31,"value":11445},"もルートドメインで配信できず、所有権確認が通らない状態だった。",{"type":25,"tag":313,"props":11447,"children":11449},{"id":11448},"原因",[11450],{"type":31,"value":11448},{"type":25,"tag":33,"props":11452,"children":11453},{},[11454,11456,11461,11463,11468],{"type":31,"value":11455},"お名前.comでは",{"type":25,"tag":529,"props":11457,"children":11459},{"className":11458},[],[11460],{"type":31,"value":11399},{"type":31,"value":11462},"のCNAMEレコードのみ設定しており、ルートドメイン（",{"type":25,"tag":529,"props":11464,"children":11466},{"className":11465},[],[11467],{"type":31,"value":11421},{"type":31,"value":11469},"）にはレコードがなかった。\n通常、ルートドメインにはCNAMEレコードを設定できない（DNS仕様上の制約）ため、ASWAへのルーティングができなかった。",{"type":25,"tag":313,"props":11471,"children":11473},{"id":11472},"なぜルートドメインにcnameが設定できないのか",[11474],{"type":31,"value":11475},"なぜルートドメインにCNAMEが設定できないのか",{"type":25,"tag":33,"props":11477,"children":11478},{},[11479],{"type":31,"value":11480},"DNS仕様（RFC 1034）では、ルートドメイン（Zone Apex）にはSOAレコードやNSレコードが必須であり、CNAMEレコードは他のレコードと共存できないという制約がある。そのため、ルートドメインにCNAMEを設定すると、これらの必須レコードと競合してしまう。",{"type":25,"tag":313,"props":11482,"children":11484},{"id":11483},"解決策-cloudflareへの移行",[11485],{"type":31,"value":11486},"解決策: Cloudflareへの移行",{"type":25,"tag":33,"props":11488,"children":11489},{},[11490,11492,11497],{"type":31,"value":11491},"Cloudflare（無料プラン）にDNSを移行し、",{"type":25,"tag":103,"props":11493,"children":11494},{},[11495],{"type":31,"value":11496},"CNAME Flattening",{"type":31,"value":11498},"を利用した。\nCNAME Flatteningは、ルートドメインのCNAMEレコードを内部的にAレコードに変換してレスポンスを返す仕組みで、DNS仕様の制約を回避できる。",{"type":25,"tag":335,"props":11500,"children":11502},{"id":11501},"手順",[11503],{"type":31,"value":11501},{"type":25,"tag":198,"props":11505,"children":11506},{},[11507,11519,11524,11559,11570,11575],{"type":25,"tag":57,"props":11508,"children":11509},{},[11510,11512,11517],{"type":31,"value":11511},"Cloudflareにアカウントを作成し、",{"type":25,"tag":529,"props":11513,"children":11515},{"className":11514},[],[11516],{"type":31,"value":11421},{"type":31,"value":11518},"を追加",{"type":25,"tag":57,"props":11520,"children":11521},{},[11522],{"type":31,"value":11523},"お名前.comのネームサーバーをCloudflare指定のNSに変更",{"type":25,"tag":57,"props":11525,"children":11526},{},[11527,11529],{"type":31,"value":11528},"Cloudflare側でDNSレコードを設定\n",{"type":25,"tag":53,"props":11530,"children":11531},{},[11532,11543,11554],{"type":25,"tag":57,"props":11533,"children":11534},{},[11535,11541],{"type":25,"tag":529,"props":11536,"children":11538},{"className":11537},[],[11539],{"type":31,"value":11540},"@",{"type":31,"value":11542},"（ルートドメイン）→ CNAME → ASWAのホスト名",{"type":25,"tag":57,"props":11544,"children":11545},{},[11546,11552],{"type":25,"tag":529,"props":11547,"children":11549},{"className":11548},[],[11550],{"type":31,"value":11551},"www",{"type":31,"value":11553}," → CNAME → ASWAのホスト名",{"type":25,"tag":57,"props":11555,"children":11556},{},[11557],{"type":31,"value":11558},"TXT → google-site-verification の値を移行",{"type":25,"tag":57,"props":11560,"children":11561},{},[11562,11564,11569],{"type":31,"value":11563},"ASWAのカスタムドメインに",{"type":25,"tag":529,"props":11565,"children":11567},{"className":11566},[],[11568],{"type":31,"value":11421},{"type":31,"value":11518},{"type":25,"tag":57,"props":11571,"children":11572},{},[11573],{"type":31,"value":11574},"ASWA側でドメイン検証用のTXTレコードをCloudflareに追加",{"type":25,"tag":57,"props":11576,"children":11577},{},[11578,11580],{"type":31,"value":11579},"検証完了後、AdSenseの所有権確認を再試行 → ",{"type":25,"tag":103,"props":11581,"children":11582},{},[11583],{"type":31,"value":11584},"成功",{"type":25,"tag":26,"props":11586,"children":11587},{"id":1746},[11588],{"type":31,"value":1746},{"type":25,"tag":33,"props":11590,"children":11591},{},[11592],{"type":31,"value":11593},"Azure Static Web Apps + Nuxt SSGの構成でAdSenseの所有権確認を通すためのポイントです。",{"type":25,"tag":53,"props":11595,"children":11596},{},[11597,11617,11630],{"type":25,"tag":57,"props":11598,"children":11599},{},[11600,11605,11608,11610,11615],{"type":25,"tag":103,"props":11601,"children":11602},{},[11603],{"type":31,"value":11604},"ルートドメインのDNS設定が最重要",{"type":25,"tag":745,"props":11606,"children":11607},{},[],{"type":31,"value":11609},"\nAdSenseはルートドメインでの登録が必須で、",{"type":25,"tag":529,"props":11611,"children":11613},{"className":11612},[],[11614],{"type":31,"value":11551},{"type":31,"value":11616},"サブドメインだけでは不可",{"type":25,"tag":57,"props":11618,"children":11619},{},[11620,11625,11628],{"type":25,"tag":103,"props":11621,"children":11622},{},[11623],{"type":31,"value":11624},"CNAME Flatteningで制約を回避",{"type":25,"tag":745,"props":11626,"children":11627},{},[],{"type":31,"value":11629},"\nCloudflare（無料プラン）でルートドメインにCNAMEを設定できる",{"type":25,"tag":57,"props":11631,"children":11632},{},[11633,11638,11641],{"type":25,"tag":103,"props":11634,"children":11635},{},[11636],{"type":31,"value":11637},"ads.txtもルートドメインで配信が必要",{"type":25,"tag":745,"props":11639,"children":11640},{},[],{"type":31,"value":11642},"\nルートドメインにアクセスできなければads.txtも到達不可",{"type":25,"tag":33,"props":11644,"children":11645},{},[11646],{"type":31,"value":11647},"ルートドメインの問題はASWAに限らずどのホスティングサービスでも起こりうるため、同様の問題に直面した方はぜひCloudflareを試してみてください。",{"title":8,"searchDepth":1149,"depth":1149,"links":11649},[11650,11651,11652,11658],{"id":28,"depth":1149,"text":28},{"id":11319,"depth":1149,"text":11319},{"id":11402,"depth":1149,"text":11405,"children":11653},[11654,11655,11656,11657],{"id":11408,"depth":1159,"text":11408},{"id":11448,"depth":1159,"text":11448},{"id":11472,"depth":1159,"text":11475},{"id":11483,"depth":1159,"text":11486},{"id":1746,"depth":1149,"text":1746},"content:articles:tech:development:adsense-azure-swa.md","articles/tech/development/adsense-azure-swa.md","articles/tech/development/adsense-azure-swa",{"_path":11663,"_dir":11287,"_draft":7,"_partial":7,"_locale":8,"title":11664,"description":11665,"date":11290,"tags":11666,"rowTypeId":18,"sitemap":11670,"body":11671,"_type":1180,"_id":11969,"_source":1182,"_file":11970,"_stem":11971,"_extension":1185},"/articles/tech/development/oss-dogfooding","OSSを公開したら使いにくかった話｜ドッグフーディングで改善する","OSSライブラリ「SoundMaker」の開発を通じて、自分で使い倒す「ドッグフーディング」の重要性を痛感しました。利用者視点を理解しているつもりでも、実際に使ってみないと気づけない問題があるという体験談です。",[11667,11668,1805,11669],"OSS","個人開発","SoundMaker",{"loc":11663,"lastmod":11290,"priority":18},{"type":22,"children":11672,"toc":11952},[11673,11677,11682,11687,11695,11701,11706,11713,11718,11731,11737,11743,11756,11761,11765,11770,11778,11783,11789,11794,11807,11812,11824,11829,11834,11839,11844,11849,11854,11859,11894,11900,11905,11910,11916,11921,11926,11930,11934],{"type":25,"tag":26,"props":11674,"children":11675},{"id":28},[11676],{"type":31,"value":28},{"type":25,"tag":33,"props":11678,"children":11679},{},[11680],{"type":31,"value":11681},"「利用者の視点に立つことが大事」",{"type":25,"tag":33,"props":11683,"children":11684},{},[11685],{"type":31,"value":11686},"これは誰もが理解していると思います。そして、自分は利用者の気持ちを理解していると考えがちだと思います。この記事は、実際は全然理解していなかったということを反省した記事となります。",{"type":25,"tag":39,"props":11688,"children":11689},{},[11690],{"type":25,"tag":33,"props":11691,"children":11692},{},[11693],{"type":31,"value":11694},"OSSライブラリ「SoundMaker」の開発を通じて、自分で使い倒す「ドッグフーディング」の重要性を痛感しました。利用者視点を理解しているつもりでも、実際に使ってみないと気づけない問題があります。テストでは見つからない使い勝手の問題を発見するためには、開発者自身が一番のユーザーになることが大切です。",{"type":25,"tag":26,"props":11696,"children":11698},{"id":11697},"開発中のoss",[11699],{"type":31,"value":11700},"開発中のOSS",{"type":25,"tag":33,"props":11702,"children":11703},{},[11704],{"type":31,"value":11705},"C#でチップチューンサウンド（昔のゲームのようなピコピコするサウンド）を生成するためのライブラリ、「SoundMaker」を開発しています。",{"type":25,"tag":11707,"props":11708,"children":11712},"external-link-card",{"to":11709,"description":11710,"image":11711,"title":11669},"https://github.com/AutumnSky1010/SoundMaker","8bit風サウンドを簡単に作成できる.NETライブラリ","https://opengraph.githubassets.com/1/AutumnSky1010/SoundMaker",[],{"type":25,"tag":313,"props":11714,"children":11716},{"id":11715},"なぜ作ろうと思ったのか",[11717],{"type":31,"value":11715},{"type":25,"tag":33,"props":11719,"children":11720},{},[11721,11723,11729],{"type":31,"value":11722},"高校生の時に基本情報技術者試験の勉強をしていた際、",{"type":25,"tag":633,"props":11724,"children":11726},{"content":11725},"音声波形をそのままデジタル数値に変換する方式。WAVファイルなどで使われる",[11727],{"type":31,"value":11728},"リニアPCM",{"type":31,"value":11730},"の仕組みを学びました。そこで、息抜きを兼ねて音を生成してWAVファイルで書き出せるプログラムを書いてみよう！となり、爆誕しました。",{"type":25,"tag":26,"props":11732,"children":11734},{"id":11733},"致命的やらかしポイント使いづらくね",[11735],{"type":31,"value":11736},"致命的やらかしポイント：使いづらくね？",{"type":25,"tag":313,"props":11738,"children":11740},{"id":11739},"使いづらポイント初期バージョンはファイル出力のみに対応",[11741],{"type":31,"value":11742},"使いづらポイント①：初期バージョンはファイル出力のみに対応",{"type":25,"tag":33,"props":11744,"children":11745},{},[11746,11748,11754],{"type":31,"value":11747},"何故か当時は",{"type":25,"tag":633,"props":11749,"children":11751},{"content":11750},"ファイルやネットワークなどへデータを順次読み書きするための仕組み。メモリ上で直接扱えるため、ファイル保存を経由せずに処理できる",[11752],{"type":31,"value":11753},"Stream",{"type":31,"value":11755},"への出力に未対応。利用者の方に指摘頂いて初めて修正しました。",{"type":25,"tag":33,"props":11757,"children":11758},{},[11759],{"type":31,"value":11760},"以下のリンクはStream未対応を指摘してくださったQiitaの記事です。",{"type":25,"tag":11707,"props":11762,"children":11764},{"to":11763},"https://qiita.com/jsakamoto/items/67d089cd95a35e8bc1d8#%E5%AE%9F%E8%A3%85%E3%81%97%E3%81%A6%E3%81%BF%E3%82%8B",[],{"type":25,"tag":33,"props":11766,"children":11767},{},[11768],{"type":31,"value":11769},"なんと、このライブラリはニーズなどを一切考慮せず",{"type":25,"tag":971,"props":11771,"children":11772},{},[11773],{"type":25,"tag":33,"props":11774,"children":11775},{},[11776],{"type":31,"value":11777},"完成したからとりあえず公開しちゃえ",{"type":25,"tag":33,"props":11779,"children":11780},{},[11781],{"type":31,"value":11782},"というノリで公開したのです。今回は「運が良く」指摘いただきましたが、もしこの指摘がなければ、長い間使いづらい状態で放置することになっていたと思います。",{"type":25,"tag":313,"props":11784,"children":11786},{"id":11785},"使いづらポイント楽譜をそのままデータ構造にしてしまった",[11787],{"type":31,"value":11788},"使いづらポイント②：楽譜をそのままデータ構造にしてしまった",{"type":25,"tag":33,"props":11790,"children":11791},{},[11792],{"type":31,"value":11793},"ファイル出力の問題は指摘で気づけました。では、ドッグフーディングで自力で見つけた問題は何か。それが楽譜構造の問題でした。",{"type":25,"tag":33,"props":11795,"children":11796},{},[11797,11799,11805],{"type":31,"value":11798},"見つけた内容は、音を鳴らさない時間を作るために必ず休符を入れる必要がある仕組みにしてしまったという内容です。つまり、本来なら「4小節目から音を鳴らす」と時間を直接指定したいのに、そこまでの空白をわざわざ休符データで埋めないといけない構造になっていました。このような構造では、",{"type":25,"tag":633,"props":11800,"children":11802},{"content":11801},"Digital Audio Workstation: 音楽制作ソフトウェアの総称。任意のタイミングで音を配置・編集できる",[11803],{"type":31,"value":11804},"DAW",{"type":31,"value":11806},"ソフトのようなユースケースに対応できません。",{"type":25,"tag":26,"props":11808,"children":11810},{"id":11809},"自分でライブラリを使い倒すことが重要",[11811],{"type":31,"value":11809},{"type":25,"tag":33,"props":11813,"children":11814},{},[11815,11817,11822],{"type":31,"value":11816},"これがこの記事の主旨となります。実際に使い倒さなければわからなかったということです。\nこのように、自分(達)の製品を自分(達)で使い倒すことを ",{"type":25,"tag":103,"props":11818,"children":11819},{},[11820],{"type":31,"value":11821},"ドッグフーディング",{"type":31,"value":11823}," と言います。",{"type":25,"tag":313,"props":11825,"children":11827},{"id":11826},"経緯",[11828],{"type":31,"value":11826},{"type":25,"tag":33,"props":11830,"children":11831},{},[11832],{"type":31,"value":11833},"気軽にチップチューン音楽を作成できるWEBアプリ「PICOM」を開発し始めました。PICOMの音声生成エンジンとして、自作したSoundMakerを組み込んだ流れです。",{"type":25,"tag":33,"props":11835,"children":11836},{},[11837],{"type":31,"value":11838},"下記動画はPICOMの宣伝動画です。動画内BGMは実際にPICOMを利用して作成したもので、チップチューンのイメージがわかない方は、どのような音楽か聞いてみてください。",{"type":25,"tag":11840,"props":11841,"children":11843},"youtube",{"id":11842},"F09hLEyYo_4",[],{"type":25,"tag":33,"props":11845,"children":11846},{},[11847],{"type":31,"value":11848},"このアプリにピアノロールを実装する際に、「このAPIでピアノロール実装無理じゃね？」となったのが経緯です。",{"type":25,"tag":313,"props":11850,"children":11852},{"id":11851},"自分で使い始めて変わったこと",[11853],{"type":31,"value":11851},{"type":25,"tag":33,"props":11855,"children":11856},{},[11857],{"type":31,"value":11858},"これをするだけでなんでこんな手順を踏まないといけないんだ？みたいな改善点が見つかりやすくなったことが一番のメリットだと思います。一応結合テストでAPIのテストはしていましたが、このテストはそのAPIが存在していることが前提なので、こういったことに気づくことができませんでした。",{"type":25,"tag":33,"props":11860,"children":11861},{},[11862,11864,11873,11874,11884,11886,11892],{"type":31,"value":11863},"また、ライブラリ開発時は内部の",{"type":25,"tag":633,"props":11865,"children":11867},{"content":11866},"そのクラス内からしかアクセスできないアクセス修飾子",[11868],{"type":25,"tag":529,"props":11869,"children":11871},{"className":11870},[],[11872],{"type":31,"value":4165},{"type":31,"value":10415},{"type":25,"tag":633,"props":11875,"children":11877},{"content":11876},"同じプロジェクト（アセンブリ）内からのみアクセスできるアクセス修飾子",[11878],{"type":25,"tag":529,"props":11879,"children":11881},{"className":11880},[],[11882],{"type":31,"value":11883},"internal",{"type":31,"value":11885},"なプロパティ、メソッドを確認できます。しかし、利用する際は",{"type":25,"tag":633,"props":11887,"children":11889},{"content":11888},"ライブラリが外部に公開しているクラスやメソッドの集合。利用者はこれだけを使ってプログラムを書く",[11890],{"type":31,"value":11891},"公開API",{"type":31,"value":11893},"のみを利用できます。この時に初めて「このプロパティを外部に公開して欲しいな」とかにも気づくことができました。",{"type":25,"tag":313,"props":11895,"children":11897},{"id":11896},"とはいえ実際は多くの人に利用してもらえたほうが楽では",[11898],{"type":31,"value":11899},"とはいえ、実際は多くの人に利用してもらえたほうが楽では？",{"type":25,"tag":33,"props":11901,"children":11902},{},[11903],{"type":31,"value":11904},"ほぼ自分だけが使っていたら自分のユースケースに特化してしまうというのは仕方がない側面もあります。ただ、この記事ではそういうことを言いたいのではなく、開発者サイドで利用者視点に立つためには実際に使い倒すことが大事だと言いたいです。できることなら、利用者には「使いやすい」とだけ感じられることが理想です。利用者からするとFBすることもコストです。",{"type":25,"tag":33,"props":11906,"children":11907},{},[11908],{"type":31,"value":11909},"そして、これは二者択一の問題ではなく、両取りすることで相互作用を生み出せると思います。「利用者からのFB✖️自分のFB」を組み合わせることで、改善点が見つかりやすくなるのだと感じました。",{"type":25,"tag":26,"props":11911,"children":11913},{"id":11912},"ossに限らず個人開発全般に言えること",[11914],{"type":31,"value":11915},"OSSに限らず個人開発全般に言えること",{"type":25,"tag":33,"props":11917,"children":11918},{},[11919],{"type":31,"value":11920},"ドッグフーディングの考え方はOSSに限った話ではなく、個人開発全般に当てはまります。自分で作ったものを日常的に使い続けることで、「動くけど使いにくい」部分が自然と浮き彫りになります。",{"type":25,"tag":33,"props":11922,"children":11923},{},[11924],{"type":31,"value":11925},"例えば、私がClaude Codeを活用して開発した家計管理アプリ「Portal」でも、実際に毎日の家計管理に使い始めてから多くの改善点に気づきました。AIエージェントに実装を任せれば「動くもの」は簡単に作れますが、使い勝手の良し悪しは使ってみないとわかりません。",{"type":25,"tag":417,"props":11927,"children":11929},{"label":11928,"to":420},"Portalの開発についてはこちら⬇️",[],{"type":25,"tag":26,"props":11931,"children":11932},{"id":1746},[11933],{"type":31,"value":1746},{"type":25,"tag":53,"props":11935,"children":11936},{},[11937,11942,11947],{"type":25,"tag":57,"props":11938,"children":11939},{},[11940],{"type":31,"value":11941},"「利用者の視点に立つ」ことは、頭で理解しているだけでは不十分で、実際に自分で使い倒して初めて本当の課題が見える",{"type":25,"tag":57,"props":11943,"children":11944},{},[11945],{"type":31,"value":11946},"自動テストは正確性を証明できても、使いやすさは証明してくれない。ユースケースを実際に動かして初めてわかる問題がある",{"type":25,"tag":57,"props":11948,"children":11949},{},[11950],{"type":31,"value":11951},"ドッグフーディングと利用者からのフィードバックは二者択一ではなく、両方を組み合わせることで改善の質が上がる",{"title":8,"searchDepth":1149,"depth":1149,"links":11953},[11954,11955,11958,11962,11967,11968],{"id":28,"depth":1149,"text":28},{"id":11697,"depth":1149,"text":11700,"children":11956},[11957],{"id":11715,"depth":1159,"text":11715},{"id":11733,"depth":1149,"text":11736,"children":11959},[11960,11961],{"id":11739,"depth":1159,"text":11742},{"id":11785,"depth":1159,"text":11788},{"id":11809,"depth":1149,"text":11809,"children":11963},[11964,11965,11966],{"id":11826,"depth":1159,"text":11826},{"id":11851,"depth":1159,"text":11851},{"id":11896,"depth":1159,"text":11899},{"id":11912,"depth":1149,"text":11915},{"id":1746,"depth":1149,"text":1746},"content:articles:tech:development:oss-dogfooding.md","articles/tech/development/oss-dogfooding.md","articles/tech/development/oss-dogfooding",{"_path":420,"_dir":11287,"_draft":7,"_partial":7,"_locale":8,"title":11973,"description":11974,"date":11975,"tags":11976,"rowTypeId":18,"sitemap":11980,"body":11981,"_type":1180,"_id":12665,"_source":1182,"_file":12666,"_stem":12667,"_extension":1185},"AIで個人開発の価値は死んだのか？","Claude Codeを活用して家計管理PWAを約1週間で開発した体験をもとに、AIで爆速開発できる時代における個人開発の価値や、エンジニアとして力を入れるべきポイントについて考えます。","2026-03-19",[11977,11978,11668,11979,16],"Claude Code","AI","Portal",{"loc":420,"lastmod":11975,"priority":18},{"type":22,"children":11982,"toc":12638},[11983,11987,11992,12000,12006,12012,12017,12023,12028,12033,12117,12122,12127,12195,12223,12229,12234,12239,12244,12344,12349,12354,12359,12365,12370,12375,12381,12386,12391,12401,12407,12412,12417,12422,12428,12433,12438,12443,12448,12453,12458,12463,12468,12473,12479,12484,12492,12510,12518,12553,12572,12577,12582,12587,12599,12611,12615],{"type":25,"tag":26,"props":11984,"children":11985},{"id":28},[11986],{"type":31,"value":28},{"type":25,"tag":33,"props":11988,"children":11989},{},[11990],{"type":31,"value":11991},"以前、AIで資産管理アプリをClaude Codeで爆速開発し、個人開発のハードルが劇的に下がっていると強く感じました。\nハードルが下がるということは、「多少知識があれば誰でもできる」ということではないでしょうか。そうであれば、単純に個人開発しているだけでは技術的にもキャリア的にも効果的ではないのでは？と考えるようになりました。\nそこで、この開発経験から、今後個人開発へどのように向き合うかの考察をこの記事では紹介します。\nこれから就活を迎える学生や、ポートフォリオを作ろうとしている駆け出しエンジニアに向けて、一つの視点を提供できれば幸いです。",{"type":25,"tag":39,"props":11993,"children":11994},{},[11995],{"type":25,"tag":33,"props":11996,"children":11997},{},[11998],{"type":31,"value":11999},"Claude Codeで家計管理PWAを1週間で開発した体験をもとに、AIによる爆速開発時代の個人開発の価値と、エンジニアとして注力すべきポイントについて考えました。結果、エンジニアに求められる本質的なスキルは変わっていないと考えました。",{"type":25,"tag":26,"props":12001,"children":12003},{"id":12002},"完成したアプリportal",[12004],{"type":31,"value":12005},"完成したアプリ「Portal」",{"type":25,"tag":313,"props":12007,"children":12009},{"id":12008},"そもそもなぜ作ろうと思ったのか",[12010],{"type":31,"value":12011},"そもそもなぜ作ろうと思ったのか？",{"type":25,"tag":33,"props":12013,"children":12014},{},[12015],{"type":31,"value":12016},"市販のアプリでは、私のように複数のプリペイドや決済サービスを利用している場合に対応しきれないからです。\nそこで、Claude Codeを利用した半自動開発をしてみたいと思っていたのでやってみようということになりました。",{"type":25,"tag":313,"props":12018,"children":12020},{"id":12019},"方針できる限りサボる極端なくらいにサボる",[12021],{"type":31,"value":12022},"方針：できる限りサボる。極端なくらいにサボる",{"type":25,"tag":33,"props":12024,"children":12025},{},[12026],{"type":31,"value":12027},"基本的にこの方針で進めました。現状、どの程度までは自動で進めて、どの程度からは人間が介入しないといけないかのバランスを掴むためです。",{"type":25,"tag":313,"props":12029,"children":12031},{"id":12030},"技術スタック",[12032],{"type":31,"value":12030},{"type":25,"tag":825,"props":12034,"children":12035},{},[12036,12051],{"type":25,"tag":829,"props":12037,"children":12038},{},[12039],{"type":25,"tag":833,"props":12040,"children":12041},{},[12042,12047],{"type":25,"tag":837,"props":12043,"children":12044},{},[12045],{"type":31,"value":12046},"レイヤー",{"type":25,"tag":837,"props":12048,"children":12049},{},[12050],{"type":31,"value":11339},{"type":25,"tag":848,"props":12052,"children":12053},{},[12054,12066,12079,12092,12105],{"type":25,"tag":833,"props":12055,"children":12056},{},[12057,12061],{"type":25,"tag":855,"props":12058,"children":12059},{},[12060],{"type":31,"value":11350},{"type":25,"tag":855,"props":12062,"children":12063},{},[12064],{"type":31,"value":12065},"Nuxt 4（Vue 3）",{"type":25,"tag":833,"props":12067,"children":12068},{},[12069,12074],{"type":25,"tag":855,"props":12070,"children":12071},{},[12072],{"type":31,"value":12073},"言語",{"type":25,"tag":855,"props":12075,"children":12076},{},[12077],{"type":31,"value":12078},"TypeScript",{"type":25,"tag":833,"props":12080,"children":12081},{},[12082,12087],{"type":25,"tag":855,"props":12083,"children":12084},{},[12085],{"type":31,"value":12086},"スタイリング",{"type":25,"tag":855,"props":12088,"children":12089},{},[12090],{"type":31,"value":12091},"Tailwind CSS + Headless UI",{"type":25,"tag":833,"props":12093,"children":12094},{},[12095,12100],{"type":25,"tag":855,"props":12096,"children":12097},{},[12098],{"type":31,"value":12099},"バックエンド",{"type":25,"tag":855,"props":12101,"children":12102},{},[12103],{"type":31,"value":12104},"Firebase（Auth + Firestore）",{"type":25,"tag":833,"props":12106,"children":12107},{},[12108,12112],{"type":25,"tag":855,"props":12109,"children":12110},{},[12111],{"type":31,"value":11363},{"type":25,"tag":855,"props":12113,"children":12114},{},[12115],{"type":31,"value":12116},"Firebase Hosting",{"type":25,"tag":33,"props":12118,"children":12119},{},[12120],{"type":31,"value":12121},"上記構成となっています。今回は認証基盤、データ基盤を簡単に構築したかったため、Firebaseを利用しています。こうすればクロスプラットフォームな実装が容易ですし、特定のGoogleアカウントでログインしなければ利用できないようにするような実装が簡単にできます。",{"type":25,"tag":313,"props":12123,"children":12125},{"id":12124},"主な機能",[12126],{"type":31,"value":12124},{"type":25,"tag":198,"props":12128,"children":12129},{},[12130,12143,12156,12169,12182],{"type":25,"tag":57,"props":12131,"children":12132},{},[12133,12138,12141],{"type":25,"tag":103,"props":12134,"children":12135},{},[12136],{"type":31,"value":12137},"ダッシュボード",{"type":25,"tag":745,"props":12139,"children":12140},{},[],{"type":31,"value":12142},"\n純資産推移・予算消化率・カテゴリ別支出・資産負債の内訳・貯蓄率を一画面に集約",{"type":25,"tag":57,"props":12144,"children":12145},{},[12146,12151,12154],{"type":25,"tag":103,"props":12147,"children":12148},{},[12149],{"type":31,"value":12150},"CSV一括インポート",{"type":25,"tag":745,"props":12152,"children":12153},{},[],{"type":31,"value":12155},"\n銀行・クレジットカード・電子マネー・証券口座・店舗プリペイドなど6種類のCSVをドラッグ&ドロップでインポート。エンコーディング検出・フォーマット自動判定・カテゴリ推論・重複排除まで自動",{"type":25,"tag":57,"props":12157,"children":12158},{},[12159,12164,12167],{"type":25,"tag":103,"props":12160,"children":12161},{},[12162],{"type":31,"value":12163},"予算管理",{"type":25,"tag":745,"props":12165,"children":12166},{},[],{"type":31,"value":12168},"\nカテゴリごとの月次予算設定と消化状況の可視化",{"type":25,"tag":57,"props":12170,"children":12171},{},[12172,12177,12180],{"type":25,"tag":103,"props":12173,"children":12174},{},[12175],{"type":31,"value":12176},"AIレポート",{"type":25,"tag":745,"props":12178,"children":12179},{},[],{"type":31,"value":12181},"\nBS・PL・予算乖離・前月比較・投資成績を含むMarkdownをワンクリック生成。Claude や ChatGPT にそのまま貼り付けて分析できる",{"type":25,"tag":57,"props":12183,"children":12184},{},[12185,12190,12193],{"type":25,"tag":103,"props":12186,"children":12187},{},[12188],{"type":31,"value":12189},"月次締め",{"type":25,"tag":745,"props":12191,"children":12192},{},[],{"type":31,"value":12194},"\n資産・負債・純資産のスナップショットを保存し、過去データを凍結",{"type":25,"tag":33,"props":12196,"children":12197},{},[12198,12200,12203,12208,12211,12213,12218,12221],{"type":31,"value":12199},"一部機能のデモ画像を紹介します。",{"type":25,"tag":745,"props":12201,"children":12202},{},[],{"type":25,"tag":739,"props":12204,"children":12207},{"alt":12205,"src":12206},"budget.png","/images/articles/tech/development/budget.png",[],{"type":25,"tag":745,"props":12209,"children":12210},{},[],{"type":31,"value":12212},"\n⬆️ 予算画面\n",{"type":25,"tag":739,"props":12214,"children":12217},{"alt":12215,"src":12216},"ai-report.png","/images/articles/tech/development/ai-report.png",[],{"type":25,"tag":745,"props":12219,"children":12220},{},[],{"type":31,"value":12222},"\n⬆️ AIレポート画面",{"type":25,"tag":26,"props":12224,"children":12226},{"id":12225},"claude-codeで開発を進める利点",[12227],{"type":31,"value":12228},"Claude Codeで開発を進める利点",{"type":25,"tag":33,"props":12230,"children":12231},{},[12232],{"type":31,"value":12233},"これだけの多機能なアプリを、今回はほぼAIエージェントに実装を任せて作り上げました。ここからは、その具体的な開発プロセスと利点について触れます。",{"type":25,"tag":313,"props":12235,"children":12237},{"id":12236},"開発スピード",[12238],{"type":31,"value":12236},{"type":25,"tag":33,"props":12240,"children":12241},{},[12242],{"type":31,"value":12243},"土日＋平日の空き時間で、以下の規模のアプリを実装できました。",{"type":25,"tag":825,"props":12245,"children":12246},{},[12247,12263],{"type":25,"tag":829,"props":12248,"children":12249},{},[12250],{"type":25,"tag":833,"props":12251,"children":12252},{},[12253,12258],{"type":25,"tag":837,"props":12254,"children":12255},{},[12256],{"type":31,"value":12257},"成果物",{"type":25,"tag":837,"props":12259,"children":12260},{},[12261],{"type":31,"value":12262},"数量",{"type":25,"tag":848,"props":12264,"children":12265},{},[12266,12279,12292,12305,12318,12331],{"type":25,"tag":833,"props":12267,"children":12268},{},[12269,12274],{"type":25,"tag":855,"props":12270,"children":12271},{},[12272],{"type":31,"value":12273},"Composable（ビジネスロジック）",{"type":25,"tag":855,"props":12275,"children":12276},{},[12277],{"type":31,"value":12278},"24個",{"type":25,"tag":833,"props":12280,"children":12281},{},[12282,12287],{"type":25,"tag":855,"props":12283,"children":12284},{},[12285],{"type":31,"value":12286},"ページ",{"type":25,"tag":855,"props":12288,"children":12289},{},[12290],{"type":31,"value":12291},"10画面",{"type":25,"tag":833,"props":12293,"children":12294},{},[12295,12300],{"type":25,"tag":855,"props":12296,"children":12297},{},[12298],{"type":31,"value":12299},"コンポーネント",{"type":25,"tag":855,"props":12301,"children":12302},{},[12303],{"type":31,"value":12304},"28個",{"type":25,"tag":833,"props":12306,"children":12307},{},[12308,12313],{"type":25,"tag":855,"props":12309,"children":12310},{},[12311],{"type":31,"value":12312},"明細CSVパーサー",{"type":25,"tag":855,"props":12314,"children":12315},{},[12316],{"type":31,"value":12317},"6種類",{"type":25,"tag":833,"props":12319,"children":12320},{},[12321,12326],{"type":25,"tag":855,"props":12322,"children":12323},{},[12324],{"type":31,"value":12325},"Firestoreコレクション",{"type":25,"tag":855,"props":12327,"children":12328},{},[12329],{"type":31,"value":12330},"11個",{"type":25,"tag":833,"props":12332,"children":12333},{},[12334,12339],{"type":25,"tag":855,"props":12335,"children":12336},{},[12337],{"type":31,"value":12338},"型定義",{"type":25,"tag":855,"props":12340,"children":12341},{},[12342],{"type":31,"value":12343},"14KB相当",{"type":25,"tag":313,"props":12345,"children":12347},{"id":12346},"特に活きた場面",[12348],{"type":31,"value":12346},{"type":25,"tag":335,"props":12350,"children":12352},{"id":12351},"定型コードの量産",[12353],{"type":31,"value":12351},{"type":25,"tag":33,"props":12355,"children":12356},{},[12357],{"type":31,"value":12358},"CSVパーサーは1つ目を丁寧に作った後、「同じパターンで〇〇版を作って」と依頼するだけで次々と生成できました。型定義・エラーハンドリング・カテゴリ推論の呼び出しまで一貫しています。",{"type":25,"tag":335,"props":12360,"children":12362},{"id":12361},"物量で押してくるタイプの面倒な作業をaiに押し付ける",[12363],{"type":31,"value":12364},"物量で押してくるタイプの面倒な作業をAIに押し付ける",{"type":25,"tag":33,"props":12366,"children":12367},{},[12368],{"type":31,"value":12369},"全角カタカナの正規表現、ApexChartsの膨大なオプション設定など、手作業では時間がかかる部分をAIに任せられました。特に明細の自動分類では正規表現で逐次マッチングしている仕組みにしているため、役立ちました。",{"type":25,"tag":26,"props":12371,"children":12373},{"id":12372},"効果的だった工夫",[12374],{"type":31,"value":12372},{"type":25,"tag":313,"props":12376,"children":12378},{"id":12377},"claudemdの整備",[12379],{"type":31,"value":12380},"CLAUDE.mdの整備",{"type":25,"tag":33,"props":12382,"children":12383},{},[12384],{"type":31,"value":12385},"コーディング規約・アーキテクチャ方針・Firestoreのルールを定義しておくと、生成されるコードのスタイルが統一されます。最初のCLAUDE.mdの品質が全体を左右します。",{"type":25,"tag":313,"props":12387,"children":12389},{"id":12388},"デザインをあらかじめ作っておく",[12390],{"type":31,"value":12388},{"type":25,"tag":33,"props":12392,"children":12393},{},[12394,12396],{"type":31,"value":12395},"この開発ではあらかじめGoogle StitchでUIの基本となるデザインを作成しました。そして、これをHTMLとしてリポジトリに配置し、AIエージェントを利用してNuxtへ移植するような順序で実装しました。こうすることで、デザインの方向性をエージェントが理解できるようになり、一貫したデザインで開発サイクルを回すことができました。\n",{"type":25,"tag":739,"props":12397,"children":12400},{"alt":12398,"src":12399},"Google_Stitch","/images/articles/tech/development/google-stitch.png",[],{"type":25,"tag":313,"props":12402,"children":12404},{"id":12403},"geminiと壁打ちして仕様書を作成する",[12405],{"type":31,"value":12406},"Geminiと壁打ちして仕様書を作成する",{"type":25,"tag":33,"props":12408,"children":12409},{},[12410],{"type":31,"value":12411},"要件は人間にしかわかりませんし、単純に「資産管理アプリを作って」というだけの指示では使えるものは作れなかったと思います。そこで、今回の開発ではGeminiと壁打ちをし、完成した仕様書をリポジトリに配置しました。この仕様通りにAIは実装するだけなので、こちらの想定通りのアプリが開発できました。",{"type":25,"tag":26,"props":12413,"children":12415},{"id":12414},"人間がリードすべきところ",[12416],{"type":31,"value":12414},{"type":25,"tag":33,"props":12418,"children":12419},{},[12420],{"type":31,"value":12421},"AIエージェントを活用した開発を進めていくうちに人間が進めるべき箇所が見えてきました。",{"type":25,"tag":313,"props":12423,"children":12425},{"id":12424},"要件定義ux関連",[12426],{"type":31,"value":12427},"要件定義・UX関連",{"type":25,"tag":33,"props":12429,"children":12430},{},[12431],{"type":31,"value":12432},"先述した通り、要件は人間にしかわからないですし、壁打ちをしてから初めてわかる「隠れていた要件」が出てくることもあります。このような場合に対応するには人間が必要です。",{"type":25,"tag":313,"props":12434,"children":12436},{"id":12435},"設計",[12437],{"type":31,"value":12435},{"type":25,"tag":33,"props":12439,"children":12440},{},[12441],{"type":31,"value":12442},"今回爆速で開発する上で、設計工程は重要なところ以外はかなり雑に進めました。しかし雑に進めたためか、機能を追加し続けると徐々に既存の不適切な設計箇所のコードが複雑化する現象が起こりました。そのため、設計は人間が関わり、定期的に見直すようにすることが必要だと感じています。",{"type":25,"tag":33,"props":12444,"children":12445},{},[12446],{"type":31,"value":12447},"特に、今回の開発以上に大規模なプロダクトになった場合、AIエージェントは全体のコンテキストを把握しながら開発することが困難になるため、この問題はより顕在化すると思われます。実際、この規模の開発でも「デグレ」が発生することがありました。",{"type":25,"tag":33,"props":12449,"children":12450},{},[12451],{"type":31,"value":12452},"また、AIエージェントに指示をすると「動くもの」は簡単に作ってくれます。しかし、データ効率や時間効率を意識した実装になっていないことがしばしばあります。したがって、パフォーマンスを試験する仕組みを人間が整える必要があると考えられます。",{"type":25,"tag":313,"props":12454,"children":12456},{"id":12455},"知識の運用",[12457],{"type":31,"value":12455},{"type":25,"tag":33,"props":12459,"children":12460},{},[12461],{"type":31,"value":12462},"AIエージェントで実装を進めると開発が高速で進みます。この結果、一瞬でコードが肥大化し、どこに何があるかを把握しきれなくなります。さらに、ドキュメントなどを書いていた場合は情報が一瞬で古くなります。これを解決するためにナレッジをどのように管理するかという仕組みを考える必要があります。",{"type":25,"tag":313,"props":12464,"children":12466},{"id":12465},"セキュリティ",[12467],{"type":31,"value":12465},{"type":25,"tag":33,"props":12469,"children":12470},{},[12471],{"type":31,"value":12472},"こればかりは目視で確認しないと安心できないです。何らかの拍子でシークレットがリポジトリにコミットされたり、プロダクションコードに脆弱性のあるコードが混入した場合、非常に大きな問題となるからです。",{"type":25,"tag":26,"props":12474,"children":12476},{"id":12475},"エンジニアとして手を抜く部分力を入れる部分",[12477],{"type":31,"value":12478},"エンジニアとして手を抜く部分・力を入れる部分",{"type":25,"tag":33,"props":12480,"children":12481},{},[12482],{"type":31,"value":12483},"これまでClaude Codeを活用する利点と人間が必要な箇所をまとめました。これを総括すれば、自ずとエンジニアが何に力を入れ、何に手を抜くかが見えてきます。",{"type":25,"tag":33,"props":12485,"children":12486},{},[12487],{"type":25,"tag":103,"props":12488,"children":12489},{},[12490],{"type":31,"value":12491},"手を抜く場面",{"type":25,"tag":53,"props":12493,"children":12494},{},[12495,12500,12505],{"type":25,"tag":57,"props":12496,"children":12497},{},[12498],{"type":31,"value":12499},"物量はあるが単純な実装",{"type":25,"tag":57,"props":12501,"children":12502},{},[12503],{"type":31,"value":12504},"文章の作成",{"type":25,"tag":57,"props":12506,"children":12507},{},[12508],{"type":31,"value":12509},"UIの実装（CSSなど）",{"type":25,"tag":33,"props":12511,"children":12512},{},[12513],{"type":25,"tag":103,"props":12514,"children":12515},{},[12516],{"type":31,"value":12517},"力を入れる部分",{"type":25,"tag":53,"props":12519,"children":12520},{},[12521,12526,12530,12543,12548],{"type":25,"tag":57,"props":12522,"children":12523},{},[12524],{"type":31,"value":12525},"要件分析・要件定義",{"type":25,"tag":57,"props":12527,"children":12528},{},[12529],{"type":31,"value":12435},{"type":25,"tag":57,"props":12531,"children":12532},{},[12533,12535],{"type":31,"value":12534},"デザイン・UXの判断\n",{"type":25,"tag":53,"props":12536,"children":12537},{},[12538],{"type":25,"tag":57,"props":12539,"children":12540},{},[12541],{"type":31,"value":12542},"アイコンの作成などブランディングの要素も含む",{"type":25,"tag":57,"props":12544,"children":12545},{},[12546],{"type":31,"value":12547},"運用の仕組みづくり",{"type":25,"tag":57,"props":12549,"children":12550},{},[12551],{"type":31,"value":12552},"セキュリティ運用",{"type":25,"tag":33,"props":12554,"children":12555},{},[12556,12558,12563,12565,12570],{"type":31,"value":12557},"こう見ると、AIエージェントが出てきたから発生した問題というよりは、「",{"type":25,"tag":103,"props":12559,"children":12560},{},[12561],{"type":31,"value":12562},"AIエージェントによって実装が効率化された結果、問題に向き合う機会が増えた",{"type":31,"value":12564},"」というのが私の考えです。つまり、エンジニアが力を入れないといけない部分というのは",{"type":25,"tag":103,"props":12566,"children":12567},{},[12568],{"type":31,"value":12569},"AIエージェントが出てくる前と変わらない",{"type":31,"value":12571},"と考えています。",{"type":25,"tag":26,"props":12573,"children":12575},{"id":12574},"個人開発の結果だけでは評価されない時代へ",[12576],{"type":31,"value":12574},{"type":25,"tag":33,"props":12578,"children":12579},{},[12580],{"type":31,"value":12581},"しかし、求められるスキルは変わらなくても、周囲からの見え方は変わりました。",{"type":25,"tag":33,"props":12583,"children":12584},{},[12585],{"type":31,"value":12586},"特に学生や未経験のエンジニアにとって、以前であれば個人開発のポートフォリオが一定の実力を示す材料だったと思います。しかし、これまで説明した通り、ただ実装するだけであれば誰でもできるようになりました。この結果、ただ個人開発で物を作ったというだけでは評価されない時代になったと思います。",{"type":25,"tag":33,"props":12588,"children":12589},{},[12590,12592,12597],{"type":31,"value":12591},"そのため、今後はただ物を作るだけでなく、「なぜこの方法を使うか」、「この設計にする意図とその副作用は何か」など開発プロセスに現れる",{"type":25,"tag":103,"props":12593,"children":12594},{},[12595],{"type":31,"value":12596},"技術の本質的な部分",{"type":31,"value":12598},"に向き合うことを意識し、説明できる力がより求められるようになると考えています。実際、過去に学んだソフトウェア設計の理論や、オーバーエンジニアリングで複雑化し拡張が困難になった経験も非常に活きています。したがって、AIに頼めばなんでもやってくれるからといって、それは使いこなせているとは言えないのです。",{"type":25,"tag":33,"props":12600,"children":12601},{},[12602,12604,12609],{"type":31,"value":12603},"もしこれを",{"type":25,"tag":103,"props":12605,"children":12606},{},[12607],{"type":31,"value":12608},"実力と勘違いしてしまうと腐ってしまう",{"type":31,"value":12610},"のではないかと危機感を覚えたので、自戒も込めて以上の内容を考えました。",{"type":25,"tag":26,"props":12612,"children":12613},{"id":1746},[12614],{"type":31,"value":1746},{"type":25,"tag":53,"props":12616,"children":12617},{},[12618,12623,12628,12633],{"type":25,"tag":57,"props":12619,"children":12620},{},[12621],{"type":31,"value":12622},"AIエージェントを利用すると、簡単に「実装」を効率化できる",{"type":25,"tag":57,"props":12624,"children":12625},{},[12626],{"type":31,"value":12627},"しかし、依然として要件定義、設計、運用などは人間が関与しないと低品質なプロダクトになってしまう",{"type":25,"tag":57,"props":12629,"children":12630},{},[12631],{"type":31,"value":12632},"これらの問題はAIが原因というわけではなく、AIエージェントによって実装が効率化された結果向き合う機会が増えただけ",{"type":25,"tag":57,"props":12634,"children":12635},{},[12636],{"type":31,"value":12637},"今後はより一層、技術の本質をどのくらい理解しているかが求められる",{"title":8,"searchDepth":1149,"depth":1149,"links":12639},[12640,12641,12647,12651,12656,12662,12663,12664],{"id":28,"depth":1149,"text":28},{"id":12002,"depth":1149,"text":12005,"children":12642},[12643,12644,12645,12646],{"id":12008,"depth":1159,"text":12011},{"id":12019,"depth":1159,"text":12022},{"id":12030,"depth":1159,"text":12030},{"id":12124,"depth":1159,"text":12124},{"id":12225,"depth":1149,"text":12228,"children":12648},[12649,12650],{"id":12236,"depth":1159,"text":12236},{"id":12346,"depth":1159,"text":12346},{"id":12372,"depth":1149,"text":12372,"children":12652},[12653,12654,12655],{"id":12377,"depth":1159,"text":12380},{"id":12388,"depth":1159,"text":12388},{"id":12403,"depth":1159,"text":12406},{"id":12414,"depth":1149,"text":12414,"children":12657},[12658,12659,12660,12661],{"id":12424,"depth":1159,"text":12427},{"id":12435,"depth":1159,"text":12435},{"id":12455,"depth":1159,"text":12455},{"id":12465,"depth":1159,"text":12465},{"id":12475,"depth":1149,"text":12478},{"id":12574,"depth":1149,"text":12574},{"id":1746,"depth":1149,"text":1746},"content:articles:tech:development:portal-with-claude-code.md","articles/tech/development/portal-with-claude-code.md","articles/tech/development/portal-with-claude-code",{"_path":12669,"_dir":11287,"_draft":7,"_partial":7,"_locale":8,"title":12670,"description":12671,"date":12672,"tags":12673,"rowTypeId":18,"sitemap":12679,"body":12680,"_type":1180,"_id":14857,"_source":1182,"_file":14858,"_stem":14859,"_extension":1185},"/articles/tech/development/yominchu-vertical-canvas","【解説】Canvas で縦書き日本語組版を実装する！詠み人の描画方法","和歌・短歌・俳句を画像化するツール「詠み人」で、CSS writing-mode を使わず Canvas で縦書きを実装した理由と、句読点や括弧などの特殊文字の扱いをまとめます。","2026-04-16",[12674,12675,12676,12078,12677,12678],"Canvas","日本語組版","縦書き","フォント","詠み人",{"loc":12669,"lastmod":12672,"priority":18},{"type":22,"children":12681,"toc":14843},[12682,12686,12705,12725,12741,12745,12813,12826,12831,12843,12848,12853,13435,13455,13475,13481,13501,13514,14304,14309,14329,14335,14362,14374,14379,14391,14396,14573,14599,14604,14623,14636,14767,14788,14792,14834,14839],{"type":25,"tag":26,"props":12683,"children":12684},{"id":28},[12685],{"type":31,"value":28},{"type":25,"tag":33,"props":12687,"children":12688},{},[12689,12691,12696,12698,12703],{"type":31,"value":12690},"和歌・短歌・俳句を縦書きの美しい画像にして SNS に投稿できるツール ",{"type":25,"tag":1354,"props":12692,"children":12694},{"href":12693},"/tools/yominchu",[12695],{"type":31,"value":12678},{"type":31,"value":12697}," を作ったとき、最大の悩みどころは「",{"type":25,"tag":103,"props":12699,"children":12700},{},[12701],{"type":31,"value":12702},"縦書きレイアウトをどう描画するか",{"type":31,"value":12704},"」でした。",{"type":25,"tag":33,"props":12706,"children":12707},{},[12708,12710,12716,12718,12723],{"type":31,"value":12709},"ブラウザには ",{"type":25,"tag":529,"props":12711,"children":12713},{"className":12712},[],[12714],{"type":31,"value":12715},"writing-mode: vertical-rl",{"type":31,"value":12717}," という CSS プロパティがあり、HTML 上で縦書きを表現することはできます。けれども詠み人では最終的に ",{"type":25,"tag":103,"props":12719,"children":12720},{},[12721],{"type":31,"value":12722},"Canvas で1文字ずつ描画する",{"type":31,"value":12724}," 方針にしました。この記事では、その判断と、ハマった特殊文字の扱いをまとめます。",{"type":25,"tag":39,"props":12726,"children":12727},{},[12728],{"type":25,"tag":33,"props":12729,"children":12730},{},[12731,12733,12739],{"type":31,"value":12732},"CSS ",{"type":25,"tag":529,"props":12734,"children":12736},{"className":12735},[],[12737],{"type":31,"value":12738},"writing-mode",{"type":31,"value":12740}," ではなく Canvas 描画を選んだのは、「最終成果物が画像である」という用途の特殊性からでした。句読点の位置調整、長音・括弧の90度回転、フォント読み込み待機など、日本語組版固有の細かいルールを自前でコントロールする必要があり、結果的に Canvas のほうが制御しやすかったです。",{"type":25,"tag":26,"props":12742,"children":12743},{"id":11319},[12744],{"type":31,"value":11319},{"type":25,"tag":825,"props":12746,"children":12747},{},[12748,12762],{"type":25,"tag":829,"props":12749,"children":12750},{},[12751],{"type":25,"tag":833,"props":12752,"children":12753},{},[12754,12758],{"type":25,"tag":837,"props":12755,"children":12756},{},[12757],{"type":31,"value":841},{"type":25,"tag":837,"props":12759,"children":12760},{},[12761],{"type":31,"value":11339},{"type":25,"tag":848,"props":12763,"children":12764},{},[12765,12777,12788,12801],{"type":25,"tag":833,"props":12766,"children":12767},{},[12768,12772],{"type":25,"tag":855,"props":12769,"children":12770},{},[12771],{"type":31,"value":11350},{"type":25,"tag":855,"props":12773,"children":12774},{},[12775],{"type":31,"value":12776},"Nuxt 4 + Vue 3",{"type":25,"tag":833,"props":12778,"children":12779},{},[12780,12784],{"type":25,"tag":855,"props":12781,"children":12782},{},[12783],{"type":31,"value":12073},{"type":25,"tag":855,"props":12785,"children":12786},{},[12787],{"type":31,"value":12078},{"type":25,"tag":833,"props":12789,"children":12790},{},[12791,12796],{"type":25,"tag":855,"props":12792,"children":12793},{},[12794],{"type":31,"value":12795},"描画",{"type":25,"tag":855,"props":12797,"children":12798},{},[12799],{"type":31,"value":12800},"HTML Canvas 2D",{"type":25,"tag":833,"props":12802,"children":12803},{},[12804,12808],{"type":25,"tag":855,"props":12805,"children":12806},{},[12807],{"type":31,"value":12677},{"type":25,"tag":855,"props":12809,"children":12810},{},[12811],{"type":31,"value":12812},"Noto Serif JP + 同梱フォント（力弱・行書・隷書）",{"type":25,"tag":26,"props":12814,"children":12816},{"id":12815},"なぜ-css-writing-mode-を選ばなかったのか",[12817,12819,12824],{"type":31,"value":12818},"なぜ CSS ",{"type":25,"tag":529,"props":12820,"children":12822},{"className":12821},[],[12823],{"type":31,"value":12738},{"type":31,"value":12825}," を選ばなかったのか",{"type":25,"tag":33,"props":12827,"children":12828},{},[12829],{"type":31,"value":12830},"詠み人は最終的に「1枚の PNG 画像」を生成するツールです。ユーザーはそれを Twitter/X にシェアしたり、画像としてダウンロードしたりします。",{"type":25,"tag":33,"props":12832,"children":12833},{},[12834,12836,12841],{"type":31,"value":12835},"「",{"type":25,"tag":103,"props":12837,"children":12838},{},[12839],{"type":31,"value":12840},"実装者が描画の全てをコントロールしたい",{"type":31,"value":12842},"」という要望にいちばん素直に応えてくれたのが、Canvas で1文字ずつ fillText する方式でした。後々背景を追加するなどを考えた際にも Canvas は扱いやすいと考えました。",{"type":25,"tag":26,"props":12844,"children":12846},{"id":12845},"基本のカラムレイアウト",[12847],{"type":31,"value":12845},{"type":25,"tag":33,"props":12849,"children":12850},{},[12851],{"type":31,"value":12852},"縦書きは「右から左へ」行が並びます。詠み人ではこれを次のように実装しています。",{"type":25,"tag":524,"props":12854,"children":12858},{"className":12855,"code":12856,"language":12857,"meta":8,"style":8},"language-typescript shiki shiki-themes vitesse-dark","validLines.forEach((line, ci) => {\n  const x = poemStartX - ci * colW\n  const chars = [...line]\n  const fs = Math.min(46, Math.floor((H - 130) / Math.max(chars.length, 1)))\n  const lh = fs + 6\n\n  ctx.fillStyle = style.text\n  ctx.font = `400 ${fs}px ${fontFamily}`\n  ctx.textAlign = 'center'\n\n  chars.forEach((ch, ci2) => {\n    drawVerticalChar(ctx, ch, x, startY + ci2 * lh, fs, fontId)\n  })\n})\n","typescript",[12859],{"type":25,"tag":529,"props":12860,"children":12861},{"__ignoreMap":8},[12862,12910,12952,12981,13108,13139,13146,13181,13247,13282,13289,13335,13419,13427],{"type":25,"tag":2682,"props":12863,"children":12864},{"class":2684,"line":18},[12865,12870,12874,12879,12884,12888,12892,12897,12901,12905],{"type":25,"tag":2682,"props":12866,"children":12867},{"style":3004},[12868],{"type":31,"value":12869},"validLines",{"type":25,"tag":2682,"props":12871,"children":12872},{"style":2700},[12873],{"type":31,"value":2703},{"type":25,"tag":2682,"props":12875,"children":12876},{"style":2852},[12877],{"type":31,"value":12878},"forEach",{"type":25,"tag":2682,"props":12880,"children":12881},{"style":2700},[12882],{"type":31,"value":12883},"((",{"type":25,"tag":2682,"props":12885,"children":12886},{"style":3004},[12887],{"type":31,"value":2684},{"type":25,"tag":2682,"props":12889,"children":12890},{"style":2700},[12891],{"type":31,"value":2921},{"type":25,"tag":2682,"props":12893,"children":12894},{"style":3004},[12895],{"type":31,"value":12896}," ci",{"type":25,"tag":2682,"props":12898,"children":12899},{"style":2700},[12900],{"type":31,"value":3021},{"type":25,"tag":2682,"props":12902,"children":12903},{"style":2700},[12904],{"type":31,"value":3336},{"type":25,"tag":2682,"props":12906,"children":12907},{"style":2700},[12908],{"type":31,"value":12909}," {\n",{"type":25,"tag":2682,"props":12911,"children":12912},{"class":2684,"line":1149},[12913,12918,12923,12927,12932,12937,12942,12947],{"type":25,"tag":2682,"props":12914,"children":12915},{"style":2762},[12916],{"type":31,"value":12917},"  const ",{"type":25,"tag":2682,"props":12919,"children":12920},{"style":3004},[12921],{"type":31,"value":12922},"x",{"type":25,"tag":2682,"props":12924,"children":12925},{"style":2700},[12926],{"type":31,"value":2968},{"type":25,"tag":2682,"props":12928,"children":12929},{"style":3004},[12930],{"type":31,"value":12931}," poemStartX",{"type":25,"tag":2682,"props":12933,"children":12934},{"style":2762},[12935],{"type":31,"value":12936}," - ",{"type":25,"tag":2682,"props":12938,"children":12939},{"style":3004},[12940],{"type":31,"value":12941},"ci",{"type":25,"tag":2682,"props":12943,"children":12944},{"style":2762},[12945],{"type":31,"value":12946}," * ",{"type":25,"tag":2682,"props":12948,"children":12949},{"style":3004},[12950],{"type":31,"value":12951},"colW\n",{"type":25,"tag":2682,"props":12953,"children":12954},{"class":2684,"line":1159},[12955,12959,12964,12968,12973,12977],{"type":25,"tag":2682,"props":12956,"children":12957},{"style":2762},[12958],{"type":31,"value":12917},{"type":25,"tag":2682,"props":12960,"children":12961},{"style":3004},[12962],{"type":31,"value":12963},"chars",{"type":25,"tag":2682,"props":12965,"children":12966},{"style":2700},[12967],{"type":31,"value":2968},{"type":25,"tag":2682,"props":12969,"children":12970},{"style":2700},[12971],{"type":31,"value":12972}," [...",{"type":25,"tag":2682,"props":12974,"children":12975},{"style":3004},[12976],{"type":31,"value":2684},{"type":25,"tag":2682,"props":12978,"children":12979},{"style":2700},[12980],{"type":31,"value":5566},{"type":25,"tag":2682,"props":12982,"children":12983},{"class":2684,"line":2758},[12984,12988,12993,12997,13002,13006,13011,13015,13020,13024,13028,13032,13037,13041,13046,13050,13055,13059,13063,13068,13072,13077,13081,13085,13089,13095,13099,13103],{"type":25,"tag":2682,"props":12985,"children":12986},{"style":2762},[12987],{"type":31,"value":12917},{"type":25,"tag":2682,"props":12989,"children":12990},{"style":3004},[12991],{"type":31,"value":12992},"fs",{"type":25,"tag":2682,"props":12994,"children":12995},{"style":2700},[12996],{"type":31,"value":2968},{"type":25,"tag":2682,"props":12998,"children":12999},{"style":3004},[13000],{"type":31,"value":13001}," Math",{"type":25,"tag":2682,"props":13003,"children":13004},{"style":2700},[13005],{"type":31,"value":2703},{"type":25,"tag":2682,"props":13007,"children":13008},{"style":2852},[13009],{"type":31,"value":13010},"min",{"type":25,"tag":2682,"props":13012,"children":13013},{"style":2700},[13014],{"type":31,"value":3076},{"type":25,"tag":2682,"props":13016,"children":13017},{"style":5609},[13018],{"type":31,"value":13019},"46",{"type":25,"tag":2682,"props":13021,"children":13022},{"style":2700},[13023],{"type":31,"value":2921},{"type":25,"tag":2682,"props":13025,"children":13026},{"style":3004},[13027],{"type":31,"value":13001},{"type":25,"tag":2682,"props":13029,"children":13030},{"style":2700},[13031],{"type":31,"value":2703},{"type":25,"tag":2682,"props":13033,"children":13034},{"style":2852},[13035],{"type":31,"value":13036},"floor",{"type":25,"tag":2682,"props":13038,"children":13039},{"style":2700},[13040],{"type":31,"value":12883},{"type":25,"tag":2682,"props":13042,"children":13043},{"style":3004},[13044],{"type":31,"value":13045},"H",{"type":25,"tag":2682,"props":13047,"children":13048},{"style":2762},[13049],{"type":31,"value":12936},{"type":25,"tag":2682,"props":13051,"children":13052},{"style":5609},[13053],{"type":31,"value":13054},"130",{"type":25,"tag":2682,"props":13056,"children":13057},{"style":2700},[13058],{"type":31,"value":3021},{"type":25,"tag":2682,"props":13060,"children":13061},{"style":2762},[13062],{"type":31,"value":5972},{"type":25,"tag":2682,"props":13064,"children":13065},{"style":3004},[13066],{"type":31,"value":13067},"Math",{"type":25,"tag":2682,"props":13069,"children":13070},{"style":2700},[13071],{"type":31,"value":2703},{"type":25,"tag":2682,"props":13073,"children":13074},{"style":2852},[13075],{"type":31,"value":13076},"max",{"type":25,"tag":2682,"props":13078,"children":13079},{"style":2700},[13080],{"type":31,"value":3076},{"type":25,"tag":2682,"props":13082,"children":13083},{"style":3004},[13084],{"type":31,"value":12963},{"type":25,"tag":2682,"props":13086,"children":13087},{"style":2700},[13088],{"type":31,"value":2703},{"type":25,"tag":2682,"props":13090,"children":13092},{"style":13091},"--shiki-default:#B8A965",[13093],{"type":31,"value":13094},"length",{"type":25,"tag":2682,"props":13096,"children":13097},{"style":2700},[13098],{"type":31,"value":2921},{"type":25,"tag":2682,"props":13100,"children":13101},{"style":5609},[13102],{"type":31,"value":7459},{"type":25,"tag":2682,"props":13104,"children":13105},{"style":2700},[13106],{"type":31,"value":13107},")))\n",{"type":25,"tag":2682,"props":13109,"children":13110},{"class":2684,"line":2777},[13111,13115,13120,13124,13129,13134],{"type":25,"tag":2682,"props":13112,"children":13113},{"style":2762},[13114],{"type":31,"value":12917},{"type":25,"tag":2682,"props":13116,"children":13117},{"style":3004},[13118],{"type":31,"value":13119},"lh",{"type":25,"tag":2682,"props":13121,"children":13122},{"style":2700},[13123],{"type":31,"value":2968},{"type":25,"tag":2682,"props":13125,"children":13126},{"style":3004},[13127],{"type":31,"value":13128}," fs",{"type":25,"tag":2682,"props":13130,"children":13131},{"style":2762},[13132],{"type":31,"value":13133}," + ",{"type":25,"tag":2682,"props":13135,"children":13136},{"style":5609},[13137],{"type":31,"value":13138},"6\n",{"type":25,"tag":2682,"props":13140,"children":13141},{"class":2684,"line":2785},[13142],{"type":25,"tag":2682,"props":13143,"children":13144},{"emptyLinePlaceholder":2752},[13145],{"type":31,"value":2755},{"type":25,"tag":2682,"props":13147,"children":13148},{"class":2684,"line":2819},[13149,13154,13158,13163,13167,13172,13176],{"type":25,"tag":2682,"props":13150,"children":13151},{"style":3004},[13152],{"type":31,"value":13153},"  ctx",{"type":25,"tag":2682,"props":13155,"children":13156},{"style":2700},[13157],{"type":31,"value":2703},{"type":25,"tag":2682,"props":13159,"children":13160},{"style":3004},[13161],{"type":31,"value":13162},"fillStyle",{"type":25,"tag":2682,"props":13164,"children":13165},{"style":2700},[13166],{"type":31,"value":2968},{"type":25,"tag":2682,"props":13168,"children":13169},{"style":3004},[13170],{"type":31,"value":13171}," style",{"type":25,"tag":2682,"props":13173,"children":13174},{"style":2700},[13175],{"type":31,"value":2703},{"type":25,"tag":2682,"props":13177,"children":13178},{"style":3004},[13179],{"type":31,"value":13180},"text\n",{"type":25,"tag":2682,"props":13182,"children":13183},{"class":2684,"line":2828},[13184,13188,13192,13197,13201,13206,13211,13216,13220,13224,13229,13233,13238,13242],{"type":25,"tag":2682,"props":13185,"children":13186},{"style":3004},[13187],{"type":31,"value":13153},{"type":25,"tag":2682,"props":13189,"children":13190},{"style":2700},[13191],{"type":31,"value":2703},{"type":25,"tag":2682,"props":13193,"children":13194},{"style":3004},[13195],{"type":31,"value":13196},"font",{"type":25,"tag":2682,"props":13198,"children":13199},{"style":2700},[13200],{"type":31,"value":2968},{"type":25,"tag":2682,"props":13202,"children":13203},{"style":3293},[13204],{"type":31,"value":13205}," `",{"type":25,"tag":2682,"props":13207,"children":13208},{"style":4759},[13209],{"type":31,"value":13210},"400 ",{"type":25,"tag":2682,"props":13212,"children":13213},{"style":2688},[13214],{"type":31,"value":13215},"${",{"type":25,"tag":2682,"props":13217,"children":13218},{"style":4759},[13219],{"type":31,"value":12992},{"type":25,"tag":2682,"props":13221,"children":13222},{"style":2688},[13223],{"type":31,"value":6882},{"type":25,"tag":2682,"props":13225,"children":13226},{"style":4759},[13227],{"type":31,"value":13228},"px ",{"type":25,"tag":2682,"props":13230,"children":13231},{"style":2688},[13232],{"type":31,"value":13215},{"type":25,"tag":2682,"props":13234,"children":13235},{"style":4759},[13236],{"type":31,"value":13237},"fontFamily",{"type":25,"tag":2682,"props":13239,"children":13240},{"style":2688},[13241],{"type":31,"value":6882},{"type":25,"tag":2682,"props":13243,"children":13244},{"style":3293},[13245],{"type":31,"value":13246},"`\n",{"type":25,"tag":2682,"props":13248,"children":13249},{"class":2684,"line":2862},[13250,13254,13258,13263,13267,13272,13277],{"type":25,"tag":2682,"props":13251,"children":13252},{"style":3004},[13253],{"type":31,"value":13153},{"type":25,"tag":2682,"props":13255,"children":13256},{"style":2700},[13257],{"type":31,"value":2703},{"type":25,"tag":2682,"props":13259,"children":13260},{"style":3004},[13261],{"type":31,"value":13262},"textAlign",{"type":25,"tag":2682,"props":13264,"children":13265},{"style":2700},[13266],{"type":31,"value":2968},{"type":25,"tag":2682,"props":13268,"children":13269},{"style":3293},[13270],{"type":31,"value":13271}," '",{"type":25,"tag":2682,"props":13273,"children":13274},{"style":4759},[13275],{"type":31,"value":13276},"center",{"type":25,"tag":2682,"props":13278,"children":13279},{"style":3293},[13280],{"type":31,"value":13281},"'\n",{"type":25,"tag":2682,"props":13283,"children":13284},{"class":2684,"line":2870},[13285],{"type":25,"tag":2682,"props":13286,"children":13287},{"emptyLinePlaceholder":2752},[13288],{"type":31,"value":2755},{"type":25,"tag":2682,"props":13290,"children":13291},{"class":2684,"line":2981},[13292,13297,13301,13305,13309,13314,13318,13323,13327,13331],{"type":25,"tag":2682,"props":13293,"children":13294},{"style":3004},[13295],{"type":31,"value":13296},"  chars",{"type":25,"tag":2682,"props":13298,"children":13299},{"style":2700},[13300],{"type":31,"value":2703},{"type":25,"tag":2682,"props":13302,"children":13303},{"style":2852},[13304],{"type":31,"value":12878},{"type":25,"tag":2682,"props":13306,"children":13307},{"style":2700},[13308],{"type":31,"value":12883},{"type":25,"tag":2682,"props":13310,"children":13311},{"style":3004},[13312],{"type":31,"value":13313},"ch",{"type":25,"tag":2682,"props":13315,"children":13316},{"style":2700},[13317],{"type":31,"value":2921},{"type":25,"tag":2682,"props":13319,"children":13320},{"style":3004},[13321],{"type":31,"value":13322}," ci2",{"type":25,"tag":2682,"props":13324,"children":13325},{"style":2700},[13326],{"type":31,"value":3021},{"type":25,"tag":2682,"props":13328,"children":13329},{"style":2700},[13330],{"type":31,"value":3336},{"type":25,"tag":2682,"props":13332,"children":13333},{"style":2700},[13334],{"type":31,"value":12909},{"type":25,"tag":2682,"props":13336,"children":13337},{"class":2684,"line":2990},[13338,13343,13347,13352,13356,13361,13365,13370,13374,13379,13384,13388,13393,13398,13402,13406,13410,13415],{"type":25,"tag":2682,"props":13339,"children":13340},{"style":2852},[13341],{"type":31,"value":13342},"    drawVerticalChar",{"type":25,"tag":2682,"props":13344,"children":13345},{"style":2700},[13346],{"type":31,"value":3076},{"type":25,"tag":2682,"props":13348,"children":13349},{"style":3004},[13350],{"type":31,"value":13351},"ctx",{"type":25,"tag":2682,"props":13353,"children":13354},{"style":2700},[13355],{"type":31,"value":2921},{"type":25,"tag":2682,"props":13357,"children":13358},{"style":3004},[13359],{"type":31,"value":13360}," ch",{"type":25,"tag":2682,"props":13362,"children":13363},{"style":2700},[13364],{"type":31,"value":2921},{"type":25,"tag":2682,"props":13366,"children":13367},{"style":3004},[13368],{"type":31,"value":13369}," x",{"type":25,"tag":2682,"props":13371,"children":13372},{"style":2700},[13373],{"type":31,"value":2921},{"type":25,"tag":2682,"props":13375,"children":13376},{"style":3004},[13377],{"type":31,"value":13378}," startY",{"type":25,"tag":2682,"props":13380,"children":13381},{"style":2762},[13382],{"type":31,"value":13383}," +",{"type":25,"tag":2682,"props":13385,"children":13386},{"style":3004},[13387],{"type":31,"value":13322},{"type":25,"tag":2682,"props":13389,"children":13390},{"style":2762},[13391],{"type":31,"value":13392}," *",{"type":25,"tag":2682,"props":13394,"children":13395},{"style":3004},[13396],{"type":31,"value":13397}," lh",{"type":25,"tag":2682,"props":13399,"children":13400},{"style":2700},[13401],{"type":31,"value":2921},{"type":25,"tag":2682,"props":13403,"children":13404},{"style":3004},[13405],{"type":31,"value":13128},{"type":25,"tag":2682,"props":13407,"children":13408},{"style":2700},[13409],{"type":31,"value":2921},{"type":25,"tag":2682,"props":13411,"children":13412},{"style":3004},[13413],{"type":31,"value":13414}," fontId",{"type":25,"tag":2682,"props":13416,"children":13417},{"style":2700},[13418],{"type":31,"value":2978},{"type":25,"tag":2682,"props":13420,"children":13421},{"class":2684,"line":3044},[13422],{"type":25,"tag":2682,"props":13423,"children":13424},{"style":2700},[13425],{"type":31,"value":13426},"  })\n",{"type":25,"tag":2682,"props":13428,"children":13429},{"class":2684,"line":3065},[13430],{"type":25,"tag":2682,"props":13431,"children":13432},{"style":2700},[13433],{"type":31,"value":13434},"})\n",{"type":25,"tag":33,"props":13436,"children":13437},{},[13438,13440,13446,13448,13453],{"type":31,"value":13439},"行ごとに x座標を ",{"type":25,"tag":529,"props":13441,"children":13443},{"className":13442},[],[13444],{"type":31,"value":13445},"colW",{"type":31,"value":13447}," だけ左にずらしていくのが「右から左へ」の実現です。各列の中では、文字を上から下に ",{"type":25,"tag":529,"props":13449,"children":13451},{"className":13450},[],[13452],{"type":31,"value":13119},{"type":31,"value":13454},"（= フォントサイズ + 6px）の間隔で配置していきます。",{"type":25,"tag":33,"props":13456,"children":13457},{},[13458,13460,13466,13468,13473],{"type":31,"value":13459},"行の文字数に応じてフォントサイズを ",{"type":25,"tag":529,"props":13461,"children":13463},{"className":13462},[],[13464],{"type":31,"value":13465},"Math.floor((H - 130) / chars.length)",{"type":31,"value":13467}," で動的に決めているのもポイントです。長い句でも必ずキャンバス内に収まるように、",{"type":25,"tag":103,"props":13469,"children":13470},{},[13471],{"type":31,"value":13472},"フォントサイズのほうを縮める",{"type":31,"value":13474}," 方針にしました。",{"type":25,"tag":26,"props":13476,"children":13478},{"id":13477},"特殊文字の扱い-詠み人最大のハマりどころ",[13479],{"type":31,"value":13480},"特殊文字の扱い — 詠み人最大のハマりどころ",{"type":25,"tag":33,"props":13482,"children":13483},{},[13484,13486,13491,13493,13499],{"type":31,"value":13485},"縦書き日本語組版で一筋縄でいかないのが、",{"type":25,"tag":103,"props":13487,"children":13488},{},[13489],{"type":31,"value":13490},"句読点・括弧・長音符",{"type":31,"value":13492}," などの特殊文字です。これらを素直に ",{"type":25,"tag":529,"props":13494,"children":13496},{"className":13495},[],[13497],{"type":31,"value":13498},"fillText",{"type":31,"value":13500}," すると、向きや位置が崩れて読めない画像になります。",{"type":25,"tag":33,"props":13502,"children":13503},{},[13504,13506,13512],{"type":31,"value":13505},"詠み人では ",{"type":25,"tag":529,"props":13507,"children":13509},{"className":13508},[],[13510],{"type":31,"value":13511},"drawVerticalChar",{"type":31,"value":13513}," の中でこれらを個別処理しています。",{"type":25,"tag":524,"props":13515,"children":13517},{"className":12855,"code":13516,"language":12857,"meta":8,"style":8},"const ROTATE_CHARS = new Set('ー〜「」『』…・（）')\nconst TOP_RIGHT_CHARS = new Set('、。')\n\nfunction drawVerticalChar(ctx, ch, x, y, fontSize, fontId) {\n  if (TOP_RIGHT_CHARS.has(ch)) {\n    // 句読点: 右上に小さめに配置\n    ctx.save()\n    const size = fontSize * 0.5\n    ctx.font = ctx.font.replace(/\\d+px/, `${size}px`)\n    ctx.fillText(ch, x + fontSize * 0.35, y - fontSize * 0.3)\n    ctx.restore()\n  } else if (ROTATE_CHARS.has(ch)) {\n    // 長音・括弧等: 90度回転\n    ctx.save()\n    ctx.translate(x, y - fontSize * 0.35)\n    ctx.rotate(Math.PI / 2)\n    ctx.fillText(ch, 0, 0)\n    ctx.restore()\n  } else {\n    ctx.fillText(ch, x, y)\n  }\n}\n",[13518],{"type":25,"tag":529,"props":13519,"children":13520},{"__ignoreMap":8},[13521,13570,13615,13622,13693,13735,13743,13764,13794,13898,13975,13995,14044,14052,14071,14123,14169,14212,14231,14246,14289,14297],{"type":25,"tag":2682,"props":13522,"children":13523},{"class":2684,"line":18},[13524,13529,13534,13538,13543,13548,13552,13557,13562,13566],{"type":25,"tag":2682,"props":13525,"children":13526},{"style":2762},[13527],{"type":31,"value":13528},"const ",{"type":25,"tag":2682,"props":13530,"children":13531},{"style":3004},[13532],{"type":31,"value":13533},"ROTATE_CHARS",{"type":25,"tag":2682,"props":13535,"children":13536},{"style":2700},[13537],{"type":31,"value":2968},{"type":25,"tag":2682,"props":13539,"children":13540},{"style":2762},[13541],{"type":31,"value":13542}," new ",{"type":25,"tag":2682,"props":13544,"children":13545},{"style":2852},[13546],{"type":31,"value":13547},"Set",{"type":25,"tag":2682,"props":13549,"children":13550},{"style":2700},[13551],{"type":31,"value":3076},{"type":25,"tag":2682,"props":13553,"children":13554},{"style":3293},[13555],{"type":31,"value":13556},"'",{"type":25,"tag":2682,"props":13558,"children":13559},{"style":4759},[13560],{"type":31,"value":13561},"ー〜「」『』…・（）",{"type":25,"tag":2682,"props":13563,"children":13564},{"style":3293},[13565],{"type":31,"value":13556},{"type":25,"tag":2682,"props":13567,"children":13568},{"style":2700},[13569],{"type":31,"value":2978},{"type":25,"tag":2682,"props":13571,"children":13572},{"class":2684,"line":1149},[13573,13577,13582,13586,13590,13594,13598,13602,13607,13611],{"type":25,"tag":2682,"props":13574,"children":13575},{"style":2762},[13576],{"type":31,"value":13528},{"type":25,"tag":2682,"props":13578,"children":13579},{"style":3004},[13580],{"type":31,"value":13581},"TOP_RIGHT_CHARS",{"type":25,"tag":2682,"props":13583,"children":13584},{"style":2700},[13585],{"type":31,"value":2968},{"type":25,"tag":2682,"props":13587,"children":13588},{"style":2762},[13589],{"type":31,"value":13542},{"type":25,"tag":2682,"props":13591,"children":13592},{"style":2852},[13593],{"type":31,"value":13547},{"type":25,"tag":2682,"props":13595,"children":13596},{"style":2700},[13597],{"type":31,"value":3076},{"type":25,"tag":2682,"props":13599,"children":13600},{"style":3293},[13601],{"type":31,"value":13556},{"type":25,"tag":2682,"props":13603,"children":13604},{"style":4759},[13605],{"type":31,"value":13606},"、。",{"type":25,"tag":2682,"props":13608,"children":13609},{"style":3293},[13610],{"type":31,"value":13556},{"type":25,"tag":2682,"props":13612,"children":13613},{"style":2700},[13614],{"type":31,"value":2978},{"type":25,"tag":2682,"props":13616,"children":13617},{"class":2684,"line":1159},[13618],{"type":25,"tag":2682,"props":13619,"children":13620},{"emptyLinePlaceholder":2752},[13621],{"type":31,"value":2755},{"type":25,"tag":2682,"props":13623,"children":13624},{"class":2684,"line":2758},[13625,13630,13635,13639,13643,13647,13651,13655,13659,13663,13668,13672,13677,13681,13685,13689],{"type":25,"tag":2682,"props":13626,"children":13627},{"style":2762},[13628],{"type":31,"value":13629},"function",{"type":25,"tag":2682,"props":13631,"children":13632},{"style":2852},[13633],{"type":31,"value":13634}," drawVerticalChar",{"type":25,"tag":2682,"props":13636,"children":13637},{"style":2700},[13638],{"type":31,"value":3076},{"type":25,"tag":2682,"props":13640,"children":13641},{"style":3004},[13642],{"type":31,"value":13351},{"type":25,"tag":2682,"props":13644,"children":13645},{"style":2700},[13646],{"type":31,"value":2921},{"type":25,"tag":2682,"props":13648,"children":13649},{"style":3004},[13650],{"type":31,"value":13360},{"type":25,"tag":2682,"props":13652,"children":13653},{"style":2700},[13654],{"type":31,"value":2921},{"type":25,"tag":2682,"props":13656,"children":13657},{"style":3004},[13658],{"type":31,"value":13369},{"type":25,"tag":2682,"props":13660,"children":13661},{"style":2700},[13662],{"type":31,"value":2921},{"type":25,"tag":2682,"props":13664,"children":13665},{"style":3004},[13666],{"type":31,"value":13667}," y",{"type":25,"tag":2682,"props":13669,"children":13670},{"style":2700},[13671],{"type":31,"value":2921},{"type":25,"tag":2682,"props":13673,"children":13674},{"style":3004},[13675],{"type":31,"value":13676}," fontSize",{"type":25,"tag":2682,"props":13678,"children":13679},{"style":2700},[13680],{"type":31,"value":2921},{"type":25,"tag":2682,"props":13682,"children":13683},{"style":3004},[13684],{"type":31,"value":13414},{"type":25,"tag":2682,"props":13686,"children":13687},{"style":2700},[13688],{"type":31,"value":3021},{"type":25,"tag":2682,"props":13690,"children":13691},{"style":2700},[13692],{"type":31,"value":12909},{"type":25,"tag":2682,"props":13694,"children":13695},{"class":2684,"line":2777},[13696,13701,13705,13709,13713,13718,13722,13726,13731],{"type":25,"tag":2682,"props":13697,"children":13698},{"style":2688},[13699],{"type":31,"value":13700},"  if",{"type":25,"tag":2682,"props":13702,"children":13703},{"style":2700},[13704],{"type":31,"value":3001},{"type":25,"tag":2682,"props":13706,"children":13707},{"style":3004},[13708],{"type":31,"value":13581},{"type":25,"tag":2682,"props":13710,"children":13711},{"style":2700},[13712],{"type":31,"value":2703},{"type":25,"tag":2682,"props":13714,"children":13715},{"style":2852},[13716],{"type":31,"value":13717},"has",{"type":25,"tag":2682,"props":13719,"children":13720},{"style":2700},[13721],{"type":31,"value":3076},{"type":25,"tag":2682,"props":13723,"children":13724},{"style":3004},[13725],{"type":31,"value":13313},{"type":25,"tag":2682,"props":13727,"children":13728},{"style":2700},[13729],{"type":31,"value":13730},"))",{"type":25,"tag":2682,"props":13732,"children":13733},{"style":2700},[13734],{"type":31,"value":12909},{"type":25,"tag":2682,"props":13736,"children":13737},{"class":2684,"line":2785},[13738],{"type":25,"tag":2682,"props":13739,"children":13740},{"style":3931},[13741],{"type":31,"value":13742},"    // 句読点: 右上に小さめに配置\n",{"type":25,"tag":2682,"props":13744,"children":13745},{"class":2684,"line":2819},[13746,13751,13755,13760],{"type":25,"tag":2682,"props":13747,"children":13748},{"style":3004},[13749],{"type":31,"value":13750},"    ctx",{"type":25,"tag":2682,"props":13752,"children":13753},{"style":2700},[13754],{"type":31,"value":2703},{"type":25,"tag":2682,"props":13756,"children":13757},{"style":2852},[13758],{"type":31,"value":13759},"save",{"type":25,"tag":2682,"props":13761,"children":13762},{"style":2700},[13763],{"type":31,"value":9595},{"type":25,"tag":2682,"props":13765,"children":13766},{"class":2684,"line":2828},[13767,13772,13777,13781,13785,13789],{"type":25,"tag":2682,"props":13768,"children":13769},{"style":2762},[13770],{"type":31,"value":13771},"    const ",{"type":25,"tag":2682,"props":13773,"children":13774},{"style":3004},[13775],{"type":31,"value":13776},"size",{"type":25,"tag":2682,"props":13778,"children":13779},{"style":2700},[13780],{"type":31,"value":2968},{"type":25,"tag":2682,"props":13782,"children":13783},{"style":3004},[13784],{"type":31,"value":13676},{"type":25,"tag":2682,"props":13786,"children":13787},{"style":2762},[13788],{"type":31,"value":12946},{"type":25,"tag":2682,"props":13790,"children":13791},{"style":5609},[13792],{"type":31,"value":13793},"0.5\n",{"type":25,"tag":2682,"props":13795,"children":13796},{"class":2684,"line":2862},[13797,13801,13805,13809,13813,13818,13822,13826,13830,13835,13839,13844,13850,13855,13861,13865,13869,13873,13877,13881,13885,13889,13894],{"type":25,"tag":2682,"props":13798,"children":13799},{"style":3004},[13800],{"type":31,"value":13750},{"type":25,"tag":2682,"props":13802,"children":13803},{"style":2700},[13804],{"type":31,"value":2703},{"type":25,"tag":2682,"props":13806,"children":13807},{"style":3004},[13808],{"type":31,"value":13196},{"type":25,"tag":2682,"props":13810,"children":13811},{"style":2700},[13812],{"type":31,"value":2968},{"type":25,"tag":2682,"props":13814,"children":13815},{"style":3004},[13816],{"type":31,"value":13817}," ctx",{"type":25,"tag":2682,"props":13819,"children":13820},{"style":2700},[13821],{"type":31,"value":2703},{"type":25,"tag":2682,"props":13823,"children":13824},{"style":3004},[13825],{"type":31,"value":13196},{"type":25,"tag":2682,"props":13827,"children":13828},{"style":2700},[13829],{"type":31,"value":2703},{"type":25,"tag":2682,"props":13831,"children":13832},{"style":2852},[13833],{"type":31,"value":13834},"replace",{"type":25,"tag":2682,"props":13836,"children":13837},{"style":2700},[13838],{"type":31,"value":3076},{"type":25,"tag":2682,"props":13840,"children":13841},{"style":3293},[13842],{"type":31,"value":13843},"/",{"type":25,"tag":2682,"props":13845,"children":13847},{"style":13846},"--shiki-default:#6872AB",[13848],{"type":31,"value":13849},"\\d",{"type":25,"tag":2682,"props":13851,"children":13852},{"style":5609},[13853],{"type":31,"value":13854},"+",{"type":25,"tag":2682,"props":13856,"children":13858},{"style":13857},"--shiki-default:#C4704F",[13859],{"type":31,"value":13860},"px",{"type":25,"tag":2682,"props":13862,"children":13863},{"style":3293},[13864],{"type":31,"value":13843},{"type":25,"tag":2682,"props":13866,"children":13867},{"style":2700},[13868],{"type":31,"value":2921},{"type":25,"tag":2682,"props":13870,"children":13871},{"style":3293},[13872],{"type":31,"value":13205},{"type":25,"tag":2682,"props":13874,"children":13875},{"style":2688},[13876],{"type":31,"value":13215},{"type":25,"tag":2682,"props":13878,"children":13879},{"style":4759},[13880],{"type":31,"value":13776},{"type":25,"tag":2682,"props":13882,"children":13883},{"style":2688},[13884],{"type":31,"value":6882},{"type":25,"tag":2682,"props":13886,"children":13887},{"style":4759},[13888],{"type":31,"value":13860},{"type":25,"tag":2682,"props":13890,"children":13891},{"style":3293},[13892],{"type":31,"value":13893},"`",{"type":25,"tag":2682,"props":13895,"children":13896},{"style":2700},[13897],{"type":31,"value":2978},{"type":25,"tag":2682,"props":13899,"children":13900},{"class":2684,"line":2870},[13901,13905,13909,13913,13917,13921,13925,13929,13933,13937,13941,13946,13950,13954,13958,13962,13966,13971],{"type":25,"tag":2682,"props":13902,"children":13903},{"style":3004},[13904],{"type":31,"value":13750},{"type":25,"tag":2682,"props":13906,"children":13907},{"style":2700},[13908],{"type":31,"value":2703},{"type":25,"tag":2682,"props":13910,"children":13911},{"style":2852},[13912],{"type":31,"value":13498},{"type":25,"tag":2682,"props":13914,"children":13915},{"style":2700},[13916],{"type":31,"value":3076},{"type":25,"tag":2682,"props":13918,"children":13919},{"style":3004},[13920],{"type":31,"value":13313},{"type":25,"tag":2682,"props":13922,"children":13923},{"style":2700},[13924],{"type":31,"value":2921},{"type":25,"tag":2682,"props":13926,"children":13927},{"style":3004},[13928],{"type":31,"value":13369},{"type":25,"tag":2682,"props":13930,"children":13931},{"style":2762},[13932],{"type":31,"value":13383},{"type":25,"tag":2682,"props":13934,"children":13935},{"style":3004},[13936],{"type":31,"value":13676},{"type":25,"tag":2682,"props":13938,"children":13939},{"style":2762},[13940],{"type":31,"value":13392},{"type":25,"tag":2682,"props":13942,"children":13943},{"style":5609},[13944],{"type":31,"value":13945}," 0.35",{"type":25,"tag":2682,"props":13947,"children":13948},{"style":2700},[13949],{"type":31,"value":2921},{"type":25,"tag":2682,"props":13951,"children":13952},{"style":3004},[13953],{"type":31,"value":13667},{"type":25,"tag":2682,"props":13955,"children":13956},{"style":2762},[13957],{"type":31,"value":10172},{"type":25,"tag":2682,"props":13959,"children":13960},{"style":3004},[13961],{"type":31,"value":13676},{"type":25,"tag":2682,"props":13963,"children":13964},{"style":2762},[13965],{"type":31,"value":13392},{"type":25,"tag":2682,"props":13967,"children":13968},{"style":5609},[13969],{"type":31,"value":13970}," 0.3",{"type":25,"tag":2682,"props":13972,"children":13973},{"style":2700},[13974],{"type":31,"value":2978},{"type":25,"tag":2682,"props":13976,"children":13977},{"class":2684,"line":2981},[13978,13982,13986,13991],{"type":25,"tag":2682,"props":13979,"children":13980},{"style":3004},[13981],{"type":31,"value":13750},{"type":25,"tag":2682,"props":13983,"children":13984},{"style":2700},[13985],{"type":31,"value":2703},{"type":25,"tag":2682,"props":13987,"children":13988},{"style":2852},[13989],{"type":31,"value":13990},"restore",{"type":25,"tag":2682,"props":13992,"children":13993},{"style":2700},[13994],{"type":31,"value":9595},{"type":25,"tag":2682,"props":13996,"children":13997},{"class":2684,"line":2990},[13998,14003,14008,14012,14016,14020,14024,14028,14032,14036,14040],{"type":25,"tag":2682,"props":13999,"children":14000},{"style":2700},[14001],{"type":31,"value":14002},"  }",{"type":25,"tag":2682,"props":14004,"children":14005},{"style":2688},[14006],{"type":31,"value":14007}," else",{"type":25,"tag":2682,"props":14009,"children":14010},{"style":2688},[14011],{"type":31,"value":4368},{"type":25,"tag":2682,"props":14013,"children":14014},{"style":2700},[14015],{"type":31,"value":3001},{"type":25,"tag":2682,"props":14017,"children":14018},{"style":3004},[14019],{"type":31,"value":13533},{"type":25,"tag":2682,"props":14021,"children":14022},{"style":2700},[14023],{"type":31,"value":2703},{"type":25,"tag":2682,"props":14025,"children":14026},{"style":2852},[14027],{"type":31,"value":13717},{"type":25,"tag":2682,"props":14029,"children":14030},{"style":2700},[14031],{"type":31,"value":3076},{"type":25,"tag":2682,"props":14033,"children":14034},{"style":3004},[14035],{"type":31,"value":13313},{"type":25,"tag":2682,"props":14037,"children":14038},{"style":2700},[14039],{"type":31,"value":13730},{"type":25,"tag":2682,"props":14041,"children":14042},{"style":2700},[14043],{"type":31,"value":12909},{"type":25,"tag":2682,"props":14045,"children":14046},{"class":2684,"line":3044},[14047],{"type":25,"tag":2682,"props":14048,"children":14049},{"style":3931},[14050],{"type":31,"value":14051},"    // 長音・括弧等: 90度回転\n",{"type":25,"tag":2682,"props":14053,"children":14054},{"class":2684,"line":3065},[14055,14059,14063,14067],{"type":25,"tag":2682,"props":14056,"children":14057},{"style":3004},[14058],{"type":31,"value":13750},{"type":25,"tag":2682,"props":14060,"children":14061},{"style":2700},[14062],{"type":31,"value":2703},{"type":25,"tag":2682,"props":14064,"children":14065},{"style":2852},[14066],{"type":31,"value":13759},{"type":25,"tag":2682,"props":14068,"children":14069},{"style":2700},[14070],{"type":31,"value":9595},{"type":25,"tag":2682,"props":14072,"children":14073},{"class":2684,"line":3088},[14074,14078,14082,14087,14091,14095,14099,14103,14107,14111,14115,14119],{"type":25,"tag":2682,"props":14075,"children":14076},{"style":3004},[14077],{"type":31,"value":13750},{"type":25,"tag":2682,"props":14079,"children":14080},{"style":2700},[14081],{"type":31,"value":2703},{"type":25,"tag":2682,"props":14083,"children":14084},{"style":2852},[14085],{"type":31,"value":14086},"translate",{"type":25,"tag":2682,"props":14088,"children":14089},{"style":2700},[14090],{"type":31,"value":3076},{"type":25,"tag":2682,"props":14092,"children":14093},{"style":3004},[14094],{"type":31,"value":12922},{"type":25,"tag":2682,"props":14096,"children":14097},{"style":2700},[14098],{"type":31,"value":2921},{"type":25,"tag":2682,"props":14100,"children":14101},{"style":3004},[14102],{"type":31,"value":13667},{"type":25,"tag":2682,"props":14104,"children":14105},{"style":2762},[14106],{"type":31,"value":10172},{"type":25,"tag":2682,"props":14108,"children":14109},{"style":3004},[14110],{"type":31,"value":13676},{"type":25,"tag":2682,"props":14112,"children":14113},{"style":2762},[14114],{"type":31,"value":13392},{"type":25,"tag":2682,"props":14116,"children":14117},{"style":5609},[14118],{"type":31,"value":13945},{"type":25,"tag":2682,"props":14120,"children":14121},{"style":2700},[14122],{"type":31,"value":2978},{"type":25,"tag":2682,"props":14124,"children":14125},{"class":2684,"line":3097},[14126,14130,14134,14139,14143,14147,14151,14156,14161,14165],{"type":25,"tag":2682,"props":14127,"children":14128},{"style":3004},[14129],{"type":31,"value":13750},{"type":25,"tag":2682,"props":14131,"children":14132},{"style":2700},[14133],{"type":31,"value":2703},{"type":25,"tag":2682,"props":14135,"children":14136},{"style":2852},[14137],{"type":31,"value":14138},"rotate",{"type":25,"tag":2682,"props":14140,"children":14141},{"style":2700},[14142],{"type":31,"value":3076},{"type":25,"tag":2682,"props":14144,"children":14145},{"style":3004},[14146],{"type":31,"value":13067},{"type":25,"tag":2682,"props":14148,"children":14149},{"style":2700},[14150],{"type":31,"value":2703},{"type":25,"tag":2682,"props":14152,"children":14153},{"style":3004},[14154],{"type":31,"value":14155},"PI",{"type":25,"tag":2682,"props":14157,"children":14158},{"style":2762},[14159],{"type":31,"value":14160}," /",{"type":25,"tag":2682,"props":14162,"children":14163},{"style":5609},[14164],{"type":31,"value":5667},{"type":25,"tag":2682,"props":14166,"children":14167},{"style":2700},[14168],{"type":31,"value":2978},{"type":25,"tag":2682,"props":14170,"children":14171},{"class":2684,"line":3105},[14172,14176,14180,14184,14188,14192,14196,14200,14204,14208],{"type":25,"tag":2682,"props":14173,"children":14174},{"style":3004},[14175],{"type":31,"value":13750},{"type":25,"tag":2682,"props":14177,"children":14178},{"style":2700},[14179],{"type":31,"value":2703},{"type":25,"tag":2682,"props":14181,"children":14182},{"style":2852},[14183],{"type":31,"value":13498},{"type":25,"tag":2682,"props":14185,"children":14186},{"style":2700},[14187],{"type":31,"value":3076},{"type":25,"tag":2682,"props":14189,"children":14190},{"style":3004},[14191],{"type":31,"value":13313},{"type":25,"tag":2682,"props":14193,"children":14194},{"style":2700},[14195],{"type":31,"value":2921},{"type":25,"tag":2682,"props":14197,"children":14198},{"style":5609},[14199],{"type":31,"value":9355},{"type":25,"tag":2682,"props":14201,"children":14202},{"style":2700},[14203],{"type":31,"value":2921},{"type":25,"tag":2682,"props":14205,"children":14206},{"style":5609},[14207],{"type":31,"value":9355},{"type":25,"tag":2682,"props":14209,"children":14210},{"style":2700},[14211],{"type":31,"value":2978},{"type":25,"tag":2682,"props":14213,"children":14214},{"class":2684,"line":3139},[14215,14219,14223,14227],{"type":25,"tag":2682,"props":14216,"children":14217},{"style":3004},[14218],{"type":31,"value":13750},{"type":25,"tag":2682,"props":14220,"children":14221},{"style":2700},[14222],{"type":31,"value":2703},{"type":25,"tag":2682,"props":14224,"children":14225},{"style":2852},[14226],{"type":31,"value":13990},{"type":25,"tag":2682,"props":14228,"children":14229},{"style":2700},[14230],{"type":31,"value":9595},{"type":25,"tag":2682,"props":14232,"children":14233},{"class":2684,"line":3147},[14234,14238,14242],{"type":25,"tag":2682,"props":14235,"children":14236},{"style":2700},[14237],{"type":31,"value":14002},{"type":25,"tag":2682,"props":14239,"children":14240},{"style":2688},[14241],{"type":31,"value":14007},{"type":25,"tag":2682,"props":14243,"children":14244},{"style":2700},[14245],{"type":31,"value":12909},{"type":25,"tag":2682,"props":14247,"children":14248},{"class":2684,"line":3205},[14249,14253,14257,14261,14265,14269,14273,14277,14281,14285],{"type":25,"tag":2682,"props":14250,"children":14251},{"style":3004},[14252],{"type":31,"value":13750},{"type":25,"tag":2682,"props":14254,"children":14255},{"style":2700},[14256],{"type":31,"value":2703},{"type":25,"tag":2682,"props":14258,"children":14259},{"style":2852},[14260],{"type":31,"value":13498},{"type":25,"tag":2682,"props":14262,"children":14263},{"style":2700},[14264],{"type":31,"value":3076},{"type":25,"tag":2682,"props":14266,"children":14267},{"style":3004},[14268],{"type":31,"value":13313},{"type":25,"tag":2682,"props":14270,"children":14271},{"style":2700},[14272],{"type":31,"value":2921},{"type":25,"tag":2682,"props":14274,"children":14275},{"style":3004},[14276],{"type":31,"value":13369},{"type":25,"tag":2682,"props":14278,"children":14279},{"style":2700},[14280],{"type":31,"value":2921},{"type":25,"tag":2682,"props":14282,"children":14283},{"style":3004},[14284],{"type":31,"value":13667},{"type":25,"tag":2682,"props":14286,"children":14287},{"style":2700},[14288],{"type":31,"value":2978},{"type":25,"tag":2682,"props":14290,"children":14291},{"class":2684,"line":3213},[14292],{"type":25,"tag":2682,"props":14293,"children":14294},{"style":2700},[14295],{"type":31,"value":14296},"  }\n",{"type":25,"tag":2682,"props":14298,"children":14299},{"class":2684,"line":9523},[14300],{"type":25,"tag":2682,"props":14301,"children":14302},{"style":2700},[14303],{"type":31,"value":3219},{"type":25,"tag":313,"props":14305,"children":14307},{"id":14306},"句読点は右上に半分サイズで置く",[14308],{"type":31,"value":14306},{"type":25,"tag":33,"props":14310,"children":14311},{},[14312,14314,14319,14321,14327],{"type":31,"value":14313},"日本の伝統的な縦書き組版では、「、」と「。」は ",{"type":25,"tag":103,"props":14315,"children":14316},{},[14317],{"type":31,"value":14318},"マス目の右上に小さく",{"type":31,"value":14320}," 配置します。詠み人ではサイズを 50%、座標を ",{"type":25,"tag":529,"props":14322,"children":14324},{"className":14323},[],[14325],{"type":31,"value":14326},"(x + fontSize * 0.35, y - fontSize * 0.3)",{"type":31,"value":14328}," に補正することで、この配置を再現しました。",{"type":25,"tag":313,"props":14330,"children":14332},{"id":14331},"長音括弧は90度回転",[14333],{"type":31,"value":14334},"長音・括弧は90度回転",{"type":25,"tag":33,"props":14336,"children":14337},{},[14338,14340,14346,14348,14353,14355,14360],{"type":31,"value":14339},"「ー」「〜」「「」「（）」などは、横書きでそのまま描くと縦書きの文脈で読みにくくなります。詠み人では ",{"type":25,"tag":529,"props":14341,"children":14343},{"className":14342},[],[14344],{"type":31,"value":14345},"ctx.rotate(Math.PI / 2)",{"type":31,"value":14347}," で ",{"type":25,"tag":103,"props":14349,"children":14350},{},[14351],{"type":31,"value":14352},"90度時計回りに回転",{"type":31,"value":14354}," して描画しています。",{"type":25,"tag":529,"props":14356,"children":14358},{"className":14357},[],[14359],{"type":31,"value":14086},{"type":31,"value":14361}," で座標を中心にずらしてから回転させるのがコツです。",{"type":25,"tag":33,"props":14363,"children":14364},{},[14365,14367,14372],{"type":31,"value":14366},"このあたりは日本語組版の JIS X 4051 や W3C の日本語組版要件を全部実装しようとすると際限がないので、",{"type":25,"tag":103,"props":14368,"children":14369},{},[14370],{"type":31,"value":14371},"詠み人に実際に入力される文字種に限って割り切る",{"type":31,"value":14373}," 方針を取りました。",{"type":25,"tag":313,"props":14375,"children":14377},{"id":14376},"フォントによって位置が異なる",[14378],{"type":31,"value":14376},{"type":25,"tag":33,"props":14380,"children":14381},{},[14382,14384,14389],{"type":31,"value":14383},"回転処理をさらに厄介にするのが、",{"type":25,"tag":103,"props":14385,"children":14386},{},[14387],{"type":31,"value":14388},"フォントごとにグリフの中心位置が微妙にずれる",{"type":31,"value":14390}," という問題です。詠み人では明朝（Noto Serif JP）・力弱（851CHIKARA-YOWAKU）・行書（AoyagiKouzanT）・隷書（AoyagiReisyosimo）の4フォントを選択できますが、このうち隷書だけは回転文字の描画位置がずれます。",{"type":25,"tag":33,"props":14392,"children":14393},{},[14394],{"type":31,"value":14395},"実装ではフォント ID を見て隷書のときだけ X 軸方向にオフセットを入れています。",{"type":25,"tag":524,"props":14397,"children":14399},{"className":12855,"code":14398,"language":12857,"meta":8,"style":8},"const offsetX = fontId === 'reisyo' ? -fontSize * 0.15 : 0\nctx.translate(x + offsetX, y - fontSize * 0.35)\nctx.rotate(Math.PI / 2)\n",[14400],{"type":25,"tag":529,"props":14401,"children":14402},{"__ignoreMap":8},[14403,14470,14530],{"type":25,"tag":2682,"props":14404,"children":14405},{"class":2684,"line":18},[14406,14410,14415,14419,14423,14428,14432,14437,14441,14446,14451,14455,14460,14465],{"type":25,"tag":2682,"props":14407,"children":14408},{"style":2762},[14409],{"type":31,"value":13528},{"type":25,"tag":2682,"props":14411,"children":14412},{"style":3004},[14413],{"type":31,"value":14414},"offsetX",{"type":25,"tag":2682,"props":14416,"children":14417},{"style":2700},[14418],{"type":31,"value":2968},{"type":25,"tag":2682,"props":14420,"children":14421},{"style":3004},[14422],{"type":31,"value":13414},{"type":25,"tag":2682,"props":14424,"children":14425},{"style":2762},[14426],{"type":31,"value":14427}," === ",{"type":25,"tag":2682,"props":14429,"children":14430},{"style":3293},[14431],{"type":31,"value":13556},{"type":25,"tag":2682,"props":14433,"children":14434},{"style":4759},[14435],{"type":31,"value":14436},"reisyo",{"type":25,"tag":2682,"props":14438,"children":14439},{"style":3293},[14440],{"type":31,"value":13556},{"type":25,"tag":2682,"props":14442,"children":14443},{"style":2762},[14444],{"type":31,"value":14445}," ? -",{"type":25,"tag":2682,"props":14447,"children":14448},{"style":3004},[14449],{"type":31,"value":14450},"fontSize",{"type":25,"tag":2682,"props":14452,"children":14453},{"style":2762},[14454],{"type":31,"value":12946},{"type":25,"tag":2682,"props":14456,"children":14457},{"style":5609},[14458],{"type":31,"value":14459},"0.15",{"type":25,"tag":2682,"props":14461,"children":14462},{"style":2762},[14463],{"type":31,"value":14464}," : ",{"type":25,"tag":2682,"props":14466,"children":14467},{"style":5609},[14468],{"type":31,"value":14469},"0\n",{"type":25,"tag":2682,"props":14471,"children":14472},{"class":2684,"line":1149},[14473,14477,14481,14485,14489,14493,14497,14502,14506,14510,14514,14518,14522,14526],{"type":25,"tag":2682,"props":14474,"children":14475},{"style":3004},[14476],{"type":31,"value":13351},{"type":25,"tag":2682,"props":14478,"children":14479},{"style":2700},[14480],{"type":31,"value":2703},{"type":25,"tag":2682,"props":14482,"children":14483},{"style":2852},[14484],{"type":31,"value":14086},{"type":25,"tag":2682,"props":14486,"children":14487},{"style":2700},[14488],{"type":31,"value":3076},{"type":25,"tag":2682,"props":14490,"children":14491},{"style":3004},[14492],{"type":31,"value":12922},{"type":25,"tag":2682,"props":14494,"children":14495},{"style":2762},[14496],{"type":31,"value":13383},{"type":25,"tag":2682,"props":14498,"children":14499},{"style":3004},[14500],{"type":31,"value":14501}," offsetX",{"type":25,"tag":2682,"props":14503,"children":14504},{"style":2700},[14505],{"type":31,"value":2921},{"type":25,"tag":2682,"props":14507,"children":14508},{"style":3004},[14509],{"type":31,"value":13667},{"type":25,"tag":2682,"props":14511,"children":14512},{"style":2762},[14513],{"type":31,"value":10172},{"type":25,"tag":2682,"props":14515,"children":14516},{"style":3004},[14517],{"type":31,"value":13676},{"type":25,"tag":2682,"props":14519,"children":14520},{"style":2762},[14521],{"type":31,"value":13392},{"type":25,"tag":2682,"props":14523,"children":14524},{"style":5609},[14525],{"type":31,"value":13945},{"type":25,"tag":2682,"props":14527,"children":14528},{"style":2700},[14529],{"type":31,"value":2978},{"type":25,"tag":2682,"props":14531,"children":14532},{"class":2684,"line":1159},[14533,14537,14541,14545,14549,14553,14557,14561,14565,14569],{"type":25,"tag":2682,"props":14534,"children":14535},{"style":3004},[14536],{"type":31,"value":13351},{"type":25,"tag":2682,"props":14538,"children":14539},{"style":2700},[14540],{"type":31,"value":2703},{"type":25,"tag":2682,"props":14542,"children":14543},{"style":2852},[14544],{"type":31,"value":14138},{"type":25,"tag":2682,"props":14546,"children":14547},{"style":2700},[14548],{"type":31,"value":3076},{"type":25,"tag":2682,"props":14550,"children":14551},{"style":3004},[14552],{"type":31,"value":13067},{"type":25,"tag":2682,"props":14554,"children":14555},{"style":2700},[14556],{"type":31,"value":2703},{"type":25,"tag":2682,"props":14558,"children":14559},{"style":3004},[14560],{"type":31,"value":14155},{"type":25,"tag":2682,"props":14562,"children":14563},{"style":2762},[14564],{"type":31,"value":14160},{"type":25,"tag":2682,"props":14566,"children":14567},{"style":5609},[14568],{"type":31,"value":5667},{"type":25,"tag":2682,"props":14570,"children":14571},{"style":2700},[14572],{"type":31,"value":2978},{"type":25,"tag":33,"props":14574,"children":14575},{},[14576,14582,14584,14590,14592,14597],{"type":25,"tag":529,"props":14577,"children":14579},{"className":14578},[],[14580],{"type":31,"value":14581},"-fontSize * 0.15",{"type":31,"value":14583}," という補正値は、隷書フォントの「ー」や「〜」を実際に描画しながら目視で合わせた数値です。フォント間でグリフメトリクスが統一されていないため、汎用的な計算式を導出するのは難しく、フォントを追加するたびにこの分岐が増える可能性があります。現状は4フォント固定なので ",{"type":25,"tag":529,"props":14585,"children":14587},{"className":14586},[],[14588],{"type":31,"value":14589},"fontId === 'reisyo'",{"type":31,"value":14591}," の1分岐で収まっていますが、フォント選択肢を増やす場合はオフセットのテーブル化が必要になるでしょう。私は ",{"type":25,"tag":103,"props":14593,"children":14594},{},[14595],{"type":31,"value":14596},"必要最低限で実装する",{"type":31,"value":14598}," という原則で実装を進めています。現時点で複雑にならないことが最優先です。",{"type":25,"tag":26,"props":14600,"children":14602},{"id":14601},"フォントの読み込み待機を忘れずに",[14603],{"type":31,"value":14601},{"type":25,"tag":33,"props":14605,"children":14606},{},[14607,14609,14614,14616,14621],{"type":31,"value":14608},"Canvas 描画で地味にハマるのが「",{"type":25,"tag":103,"props":14610,"children":14611},{},[14612],{"type":31,"value":14613},"指定したフォントがまだ読み込まれていないとデフォルトフォントで描画される",{"type":31,"value":14615},"」問題です。特に Google Fonts のような外部フォントを使う場合、初回描画時に ",{"type":25,"tag":529,"props":14617,"children":14619},{"className":14618},[],[14620],{"type":31,"value":13498},{"type":31,"value":14622}," が意図しないフォントで動き、2回目以降は正常、という不安定な挙動を起こします。",{"type":25,"tag":33,"props":14624,"children":14625},{},[14626,14628,14634],{"type":31,"value":14627},"詠み人では描画開始前に ",{"type":25,"tag":529,"props":14629,"children":14631},{"className":14630},[],[14632],{"type":31,"value":14633},"document.fonts.load()",{"type":31,"value":14635}," を明示的に待機しています。",{"type":25,"tag":524,"props":14637,"children":14639},{"className":12855,"code":14638,"language":12857,"meta":8,"style":8},"await document.fonts.load(`400 46px ${fontFamily}`)\nawait document.fonts.load(`300 13px ${fontFamily}`)\n",[14640],{"type":25,"tag":529,"props":14641,"children":14642},{"__ignoreMap":8},[14643,14707],{"type":25,"tag":2682,"props":14644,"children":14645},{"class":2684,"line":18},[14646,14651,14656,14660,14665,14669,14674,14678,14682,14687,14691,14695,14699,14703],{"type":25,"tag":2682,"props":14647,"children":14648},{"style":2688},[14649],{"type":31,"value":14650},"await",{"type":25,"tag":2682,"props":14652,"children":14653},{"style":3004},[14654],{"type":31,"value":14655}," document",{"type":25,"tag":2682,"props":14657,"children":14658},{"style":2700},[14659],{"type":31,"value":2703},{"type":25,"tag":2682,"props":14661,"children":14662},{"style":3004},[14663],{"type":31,"value":14664},"fonts",{"type":25,"tag":2682,"props":14666,"children":14667},{"style":2700},[14668],{"type":31,"value":2703},{"type":25,"tag":2682,"props":14670,"children":14671},{"style":2852},[14672],{"type":31,"value":14673},"load",{"type":25,"tag":2682,"props":14675,"children":14676},{"style":2700},[14677],{"type":31,"value":3076},{"type":25,"tag":2682,"props":14679,"children":14680},{"style":3293},[14681],{"type":31,"value":13893},{"type":25,"tag":2682,"props":14683,"children":14684},{"style":4759},[14685],{"type":31,"value":14686},"400 46px ",{"type":25,"tag":2682,"props":14688,"children":14689},{"style":2688},[14690],{"type":31,"value":13215},{"type":25,"tag":2682,"props":14692,"children":14693},{"style":4759},[14694],{"type":31,"value":13237},{"type":25,"tag":2682,"props":14696,"children":14697},{"style":2688},[14698],{"type":31,"value":6882},{"type":25,"tag":2682,"props":14700,"children":14701},{"style":3293},[14702],{"type":31,"value":13893},{"type":25,"tag":2682,"props":14704,"children":14705},{"style":2700},[14706],{"type":31,"value":2978},{"type":25,"tag":2682,"props":14708,"children":14709},{"class":2684,"line":1149},[14710,14714,14718,14722,14726,14730,14734,14738,14742,14747,14751,14755,14759,14763],{"type":25,"tag":2682,"props":14711,"children":14712},{"style":2688},[14713],{"type":31,"value":14650},{"type":25,"tag":2682,"props":14715,"children":14716},{"style":3004},[14717],{"type":31,"value":14655},{"type":25,"tag":2682,"props":14719,"children":14720},{"style":2700},[14721],{"type":31,"value":2703},{"type":25,"tag":2682,"props":14723,"children":14724},{"style":3004},[14725],{"type":31,"value":14664},{"type":25,"tag":2682,"props":14727,"children":14728},{"style":2700},[14729],{"type":31,"value":2703},{"type":25,"tag":2682,"props":14731,"children":14732},{"style":2852},[14733],{"type":31,"value":14673},{"type":25,"tag":2682,"props":14735,"children":14736},{"style":2700},[14737],{"type":31,"value":3076},{"type":25,"tag":2682,"props":14739,"children":14740},{"style":3293},[14741],{"type":31,"value":13893},{"type":25,"tag":2682,"props":14743,"children":14744},{"style":4759},[14745],{"type":31,"value":14746},"300 13px ",{"type":25,"tag":2682,"props":14748,"children":14749},{"style":2688},[14750],{"type":31,"value":13215},{"type":25,"tag":2682,"props":14752,"children":14753},{"style":4759},[14754],{"type":31,"value":13237},{"type":25,"tag":2682,"props":14756,"children":14757},{"style":2688},[14758],{"type":31,"value":6882},{"type":25,"tag":2682,"props":14760,"children":14761},{"style":3293},[14762],{"type":31,"value":13893},{"type":25,"tag":2682,"props":14764,"children":14765},{"style":2700},[14766],{"type":31,"value":2978},{"type":25,"tag":33,"props":14768,"children":14769},{},[14770,14772,14778,14780,14786],{"type":31,"value":14771},"本文用（46px）と作者名用（13px）の両方をロードしないと、作者名だけがデフォルトフォントになる、という微妙なバグが出ます。フォントウェイトが異なる（本文は ",{"type":25,"tag":529,"props":14773,"children":14775},{"className":14774},[],[14776],{"type":31,"value":14777},"400",{"type":31,"value":14779},"、作者名は ",{"type":25,"tag":529,"props":14781,"children":14783},{"className":14782},[],[14784],{"type":31,"value":14785},"300",{"type":31,"value":14787},"）ため、それぞれ個別に await しています。",{"type":25,"tag":26,"props":14789,"children":14790},{"id":1746},[14791],{"type":31,"value":1746},{"type":25,"tag":53,"props":14793,"children":14794},{},[14795,14807,14812,14824],{"type":25,"tag":57,"props":14796,"children":14797},{},[14798,14800,14805],{"type":31,"value":14799},"Canvas での縦書き実装は、",{"type":25,"tag":103,"props":14801,"children":14802},{},[14803],{"type":31,"value":14804},"画像生成ツール",{"type":31,"value":14806}," という用途では CSS より素直",{"type":25,"tag":57,"props":14808,"children":14809},{},[14810],{"type":31,"value":14811},"句読点は 50% サイズで右上配置、長音・括弧は 90° 回転、という特殊文字処理が必須",{"type":25,"tag":57,"props":14813,"children":14814},{},[14815,14817,14822],{"type":31,"value":14816},"日本語組版の全仕様を追うのではなく、",{"type":25,"tag":103,"props":14818,"children":14819},{},[14820],{"type":31,"value":14821},"自分のツールに実際に入力される文字",{"type":31,"value":14823}," に絞って割り切る",{"type":25,"tag":57,"props":14825,"children":14826},{},[14827,14832],{"type":25,"tag":529,"props":14828,"children":14830},{"className":14829},[],[14831],{"type":31,"value":14633},{"type":31,"value":14833}," を複数サイズで await することで、フォント未ロード起因のバグを回避",{"type":25,"tag":33,"props":14835,"children":14836},{},[14837],{"type":31,"value":14838},"縦書きは奥が深い世界ですが、画像生成用途に限れば Canvas で十分戦えます。",{"type":25,"tag":5239,"props":14840,"children":14841},{},[14842],{"type":31,"value":5243},{"title":8,"searchDepth":1149,"depth":1149,"links":14844},[14845,14846,14847,14849,14850,14855,14856],{"id":28,"depth":1149,"text":28},{"id":11319,"depth":1149,"text":11319},{"id":12815,"depth":1149,"text":14848},"なぜ CSS writing-mode を選ばなかったのか",{"id":12845,"depth":1149,"text":12845},{"id":13477,"depth":1149,"text":13480,"children":14851},[14852,14853,14854],{"id":14306,"depth":1159,"text":14306},{"id":14331,"depth":1159,"text":14334},{"id":14376,"depth":1159,"text":14376},{"id":14601,"depth":1149,"text":14601},{"id":1746,"depth":1149,"text":1746},"content:articles:tech:development:yominchu-vertical-canvas.md","articles/tech/development/yominchu-vertical-canvas.md","articles/tech/development/yominchu-vertical-canvas",{"_path":14861,"_dir":14862,"_draft":7,"_partial":7,"_locale":8,"title":14863,"description":14864,"date":14865,"tags":14866,"rowTypeId":18,"sitemap":14869,"body":14870,"_type":1180,"_id":15202,"_source":1182,"_file":15203,"_stem":15204,"_extension":1185},"/articles/tech/mindset/how-to-practice-tech-stack","mindset","銀の弾丸を求めて迷走した私の失敗談。個人開発でオーバーエンジニアリングして気づいた「技術のコスト」の話","本や記事で学んだ「理想の設計」を試すと思ったようにいかないことが多いです。実務で失敗すると多大なコストを支払うことになりますが、個人開発であれば安心して試せます。安心して試すことで技術の限界を学ぶことができ、結果的に実務で活かせる経験となります。","2025-12-28",[1805,11668,14867,12435,14868],"技術選定","オーバーエンジニアリング",{"loc":14861,"lastmod":14865,"priority":18},{"type":22,"children":14871,"toc":15186},[14872,14876,14881,14888,14893,14899,14904,14909,14914,14920,14925,14931,14936,14942,14954,14960,14965,15013,15018,15024,15029,15034,15039,15044,15049,15061,15067,15072,15120,15125,15131,15136,15162,15181],{"type":25,"tag":26,"props":14873,"children":14874},{"id":28},[14875],{"type":31,"value":28},{"type":25,"tag":33,"props":14877,"children":14878},{},[14879],{"type":31,"value":14880},"本で読んだ『理想の設計』をコードに落とし込んだはずなのに、なぜかコードを書く手が止まってしまう……。そんな経験はありませんか？ここでは失敗談をベースに学んだことをまとめていきます。",{"type":25,"tag":39,"props":14882,"children":14883},{},[14884],{"type":25,"tag":33,"props":14885,"children":14886},{},[14887],{"type":31,"value":14864},{"type":25,"tag":26,"props":14889,"children":14891},{"id":14890},"個人開発でオニオンアーキテクチャを導入して失敗した話",[14892],{"type":31,"value":14890},{"type":25,"tag":313,"props":14894,"children":14896},{"id":14895},"そもそもなぜ導入しようと思ったの失敗から始まった導入",[14897],{"type":31,"value":14898},"そもそもなぜ導入しようと思ったの？失敗から始まった導入",{"type":25,"tag":33,"props":14900,"children":14901},{},[14902],{"type":31,"value":14903},"導入以前に作ったプログラムでは1つのメソッドに1000行ぐらい実装するというヤンチャなことをしており、それが原因でバグの特定や機能拡張が困難になった経験があります。この経験から、ソフトウェア設計技術に関心を持つようになりました。この影響で徹底した責務の分離や拡張性の担保のためにオニオンアーキテクチャを導入しようと考えました。",{"type":25,"tag":313,"props":14905,"children":14907},{"id":14906},"結果どうなったのか",[14908],{"type":31,"value":14906},{"type":25,"tag":33,"props":14910,"children":14911},{},[14912],{"type":31,"value":14913},"確かに責務が分離され、一定の効果を挙げましたが副作用に悩まされました。",{"type":25,"tag":313,"props":14915,"children":14917},{"id":14916},"最大の失敗理由開発物と技術のマッチング",[14918],{"type":31,"value":14919},"最大の失敗理由：開発物と技術のマッチング",{"type":25,"tag":33,"props":14921,"children":14922},{},[14923],{"type":31,"value":14924},"当時開発したものは一般的な企業製のアプリと比較して小規模かつドメインが単純なものでした。しかし、オニオンアーキテクチャは、チーム開発や長期運用を前提とした「変更耐性」を重視する設計です。\nそのため、変更頻度や人の出入りが少ない小規模な個人開発では、その恩恵を受ける前に構造の複雑さというコストが先に顕在化しました。また、プロダクトの性質上GUIの実装とデータ出力や入力処理が複雑化しやすかったためオニオンアーキテクチャだけでは担保できない部分で問題が発生することもありました。",{"type":25,"tag":26,"props":14926,"children":14928},{"id":14927},"でも技術を試したいなら個人開発が一番安全",[14929],{"type":31,"value":14930},"でも、技術を試したいなら個人開発が一番安全",{"type":25,"tag":33,"props":14932,"children":14933},{},[14934],{"type":31,"value":14935},"こう感じている理由を説明します。",{"type":25,"tag":313,"props":14937,"children":14939},{"id":14938},"個人開発なら失敗しても問題ない",[14940],{"type":31,"value":14941},"個人開発なら失敗しても問題ない！",{"type":25,"tag":33,"props":14943,"children":14944},{},[14945,14947,14952],{"type":31,"value":14946},"失敗談は個人開発のもので、技術的なコスト以外は発生しませんでした。もし実務上であった場合はどうなるでしょうか。実務で失敗するとチーム全体が疲弊しますが、個人開発なら、途中で「やっぱりこの設計はやめた！」と全部壊しても誰にも迷惑をかけません。この ",{"type":25,"tag":103,"props":14948,"children":14949},{},[14950],{"type":31,"value":14951},"「設計の破壊的変更を許容できる場所」",{"type":31,"value":14953}," は個人開発でなければ得られない場所だと思います。失敗できるという心理的安全性は学習のモチベーションにも大きく関わると思います(例えば、AzureやAWSなどクラウドの学習はお金がかかるので心理的ハードルが高い)。このように、個人開発は失敗しても大きな代償を払わずに済みます。\nだからこそ、実務では許されないような「極端な選択」をあえて試すことができます。",{"type":25,"tag":335,"props":14955,"children":14957},{"id":14956},"失敗しても良いなら原理主義的な学習実践ができる",[14958],{"type":31,"value":14959},"失敗しても良いなら原理主義的な学習・実践ができる",{"type":25,"tag":33,"props":14961,"children":14962},{},[14963],{"type":31,"value":14964},"ここでいう「原理主義的」とは、実用性よりも設計原則を最優先する姿勢を指します。\n目的は「技術の限界を知ること」です。デメリットやコストを度外視して 100か0かの極端な実装を試すことで、その技術の真の境界線が見えてきます。原理主義的に実践することのメリット・デメリットを挙げます。",{"type":25,"tag":346,"props":14966,"children":14967},{},[14968,14992],{"type":25,"tag":352,"props":14969,"children":14970},{"v-slot:cons":8},[14971],{"type":25,"tag":53,"props":14972,"children":14973},{},[14974,14987],{"type":25,"tag":57,"props":14975,"children":14976},{},[14977,14979],{"type":31,"value":14978},"必ずしも実用的とは限らない\n",{"type":25,"tag":53,"props":14980,"children":14981},{},[14982],{"type":25,"tag":57,"props":14983,"children":14984},{},[14985],{"type":31,"value":14986},"経験が浅いと原理主義的に実装することが正義と誤解しやすい(所謂「あたまでっかち」な状態)",{"type":25,"tag":57,"props":14988,"children":14989},{},[14990],{"type":31,"value":14991},"本などの情報源に従わないといけないという意識で思考が放棄されやすい",{"type":25,"tag":352,"props":14993,"children":14994},{"v-slot:pros":8},[14995],{"type":25,"tag":53,"props":14996,"children":14997},{},[14998,15003,15008],{"type":25,"tag":57,"props":14999,"children":15000},{},[15001],{"type":31,"value":15002},"技術が解決する限界値を体感できる",{"type":25,"tag":57,"props":15004,"children":15005},{},[15006],{"type":31,"value":15007},"技術のメリット・デメリットが強く結果に現れる",{"type":25,"tag":57,"props":15009,"children":15010},{},[15011],{"type":31,"value":15012},"技術の定義や方法に従うため、答えがわかりやすい",{"type":25,"tag":33,"props":15014,"children":15015},{},[15016],{"type":31,"value":15017},"このように一長一短ではありますが、学習は選択肢を広げるためにあると思うので、試してみる価値はあると思います。",{"type":25,"tag":335,"props":15019,"children":15021},{"id":15020},"その一方実務では実利的な選択が求められる",[15022],{"type":31,"value":15023},"その一方、実務では実利的な選択が求められる",{"type":25,"tag":33,"props":15025,"children":15026},{},[15027],{"type":31,"value":15028},"実務ではコスト・パフォーマンスのバランスをとった実利的な選択を余儀なくされるため、このような原理主義的な学習はできません。チームの習熟度や運用コスト、納期を考慮し、35や67といった「泥臭い妥協点」を探る判断が求められます。では、そのバランス感覚はどのように身につけるかを考えた際に、個人開発で失敗した経験は単純な知識量以上に役立っていると感じています。",{"type":25,"tag":26,"props":15030,"children":15032},{"id":15031},"学ぶ時意識した方が良かったなと後悔したこと",[15033],{"type":31,"value":15031},{"type":25,"tag":33,"props":15035,"children":15036},{},[15037],{"type":31,"value":15038},"技術を学ぶ・導入する目的を明確にすることに意識を向けていましたが、それだけでは不十分で、その目的を達成するまでの「コスト・パフォーマンス」について意識すべきだったと思います。何か特定のトピックについて学ぶ際は基本的にその技術のメリットの部分に注目しがちでデメリットを過小評価してしまいました。",{"type":25,"tag":313,"props":15040,"children":15042},{"id":15041},"何かを得るために何かを犠牲にするという前提があると思った方が良かった",[15043],{"type":31,"value":15041},{"type":25,"tag":33,"props":15045,"children":15046},{},[15047],{"type":31,"value":15048},"「銀の弾丸は存在しない」という意識を常に持たなければいけないと感じています。かなり前に話題になったFizzBuzz Enterprise Editionというジョークコードからも分かりますが、「SOLID 原則」「Clean Architecture」などを盲目的に適用し、結果的に「疎結合・高凝集」などのパフォーマンス以上に「複雑さ」というコストが増してしまうことが現実に起こり得ます。",{"type":25,"tag":33,"props":15050,"children":15051},{},[15052,15054],{"type":31,"value":15053},"参考：",{"type":25,"tag":1354,"props":15055,"children":15058},{"href":15056,"rel":15057},"https://github.com/EnterpriseQualityCoding/FizzBuzzEnterpriseEdition",[1358],[15059],{"type":31,"value":15060},"FizzBuzz Enterprise Editionのリポジトリ",{"type":25,"tag":313,"props":15062,"children":15064},{"id":15063},"技術のメリットやデメリットの影響は開発するプロダクト組織によって異なること",[15065],{"type":31,"value":15066},"技術のメリットやデメリットの影響は開発するプロダクト・組織によって異なること",{"type":25,"tag":33,"props":15068,"children":15069},{},[15070],{"type":31,"value":15071},"本や記事に書かれている技術のメリット・デメリットは一般的に説明されています。例えば、「Dockerを導入するメリット・デメリット」について説明すると、",{"type":25,"tag":346,"props":15073,"children":15074},{},[15075,15096],{"type":25,"tag":352,"props":15076,"children":15077},{"v-slot:cons":8},[15078],{"type":25,"tag":53,"props":15079,"children":15080},{},[15081,15086,15091],{"type":25,"tag":57,"props":15082,"children":15083},{},[15084],{"type":31,"value":15085},"ビルド時間のオーバーヘッド",{"type":25,"tag":57,"props":15087,"children":15088},{},[15089],{"type":31,"value":15090},"PCが重くなる",{"type":25,"tag":57,"props":15092,"children":15093},{},[15094],{"type":31,"value":15095},"設定ファイル（Dockerfile等）の学習コスト",{"type":25,"tag":352,"props":15097,"children":15098},{"v-slot:pros":8},[15099],{"type":25,"tag":53,"props":15100,"children":15101},{},[15102,15115],{"type":25,"tag":57,"props":15103,"children":15104},{},[15105,15107],{"type":31,"value":15106},"環境構築の完全な再現性\n",{"type":25,"tag":53,"props":15108,"children":15109},{},[15110],{"type":25,"tag":57,"props":15111,"children":15112},{},[15113],{"type":31,"value":15114},"チームでも安心",{"type":25,"tag":57,"props":15116,"children":15117},{},[15118],{"type":31,"value":15119},"クラウドへのデプロイが容易",{"type":25,"tag":33,"props":15121,"children":15122},{},[15123],{"type":31,"value":15124},"というような内容が挙がると思います。しかし、簡単な個人開発で1台のPCのみで開発する場合などは、Dockerを導入するコスト（設定ファイルの記述、ビルド待ち時間、リソース消費）が、得られるメリット（環境の統一）を上回ってしまうことがあります。「みんなが使っているから」ではなく、「今の自分のプロジェクトに、そのオーバーヘッドを支払う価値があるか？」を判断する力こそが、現場で求められるエンジニアの技術選定の力なのだと最近感じています。",{"type":25,"tag":26,"props":15126,"children":15128},{"id":15127},"まとめ技術を武器にするために",[15129],{"type":31,"value":15130},"まとめ：技術を「武器」にするために",{"type":25,"tag":33,"props":15132,"children":15133},{},[15134],{"type":31,"value":15135},"本や記事で学んだ「理想の設計」や「最新技術」は、あくまで道具箱の中の一つのツールに過ぎません。それらを自分の真の武器にするためには、以下の3つのステップが重要だと私は考えています。",{"type":25,"tag":53,"props":15137,"children":15138},{},[15139,15144,15157],{"type":25,"tag":57,"props":15140,"children":15141},{},[15142],{"type":31,"value":15143},"個人開発を「技術の実験場」にする: 失敗が許される環境で、あえて「やりすぎ」なほど原理主義的に技術を適用し、その限界（境界線）を感じる。",{"type":25,"tag":57,"props":15145,"children":15146},{},[15147,15149],{"type":31,"value":15148},"「得られるメリット」と「支払うコスト」を天秤にかける: 導入することで複雑性が増していないか、今のプロジェクト規模に合っているかを常に自問自答する\n",{"type":25,"tag":53,"props":15150,"children":15151},{},[15152],{"type":25,"tag":57,"props":15153,"children":15154},{},[15155],{"type":31,"value":15156},"可能であれば定量的に測れると良いと考えています",{"type":25,"tag":57,"props":15158,"children":15159},{},[15160],{"type":31,"value":15161},"実務では「泥臭い最適解」を恐れないようにする。100点満点の設計を目指すのではなく、チームや目的に合わせて「35点や67点の妥協点」を戦略的に選べるエンジニアを目指す",{"type":25,"tag":33,"props":15163,"children":15164},{},[15165,15167,15172,15174,15179],{"type":31,"value":15166},"そして何よりも",{"type":25,"tag":103,"props":15168,"children":15169},{},[15170],{"type":31,"value":15171},"失敗を恐れない",{"type":31,"value":15173},"。そのために",{"type":25,"tag":103,"props":15175,"children":15176},{},[15177],{"type":31,"value":15178},"失敗をしても良い環境を用意する",{"type":31,"value":15180},"ことが大事であると強調したいです。",{"type":25,"tag":33,"props":15182,"children":15183},{},[15184],{"type":31,"value":15185},"「みんなが使っているから」という理由を超えて、「今のこの課題には、このコストを支払ってでもこの技術が必要だ」と自信を持って言える技術選定の力を、これからも個人開発を通じて養っていきたいと思います。",{"title":8,"searchDepth":1149,"depth":1149,"links":15187},[15188,15189,15194,15197,15201],{"id":28,"depth":1149,"text":28},{"id":14890,"depth":1149,"text":14890,"children":15190},[15191,15192,15193],{"id":14895,"depth":1159,"text":14898},{"id":14906,"depth":1159,"text":14906},{"id":14916,"depth":1159,"text":14919},{"id":14927,"depth":1149,"text":14930,"children":15195},[15196],{"id":14938,"depth":1159,"text":14941},{"id":15031,"depth":1149,"text":15031,"children":15198},[15199,15200],{"id":15041,"depth":1159,"text":15041},{"id":15063,"depth":1159,"text":15066},{"id":15127,"depth":1149,"text":15130},"content:articles:tech:mindset:how-to-practice-tech-stack.md","articles/tech/mindset/how-to-practice-tech-stack.md","articles/tech/mindset/how-to-practice-tech-stack",{"_path":15206,"_dir":14862,"_draft":7,"_partial":7,"_locale":8,"title":15207,"description":15208,"date":20,"tags":15209,"rowTypeId":18,"sitemap":15210,"body":15211,"_type":1180,"_id":15928,"_source":1182,"_file":15929,"_stem":15930,"_extension":1185},"/articles/tech/mindset/saas-is-not-dead","多分SaaSは死なない — エンジニアから見た「SaaSの死」への違和感","SaaS不要論が広がりましたが、ガバナンス・責任・コスト・運用の観点からSaaSが死なない理由をエンジニア視点で考察します。",[2373,11978,1805],{"loc":15206,"lastmod":20,"priority":18},{"type":22,"children":15212,"toc":15901},[15213,15221,15225,15230,15234,15247,15253,15258,15291,15296,15301,15306,15311,15353,15358,15363,15368,15373,15378,15383,15403,15408,15420,15425,15430,15436,15441,15446,15451,15456,15461,15466,15472,15477,15489,15495,15500,15505,15511,15516,15521,15550,15562,15567,15572,15636,15641,15653,15658,15663,15718,15730,15736,15741,15745,15750,15815,15820,15825],{"type":25,"tag":39,"props":15214,"children":15215},{},[15216],{"type":25,"tag":33,"props":15217,"children":15218},{},[15219],{"type":31,"value":15220},"AIエージェントの台頭により「SaaSは死ぬ」という言説が広がっていますが、ガバナンス・責任の所在・内製化の限界・AIコストの持続可能性・SaaS自身の進化という5つの観点から、SaaSが死なない理由を考察します。AIに最も有利な前提を置いた思考実験でも、SaaSの完全な代替は困難であることを示します。",{"type":25,"tag":26,"props":15222,"children":15223},{"id":28},[15224],{"type":31,"value":28},{"type":25,"tag":33,"props":15226,"children":15227},{},[15228],{"type":31,"value":15229},"2026年1月、AnthropicがAIエージェント「Claude Cowork」を発表し、その後SaaS関連株が大幅に下落する「SaaSpocalypse」と呼ばれる現象が起きました。これをきっかけに、SaaSが不要になるという言説が広まりました。\nこの言説に対して感じた違和感をこの記事では考えていきます。",{"type":25,"tag":26,"props":15231,"children":15232},{"id":49},[15233],{"type":31,"value":49},{"type":25,"tag":53,"props":15235,"children":15236},{},[15237,15242],{"type":25,"tag":57,"props":15238,"children":15239},{},[15240],{"type":31,"value":15241},"SaaS業界で働いている人",{"type":25,"tag":57,"props":15243,"children":15244},{},[15245],{"type":31,"value":15246},"ITに詳しくない方",{"type":25,"tag":26,"props":15248,"children":15250},{"id":15249},"多分saasは死なないと考える理由",[15251],{"type":31,"value":15252},"「多分SaaSは死なない」と考える理由",{"type":25,"tag":33,"props":15254,"children":15255},{},[15256],{"type":31,"value":15257},"タイトルにもある通り、私は「SaaSは死なない」と考えています。これには複数の理由があります。",{"type":25,"tag":53,"props":15259,"children":15260},{},[15261,15266,15276,15281,15286],{"type":25,"tag":57,"props":15262,"children":15263},{},[15264],{"type":31,"value":15265},"AIの知能が向上しても、信頼性を具体的に示すことができない",{"type":25,"tag":57,"props":15267,"children":15268},{},[15269,15271],{"type":31,"value":15270},"AIは",{"type":25,"tag":103,"props":15272,"children":15273},{},[15274],{"type":31,"value":15275},"責任を取らず、使った人に責任が生じる",{"type":25,"tag":57,"props":15277,"children":15278},{},[15279],{"type":31,"value":15280},"AIによって内製化が進んでも運用・保守コストを自社で抱えることになる",{"type":25,"tag":57,"props":15282,"children":15283},{},[15284],{"type":31,"value":15285},"AIのコストはAIを提供している企業が赤字で負担している",{"type":25,"tag":57,"props":15287,"children":15288},{},[15289],{"type":31,"value":15290},"SaaSも進化を続けている",{"type":25,"tag":33,"props":15292,"children":15293},{},[15294],{"type":31,"value":15295},"これらの理由を複合的に考慮するとSaaSが死ぬことは現実的ではないと考えています。\n以降のセクションでは、SaaSをAIエージェントに完全に置き換えた場合の思考実験を通じて、それぞれの理由を掘り下げます。",{"type":25,"tag":313,"props":15297,"children":15299},{"id":15298},"思考実験の前提条件",[15300],{"type":31,"value":15298},{"type":25,"tag":33,"props":15302,"children":15303},{},[15304],{"type":31,"value":15305},"この記事では「SaaSをAIエージェントにそのまま置き換えたらどうなるか」を考えます。AIが原因でSaaSが死ぬというならば、実際に置き換えた状態で思考実験すれば良いはずです。",{"type":25,"tag":33,"props":15307,"children":15308},{},[15309],{"type":31,"value":15310},"以下を前提とします。",{"type":25,"tag":53,"props":15312,"children":15313},{},[15314,15327,15340],{"type":25,"tag":57,"props":15315,"children":15316},{},[15317,15322,15325],{"type":25,"tag":103,"props":15318,"children":15319},{},[15320],{"type":31,"value":15321},"AIエージェントは十分に賢い",{"type":25,"tag":745,"props":15323,"children":15324},{},[],{"type":31,"value":15326},"\n既存のSaaSが提供する機能（例: 顧客管理、会計処理、プロジェクト管理など）を、指示すれば正確に実行できるだけの知能を持つものとする",{"type":25,"tag":57,"props":15328,"children":15329},{},[15330,15335,15338],{"type":25,"tag":103,"props":15331,"children":15332},{},[15333],{"type":31,"value":15334},"AIエージェントは自律的に動作する",{"type":25,"tag":745,"props":15336,"children":15337},{},[],{"type":31,"value":15339},"\n人間が逐一指示しなくても、目的を与えれば必要なタスクを自ら判断し実行できるものとする",{"type":25,"tag":57,"props":15341,"children":15342},{},[15343,15348,15351],{"type":25,"tag":103,"props":15344,"children":15345},{},[15346],{"type":31,"value":15347},"既存のSaaSは完全に廃止する",{"type":25,"tag":745,"props":15349,"children":15350},{},[],{"type":31,"value":15352},"\nSaaSとAIエージェントを併用するのではなく、SaaSが担っていた役割をすべてAIエージェントに移行した状態を考える",{"type":25,"tag":33,"props":15354,"children":15355},{},[15356],{"type":31,"value":15357},"つまり、AIの能力面では最も楽観的なシナリオを仮定します。それでもなお問題が生じることを示すのが、この思考実験の目的です。",{"type":25,"tag":26,"props":15359,"children":15361},{"id":15360},"aiの知能が向上しても信頼性を具体的に示すことができない",[15362],{"type":31,"value":15265},{"type":25,"tag":33,"props":15364,"children":15365},{},[15366],{"type":31,"value":15367},"AIエージェントに対し、「この情報は社外秘なので外部へ漏洩しないこと」と命じたとします。性能は十分なので、実際にAIエージェントは外部へ漏洩することはしませんでした。",{"type":25,"tag":33,"props":15369,"children":15370},{},[15371],{"type":31,"value":15372},"ですが、AIに対し、「外部へ漏洩しなかったか」と聞いてもそれが証明になることはありません。通信内容を監視するという方法もありますが、人件費削減や業務効率化を目的にして導入している以上、本末転倒です。",{"type":25,"tag":33,"props":15374,"children":15375},{},[15376],{"type":31,"value":15377},"「SaaSの死」論の多くは、SaaSを単なるソフトウェアとして捉えています。しかし、SaaSが実際に提供している価値はそれだけではありません。",{"type":25,"tag":313,"props":15379,"children":15381},{"id":15380},"品質保証と法的責任",[15382],{"type":31,"value":15380},{"type":25,"tag":33,"props":15384,"children":15385},{},[15386,15388,15394,15396,15401],{"type":31,"value":15387},"SaaS事業者は、",{"type":25,"tag":633,"props":15389,"children":15391},{"content":15390},"Service Level Agreement: サービス品質保証契約。稼働率や応答速度などのサービス品質を契約で保証する仕組み",[15392],{"type":31,"value":15393},"SLA",{"type":31,"value":15395},"として稼働率99.9%などを契約で保証し、違反時には返金や補償を行います。これは",{"type":25,"tag":103,"props":15397,"children":15398},{},[15399],{"type":31,"value":15400},"法的拘束力のある責任の引き受け",{"type":31,"value":15402},"です。AIエージェントが生成したシステムに障害が起きた場合、その責任を取る主体はどこにもいません。",{"type":25,"tag":313,"props":15404,"children":15406},{"id":15405},"コンプライアンスと認証",[15407],{"type":31,"value":15405},{"type":25,"tag":33,"props":15409,"children":15410},{},[15411,15413,15418],{"type":31,"value":15412},"SaaSには、",{"type":25,"tag":103,"props":15414,"children":15415},{},[15416],{"type":31,"value":15417},"第三者機関によるセキュリティ・プライバシー認証",{"type":31,"value":15419},"をとっているものがあります。これらはSaaS事業者が多大なコストと時間をかけて取得・維持しているものです。AIエージェントが構築したシステムは、こうした認証を受けていません。第三者機関による認証がされていないシステムを金融・医療など「少しでも失敗してはならない」業務で利用することは困難だと考えています。",{"type":25,"tag":313,"props":15421,"children":15423},{"id":15422},"コストの分散",[15424],{"type":31,"value":15422},{"type":25,"tag":33,"props":15426,"children":15427},{},[15428],{"type":31,"value":15429},"SaaSは一つのシステムを複数の企業が共同利用する仕組みで、インフラ費用・開発費用・セキュリティ対策費用を多数の顧客で分散しています。同等の品質をAIエージェントで自社専用に構築しようとすると、これらの費用をすべて自社で負担することになります(システムを自分で設置しないといけないため)。「AIが安くソフトウェアを作れる」としても、運用・セキュリティ・コンプライアンスのコストは残り続けます。",{"type":25,"tag":26,"props":15431,"children":15433},{"id":15432},"aiは責任を取らず使った人に責任が生じる",[15434],{"type":31,"value":15435},"AIは責任を取らず、使った人に責任が生じる",{"type":25,"tag":33,"props":15437,"children":15438},{},[15439],{"type":31,"value":15440},"仮にAIエージェントが情報を漏洩していた場合、AIではなく、AIを利用し命じた人に責任が生じます。",{"type":25,"tag":33,"props":15442,"children":15443},{},[15444],{"type":31,"value":15445},"これはSaaSとの大きな違いです。SaaS事業者に業務を委託する場合、契約に基づいて事業者側が法的責任を負います。しかしAIエージェントは契約の主体になれません。AIが起こした問題の責任は、すべてAIを使った側が引き受けることになります。",{"type":25,"tag":26,"props":15447,"children":15449},{"id":15448},"aiによって内製化が進んでも運用保守コストを自社で抱えることになる",[15450],{"type":31,"value":15280},{"type":25,"tag":33,"props":15452,"children":15453},{},[15454],{"type":31,"value":15455},"AIエージェントを利用して「自社専用SaaS」を内製化する方法も考えられます。この方法ではおそらく開発に成功し、短期的には導入成功に終わると思います。しかし、長期的に運用するとなると問題となります。",{"type":25,"tag":313,"props":15457,"children":15459},{"id":15458},"要件がそもそも間違っていた場合",[15460],{"type":31,"value":15458},{"type":25,"tag":33,"props":15462,"children":15463},{},[15464],{"type":31,"value":15465},"AIがいくら進化しようと、要件は人間が考える必要があります。ここで「不要な機能を入れてしまった」、「この前提条件が漏れていた」というような間違いが混入しやすいです。AIの性能が向上したとしても、AIは与えられた命令に忠実に実装を進めるため、要件の誤りを自ら指摘・修正してくれるとは限りません。",{"type":25,"tag":313,"props":15467,"children":15469},{"id":15468},"ソフトウェアへの機能追加が容易かはソフトウェアの構造に依存しやすい",[15470],{"type":31,"value":15471},"ソフトウェアへの機能追加が容易かは「ソフトウェアの構造」に依存しやすい",{"type":25,"tag":33,"props":15473,"children":15474},{},[15475],{"type":31,"value":15476},"複雑な要件をもつソフトウェアの場合、データの構造も複雑化します。このようなデータを壊さないように拡張し続けるのは難しいか不可能であることが多いです。このような問題はAIの性能だけでは解決できませんし、「構造上拡張は不可能」という結論に至る可能性すらあります。仮にできたとしても変更のリスクが大きいなど障壁が高い場合もあります。",{"type":25,"tag":33,"props":15478,"children":15479},{},[15480,15482,15487],{"type":31,"value":15481},"また、",{"type":25,"tag":103,"props":15483,"children":15484},{},[15485],{"type":31,"value":15486},"ソフトウェア開発に完璧な一手は存在しない",{"type":31,"value":15488},"という、構造的な問題があります。ある時点では完璧な選択をしたとしても、数年運用した後はその選択が間違いだったということは頻繁に発生します。逆に、数年後ではその選択が正解だったとしても、現時点では不正解であることもあります。したがって、実装した時点でその選択が最適であるかはわからないケースの方が多いのです。",{"type":25,"tag":313,"props":15490,"children":15492},{"id":15491},"運用保守の負担は消えない",[15493],{"type":31,"value":15494},"運用・保守の負担は消えない",{"type":25,"tag":33,"props":15496,"children":15497},{},[15498],{"type":31,"value":15499},"ソフトウェアは作って終わりではありません。セキュリティパッチの適用、インフラの監視、障害対応、バックアップなど、日々の運用業務が発生します。SaaSであればこれらはすべてサービス提供者が担いますが、内製化した場合はすべて自社の責任です。",{"type":25,"tag":33,"props":15501,"children":15502},{},[15503],{"type":31,"value":15504},"AIエージェントに運用を任せるとしても、「AIが適用したパッチが既存機能を壊していないか」「障害時の復旧手順は正しいか」を検証する人間が結局必要になります。",{"type":25,"tag":26,"props":15506,"children":15508},{"id":15507},"安価なaiは提供している企業が赤字で負担している",[15509],{"type":31,"value":15510},"安価なAIは提供している企業が赤字で負担している",{"type":25,"tag":33,"props":15512,"children":15513},{},[15514],{"type":31,"value":15515},"現在、人件費と比較すると、AIのコストは非常に安価です。しかし、この値段を実現している背景で、各種報道によればAIの事業者は赤字決算です。",{"type":25,"tag":33,"props":15517,"children":15518},{},[15519],{"type":31,"value":15520},"具体的な数字を見てみましょう。",{"type":25,"tag":53,"props":15522,"children":15523},{},[15524,15537],{"type":25,"tag":57,"props":15525,"children":15526},{},[15527,15532,15535],{"type":25,"tag":103,"props":15528,"children":15529},{},[15530],{"type":31,"value":15531},"OpenAI",{"type":25,"tag":745,"props":15533,"children":15534},{},[],{"type":31,"value":15536},"\n2024年の年間売上は約37億ドルに対し、年間損失は約50億ドル（約7,500億円）。2025年上半期だけで純損失135億ドルを計上しています",{"type":25,"tag":57,"props":15538,"children":15539},{},[15540,15545,15548],{"type":25,"tag":103,"props":15541,"children":15542},{},[15543],{"type":31,"value":15544},"Anthropic",{"type":25,"tag":745,"props":15546,"children":15547},{},[],{"type":31,"value":15549},"\n2024年の年間売上は約10億ドルに対し、消費は約56億ドル。売上の5倍以上を消費しています",{"type":25,"tag":33,"props":15551,"children":15552},{},[15553,15555,15560],{"type":31,"value":15554},"これらの数字が示すのは、現在のAIの利用料金は",{"type":25,"tag":103,"props":15556,"children":15557},{},[15558],{"type":31,"value":15559},"持続可能な価格ではなく、市場シェア獲得のための投資的な価格設定",{"type":31,"value":15561},"であるということです。",{"type":25,"tag":313,"props":15563,"children":15565},{"id":15564},"なぜこれほどコストがかかるのか",[15566],{"type":31,"value":15564},{"type":25,"tag":33,"props":15568,"children":15569},{},[15570],{"type":31,"value":15571},"AIの運用には莫大なインフラコストがかかります。",{"type":25,"tag":53,"props":15573,"children":15574},{},[15575,15599,15614],{"type":25,"tag":57,"props":15576,"children":15577},{},[15578,15580,15585,15597],{"type":31,"value":15579},"AIの学習・推論に使われるGPU（NVIDIA H100）は",{"type":25,"tag":103,"props":15581,"children":15582},{},[15583],{"type":31,"value":15584},"1基あたり約2.5万〜4万ドル（約375万〜600万円）",{"type":25,"tag":15586,"props":15587,"children":15588},"sup",{},[15589],{"type":25,"tag":1354,"props":15590,"children":15595},{"href":15591,"ariaDescribedBy":15592,"dataFootnoteRef":8,"id":15594},"#user-content-fn-1",[15593],"footnote-label","user-content-fnref-1",[15596],{"type":31,"value":5698},{"type":31,"value":15598},"。大規模なAIモデルの学習には数千〜数万基が必要です",{"type":25,"tag":57,"props":15600,"children":15601},{},[15602,15604],{"type":31,"value":15603},"世界のデータセンターの電力消費は2024年時点で約415TWh（世界全電力消費の約1.5%）に達しており、2030年には約945TWh（約2.3倍）に膨らむと予測されています",{"type":25,"tag":15586,"props":15605,"children":15606},{},[15607],{"type":25,"tag":1354,"props":15608,"children":15612},{"href":15609,"ariaDescribedBy":15610,"dataFootnoteRef":8,"id":15611},"#user-content-fn-2",[15593],"user-content-fnref-2",[15613],{"type":31,"value":5782},{"type":25,"tag":57,"props":15615,"children":15616},{},[15617,15619,15624,15626],{"type":31,"value":15618},"ChatGPTへの質問1回あたりの電力消費は約2.9Whで、",{"type":25,"tag":103,"props":15620,"children":15621},{},[15622],{"type":31,"value":15623},"Google検索の約10倍",{"type":31,"value":15625},"です",{"type":25,"tag":15586,"props":15627,"children":15628},{},[15629],{"type":25,"tag":1354,"props":15630,"children":15634},{"href":15631,"ariaDescribedBy":15632,"dataFootnoteRef":8,"id":15633},"#user-content-fn-3",[15593],"user-content-fnref-3",[15635],{"type":31,"value":5866},{"type":25,"tag":33,"props":15637,"children":15638},{},[15639],{"type":31,"value":15640},"「AIが安いからSaaSを置き換えられる」という前提は、この投資的な価格が永続することで初めて成り立ちます。しかし、AI事業者がいずれ黒字化を目指す段階では大幅に値上げする必要があります。",{"type":25,"tag":33,"props":15642,"children":15643},{},[15644,15646,15651],{"type":31,"value":15645},"さらに、SaaSの利用料金は月額・年額で予測可能なのに対し、AIエージェントの利用料金はサブスクリプションの範囲に収まらなければ従量課金制になることが多いです。業務量が増えれば増えるほどコストが膨らむため、",{"type":25,"tag":103,"props":15647,"children":15648},{},[15649],{"type":31,"value":15650},"コストの予測可能性という観点でもSaaSに優位性",{"type":31,"value":15652},"があります。",{"type":25,"tag":26,"props":15654,"children":15656},{"id":15655},"saasも進化を続けている",[15657],{"type":31,"value":15290},{"type":25,"tag":33,"props":15659,"children":15660},{},[15661],{"type":31,"value":15662},"近年、AIを利用した機能を組み込むSaaSは増加傾向にあります。例えば、Google WorkspaceではGeminiを組み込み、Google Drive内のファイルを探したり、Google Documentでは記述をサポートしたりする機能が搭載され始めています。国内SaaSでも同様の動きが加速しています。",{"type":25,"tag":53,"props":15664,"children":15665},{},[15666,15679,15692,15705],{"type":25,"tag":57,"props":15667,"children":15668},{},[15669,15674,15677],{"type":25,"tag":103,"props":15670,"children":15671},{},[15672],{"type":31,"value":15673},"サイボウズ",{"type":25,"tag":745,"props":15675,"children":15676},{},[],{"type":31,"value":15678},"\n自然言語でのデータ検索や、チャットでアプリを自動作成する「kintone AIラボ」を展開",{"type":25,"tag":57,"props":15680,"children":15681},{},[15682,15687,15690],{"type":25,"tag":103,"props":15683,"children":15684},{},[15685],{"type":31,"value":15686},"freee",{"type":25,"tag":745,"props":15688,"children":15689},{},[],{"type":31,"value":15691},"\n領収書を撮影するだけでAIが経費申請内容を自動推測する「まほう経費精算」や、AIが従業員の質問に自動応答する「freee AIヘルプデスク」を提供",{"type":25,"tag":57,"props":15693,"children":15694},{},[15695,15700,15703],{"type":25,"tag":103,"props":15696,"children":15697},{},[15698],{"type":31,"value":15699},"SmartHR",{"type":25,"tag":745,"props":15701,"children":15702},{},[],{"type":31,"value":15704},"\n人事・労務の社内問い合わせにAIが24時間自動回答する「AIアシスタント」を搭載し、社内問い合わせ約20%削減の実績",{"type":25,"tag":57,"props":15706,"children":15707},{},[15708,15713,15716],{"type":25,"tag":103,"props":15709,"children":15710},{},[15711],{"type":31,"value":15712},"マネーフォワード",{"type":25,"tag":745,"props":15714,"children":15715},{},[],{"type":31,"value":15717},"\n「No.1バックオフィスAIカンパニー」を掲げ、経費精算のAIエージェントやAI確定申告など、AIネイティブプロダクトを次々にリリース",{"type":25,"tag":33,"props":15719,"children":15720},{},[15721,15723,15728],{"type":31,"value":15722},"つまり、SaaSは「AIに置き換えられる側」ではなく、",{"type":25,"tag":103,"props":15724,"children":15725},{},[15726],{"type":31,"value":15727},"AIを取り込んで進化する側",{"type":31,"value":15729},"です。SaaS事業者は自社の信頼性・コンプライアンス基盤の上にAI機能を載せることで、個人がAIエージェントで一から構築するよりも安全かつ低コストにAIの恩恵を届けられます。",{"type":25,"tag":313,"props":15731,"children":15733},{"id":15732},"aiでsaasが進化したらコスト問題はsaasにも直撃するのでは",[15734],{"type":31,"value":15735},"AIでSaaSが進化したらコスト問題はSaaSにも直撃するのでは？",{"type":25,"tag":33,"props":15737,"children":15738},{},[15739],{"type":31,"value":15740},"先ほどAIのコストを話しましたが、SaaS企業がAIの機能を搭載する場合にも影響を与えることになります。しかし、あくまでSaaS基盤機能をAIに依存しておらず、AIのコストが大きくなれば、AIの機能を切り捨てて価格を維持したり、AI機能付きを利用したいユーザだけに提供するなど、いくらでも逃げ道があります。",{"type":25,"tag":26,"props":15742,"children":15743},{"id":1072},[15744],{"type":31,"value":1072},{"type":25,"tag":33,"props":15746,"children":15747},{},[15748],{"type":31,"value":15749},"AIは確かに性能が日々進歩しています。しかし、本記事で見てきたように以下の問題がある以上、AIエージェントだけでSaaSを完全に代替することは困難です。",{"type":25,"tag":53,"props":15751,"children":15752},{},[15753,15763,15776,15789,15802],{"type":25,"tag":57,"props":15754,"children":15755},{},[15756,15761],{"type":25,"tag":103,"props":15757,"children":15758},{},[15759],{"type":31,"value":15760},"ガバナンスの問題",{"type":31,"value":15762},"\nAIの出力を「信頼できる」と証明する手段がなく、責任はAIではなく利用者に帰属する",{"type":25,"tag":57,"props":15764,"children":15765},{},[15766,15771,15774],{"type":25,"tag":103,"props":15767,"children":15768},{},[15769],{"type":31,"value":15770},"内製化の限界",{"type":25,"tag":745,"props":15772,"children":15773},{},[],{"type":31,"value":15775},"\n要件定義の誤り、ソフトウェア構造の複雑化、運用・保守の負担は、AIの性能とは無関係に発生し続ける",{"type":25,"tag":57,"props":15777,"children":15778},{},[15779,15784,15787],{"type":25,"tag":103,"props":15780,"children":15781},{},[15782],{"type":31,"value":15783},"SaaS固有の価値",{"type":25,"tag":745,"props":15785,"children":15786},{},[],{"type":31,"value":15788},"\nSLA・コンプライアンス認証・コスト分散など、ソフトウェア以外の価値はAIでは再現できない",{"type":25,"tag":57,"props":15790,"children":15791},{},[15792,15797,15800],{"type":25,"tag":103,"props":15793,"children":15794},{},[15795],{"type":31,"value":15796},"AIコストの持続可能性",{"type":25,"tag":745,"props":15798,"children":15799},{},[],{"type":31,"value":15801},"\n現在の低価格は事業者の赤字に支えられており、値上げリスクと従量課金の不透明さが残る",{"type":25,"tag":57,"props":15803,"children":15804},{},[15805,15810,15813],{"type":25,"tag":103,"props":15806,"children":15807},{},[15808],{"type":31,"value":15809},"SaaS自身の進化",{"type":25,"tag":745,"props":15811,"children":15812},{},[],{"type":31,"value":15814},"\nSaaSはAIを取り込む側であり、信頼性の上にAIの利便性を載せるという優位なポジションにいる",{"type":25,"tag":33,"props":15816,"children":15817},{},[15818],{"type":31,"value":15819},"したがって、AIの導入が進んでもSaaSを切り捨てるという選択は現実的ではなく、SaaSとAIがハイブリッドに共存する形態が今後も続くのではないでしょうか。",{"type":25,"tag":26,"props":15821,"children":15823},{"id":15822},"参考文献",[15824],{"type":31,"value":15822},{"type":25,"tag":15826,"props":15827,"children":15830},"section",{"className":15828,"dataFootnotes":8},[15829],"footnotes",[15831,15838],{"type":25,"tag":26,"props":15832,"children":15835},{"className":15833,"id":15593},[15834],"sr-only",[15836],{"type":31,"value":15837},"Footnotes",{"type":25,"tag":198,"props":15839,"children":15840},{},[15841,15863,15882],{"type":25,"tag":57,"props":15842,"children":15844},{"id":15843},"user-content-fn-1",[15845,15852,15854],{"type":25,"tag":1354,"props":15846,"children":15849},{"href":15847,"rel":15848},"https://highreso.jp/edgehub/generationaikiso/h100.html",[1358],[15850],{"type":31,"value":15851},"NVIDIA H100とは？生成AI向けGPUの性能・価格・消費電力を徹底解説 - EdgeHUB",{"type":31,"value":15853}," ",{"type":25,"tag":1354,"props":15855,"children":15860},{"href":15856,"ariaLabel":15857,"className":15858,"dataFootnoteBackref":8},"#user-content-fnref-1","Back to reference 1",[15859],"data-footnote-backref",[15861],{"type":31,"value":15862},"↩",{"type":25,"tag":57,"props":15864,"children":15866},{"id":15865},"user-content-fn-2",[15867,15874,15875],{"type":25,"tag":1354,"props":15868,"children":15871},{"href":15869,"rel":15870},"https://www.iea.org/reports/energy-and-ai/energy-demand-from-ai",[1358],[15872],{"type":31,"value":15873},"Energy demand from AI - IEA \"Energy and AI\" Report",{"type":31,"value":15853},{"type":25,"tag":1354,"props":15876,"children":15880},{"href":15877,"ariaLabel":15878,"className":15879,"dataFootnoteBackref":8},"#user-content-fnref-2","Back to reference 2",[15859],[15881],{"type":31,"value":15862},{"type":25,"tag":57,"props":15883,"children":15885},{"id":15884},"user-content-fn-3",[15886,15893,15894],{"type":25,"tag":1354,"props":15887,"children":15890},{"href":15888,"rel":15889},"https://www.nikkei.com/article/DGXZQOCD144AB0U5A510C2000000/",[1358],[15891],{"type":31,"value":15892},"AIが電力「爆食い」 ChatGPT、Google検索の10倍消費 - 日本経済新聞",{"type":31,"value":15853},{"type":25,"tag":1354,"props":15895,"children":15899},{"href":15896,"ariaLabel":15897,"className":15898,"dataFootnoteBackref":8},"#user-content-fnref-3","Back to reference 3",[15859],[15900],{"type":31,"value":15862},{"title":8,"searchDepth":1149,"depth":1149,"links":15902},[15903,15904,15905,15908,15913,15914,15919,15922,15925,15926,15927],{"id":28,"depth":1149,"text":28},{"id":49,"depth":1149,"text":49},{"id":15249,"depth":1149,"text":15252,"children":15906},[15907],{"id":15298,"depth":1159,"text":15298},{"id":15360,"depth":1149,"text":15265,"children":15909},[15910,15911,15912],{"id":15380,"depth":1159,"text":15380},{"id":15405,"depth":1159,"text":15405},{"id":15422,"depth":1159,"text":15422},{"id":15432,"depth":1149,"text":15435},{"id":15448,"depth":1149,"text":15280,"children":15915},[15916,15917,15918],{"id":15458,"depth":1159,"text":15458},{"id":15468,"depth":1159,"text":15471},{"id":15491,"depth":1159,"text":15494},{"id":15507,"depth":1149,"text":15510,"children":15920},[15921],{"id":15564,"depth":1159,"text":15564},{"id":15655,"depth":1149,"text":15290,"children":15923},[15924],{"id":15732,"depth":1159,"text":15735},{"id":1072,"depth":1149,"text":1072},{"id":15822,"depth":1149,"text":15822},{"id":15593,"depth":1149,"text":15837},"content:articles:tech:mindset:saas-is-not-dead.md","articles/tech/mindset/saas-is-not-dead.md","articles/tech/mindset/saas-is-not-dead",{"_path":15932,"_dir":15933,"_draft":7,"_partial":7,"_locale":8,"title":15934,"description":15935,"date":15936,"rowTypeId":18,"sitemap":15937,"body":15940,"_type":1180,"_id":16342,"_source":1182,"_file":16343,"_stem":16344,"_extension":1185},"/articles/tech/testing/how_the_test_operates_basic","testing","ソフトウェアテストの運用","ソフトウェアテストの運用において重要な考えを紹介しています。具体的な実装例などでないため、どのプログラミング言語でも通用する内容です。","2025/03/01",{"loc":15932,"lastmod":15938,"priority":15939},"2025-03-01",0.8,{"type":22,"children":15941,"toc":16323},[15942,15946,15951,15956,15961,15965,15978,15983,16057,16062,16072,16077,16082,16087,16092,16097,16102,16108,16113,16118,16123,16146,16151,16157,16162,16167,16179,16184,16207,16212,16217,16222,16228,16233,16246,16258,16263,16268,16286,16291,16296,16301,16306,16311,16315],{"type":25,"tag":26,"props":15943,"children":15944},{"id":28},[15945],{"type":31,"value":28},{"type":25,"tag":33,"props":15947,"children":15948},{},[15949],{"type":31,"value":15950},"この記事では、ソフトウェアテストの運用時の注意点について解説します。",{"type":25,"tag":26,"props":15952,"children":15954},{"id":15953},"ここで扱うテスト",[15955],{"type":31,"value":15953},{"type":25,"tag":33,"props":15957,"children":15958},{},[15959],{"type":31,"value":15960},"単体テスト、結合テスト等開発者側で実施するテストを対象とします。そのため、受け入れテストなど顧客側で実施するテストはここでは扱いません。",{"type":25,"tag":26,"props":15962,"children":15963},{"id":49},[15964],{"type":31,"value":49},{"type":25,"tag":53,"props":15966,"children":15967},{},[15968,15973],{"type":25,"tag":57,"props":15969,"children":15970},{},[15971],{"type":31,"value":15972},"テストコードの運用方法を学びたい人",{"type":25,"tag":57,"props":15974,"children":15975},{},[15976],{"type":31,"value":15977},"テストの結果の見方を理解したい人",{"type":25,"tag":26,"props":15979,"children":15981},{"id":15980},"本記事で扱う用語の定義",[15982],{"type":31,"value":15980},{"type":25,"tag":825,"props":15984,"children":15985},{},[15986,16002],{"type":25,"tag":829,"props":15987,"children":15988},{},[15989],{"type":25,"tag":833,"props":15990,"children":15991},{},[15992,15997],{"type":25,"tag":837,"props":15993,"children":15994},{},[15995],{"type":31,"value":15996},"用語",{"type":25,"tag":837,"props":15998,"children":15999},{},[16000],{"type":31,"value":16001},"意味",{"type":25,"tag":848,"props":16003,"children":16004},{},[16005,16018,16031,16044],{"type":25,"tag":833,"props":16006,"children":16007},{},[16008,16013],{"type":25,"tag":855,"props":16009,"children":16010},{},[16011],{"type":31,"value":16012},"プロダクションコード",{"type":25,"tag":855,"props":16014,"children":16015},{},[16016],{"type":31,"value":16017},"実際のアプリケーションやシステムで使用されるコード。",{"type":25,"tag":833,"props":16019,"children":16020},{},[16021,16026],{"type":25,"tag":855,"props":16022,"children":16023},{},[16024],{"type":31,"value":16025},"テストコード",{"type":25,"tag":855,"props":16027,"children":16028},{},[16029],{"type":31,"value":16030},"プロダクションコードをテストするためのコード。",{"type":25,"tag":833,"props":16032,"children":16033},{},[16034,16039],{"type":25,"tag":855,"props":16035,"children":16036},{},[16037],{"type":31,"value":16038},"リファクタリング",{"type":25,"tag":855,"props":16040,"children":16041},{},[16042],{"type":31,"value":16043},"外部から見た振る舞いを変えずに実装を変更すること。",{"type":25,"tag":833,"props":16045,"children":16046},{},[16047,16052],{"type":25,"tag":855,"props":16048,"children":16049},{},[16050],{"type":31,"value":16051},"モック",{"type":25,"tag":855,"props":16053,"children":16054},{},[16055],{"type":31,"value":16056},"外部依存部分をシミュレートし、テストのために差し替えたオブジェクト。外部APIやデータベースの動作を模擬するために使用されます。",{"type":25,"tag":26,"props":16058,"children":16060},{"id":16059},"テスト実施の目的",[16061],{"type":31,"value":16059},{"type":25,"tag":33,"props":16063,"children":16064},{},[16065,16070],{"type":25,"tag":103,"props":16066,"children":16067},{},[16068],{"type":31,"value":16069},"ソフトウェアの寿命を延ばす",{"type":31,"value":16071},"ことが目的です。\n近年、開発したソフトウェアはリリース後も継続的にアップデートすることが一般的です。\n特にインターネットが普及してからは更新プログラムを配布したり、WEBアプリケーションであればクラウドサービスにデプロイしたりするなど、ソフトウェアのアップデートは当たり前になりました。\nしかし、アップデート作業のコストはソフトウェアが成長するほど徐々に大きくなっていきます。",{"type":25,"tag":313,"props":16073,"children":16075},{"id":16074},"適切なテストをしないとどうなるのか",[16076],{"type":31,"value":16074},{"type":25,"tag":33,"props":16078,"children":16079},{},[16080],{"type":31,"value":16081},"ここでは適切なテストをしないことによってどのようにソフトウェアの寿命が短くなるかを説明します。",{"type":25,"tag":335,"props":16083,"children":16085},{"id":16084},"機能追加が難しくなる",[16086],{"type":31,"value":16084},{"type":25,"tag":33,"props":16088,"children":16089},{},[16090],{"type":31,"value":16091},"機能の追加に伴い新規機能周辺のプロダクションコードの変更を行うことがあります。周辺プロダクションコードのテストを実施していなかった場合、新規のバグが発生しても気づくことが難しくなります。",{"type":25,"tag":335,"props":16093,"children":16095},{"id":16094},"リファクタリングが難しくなる",[16096],{"type":31,"value":16094},{"type":25,"tag":33,"props":16098,"children":16099},{},[16100],{"type":31,"value":16101},"リファクタリングの定義からわかるように、外部から見た振る舞いを変えないことが必要です。テストを実施しなければ、振る舞いが変化しなかったことを保証することが難しいでしょう。\nさらに、リファクタリングが困難になることで、プロダクションコードの複雑さを解消できなくなり、新規機能追加も困難になります。",{"type":25,"tag":26,"props":16103,"children":16105},{"id":16104},"テストは銀の弾丸ではなくコストがかかる",[16106],{"type":31,"value":16107},"テストは銀の弾丸ではなく、コストがかかる",{"type":25,"tag":33,"props":16109,"children":16110},{},[16111],{"type":31,"value":16112},"テストを実施することはソフトウェアの寿命を伸ばすために不可欠であることを説明しましたが、テストするにもコストがかかります。\n特に開発初期は機能追加やリファクタリングも容易であり、テストを実施するコストがとても大きく感じます。\nさらに、テストは「一度作って放置」という運用ができません。そのため、テストを実施する際はプロダクションコードだけでなくテストをどのように維持するかも考慮する必要があります。",{"type":25,"tag":26,"props":16114,"children":16116},{"id":16115},"テスト結果の見方",[16117],{"type":31,"value":16115},{"type":25,"tag":33,"props":16119,"children":16120},{},[16121],{"type":31,"value":16122},"テスト結果は単純に成功したか失敗したかを見るだけでは不十分です。そこで、次の4パターンを考えます。テストコードが正しいとは限らないことに注意してください。",{"type":25,"tag":198,"props":16124,"children":16125},{},[16126,16131,16136,16141],{"type":25,"tag":57,"props":16127,"children":16128},{},[16129],{"type":31,"value":16130},"プロダクションコードが正しく、テストも成功した",{"type":25,"tag":57,"props":16132,"children":16133},{},[16134],{"type":31,"value":16135},"プロダクションコードに誤りがあり、テストも失敗した",{"type":25,"tag":57,"props":16137,"children":16138},{},[16139],{"type":31,"value":16140},"プロダクションコードが正しくないのに、テストが成功した",{"type":25,"tag":57,"props":16142,"children":16143},{},[16144],{"type":31,"value":16145},"プロダクションコードが正しいのに、テストが成功した",{"type":25,"tag":33,"props":16147,"children":16148},{},[16149],{"type":31,"value":16150},"1、2のパターンではテストの結果とプロダクションコードの正しさは整合性が取れているため、現時点では問題になりません。しかし3、4のパターンは問題が発生します。\nこのケースについて詳しく見ていきましょう。",{"type":25,"tag":313,"props":16152,"children":16154},{"id":16153},"プロダクションコードが正しくないのにテストが成功した",[16155],{"type":31,"value":16156},"プロダクションコードが「正しくないのにテストが成功」した",{"type":25,"tag":33,"props":16158,"children":16159},{},[16160],{"type":31,"value":16161},"この結果はプロダクションコードに潜在的なバグが混入していることを示しています。\nこの場合、バグが顕在化するまで気づかないことが多いため、防ぐためにはテストの実装時に注意する必要があります。",{"type":25,"tag":335,"props":16163,"children":16165},{"id":16164},"テストケースが不十分な場合",[16166],{"type":31,"value":16164},{"type":25,"tag":33,"props":16168,"children":16169},{},[16170,16172,16177],{"type":31,"value":16171},"この場合はテストケースを改善することでテストの強度を高めることができます。\n改善策の1つに",{"type":25,"tag":103,"props":16173,"children":16174},{},[16175],{"type":31,"value":16176},"境界値分析",{"type":31,"value":16178},"というものがあります。境界値分析は、「入力データの境界値付近でバグが発生することが多いため、境界値を重点的にテストするべき」という考えに則るものです。",{"type":25,"tag":33,"props":16180,"children":16181},{},[16182],{"type":31,"value":16183},"年齢をもとに成年かを判定するプログラムを実装した場合を考えます。このプログラムは、「年齢を表すint型の引数ageを受けとり、成年の場合はtrueを返し、未成年の場合はfalseを返す。負数など、不正な引数の場合は例外をスローする」という仕様です。\n境界値分析では、実行結果が変化する入力データを考えるので、ageが17または18、0、-1である時である時を考えれば良いです。このことから、次のようなケースを考えます。",{"type":25,"tag":53,"props":16185,"children":16186},{},[16187,16192,16197,16202],{"type":25,"tag":57,"props":16188,"children":16189},{},[16190],{"type":31,"value":16191},"if age = -1 => throw exception",{"type":25,"tag":57,"props":16193,"children":16194},{},[16195],{"type":31,"value":16196},"if age = 0 => return false",{"type":25,"tag":57,"props":16198,"children":16199},{},[16200],{"type":31,"value":16201},"if age = 17 => return false",{"type":25,"tag":57,"props":16203,"children":16204},{},[16205],{"type":31,"value":16206},"if age = 18 => return true",{"type":25,"tag":33,"props":16208,"children":16209},{},[16210],{"type":31,"value":16211},"今回扱っていませんが、null値が与えられたり、int型以外の値が与えられたりする場合も考慮した方が良いです。\nこの考慮の必要性は使用言語の型付け(動的型付け、弱い静的型付け等)によって異なるため、注意してください。",{"type":25,"tag":335,"props":16213,"children":16215},{"id":16214},"外部依存を置き換えたテストの場合",[16216],{"type":31,"value":16214},{"type":25,"tag":33,"props":16218,"children":16219},{},[16220],{"type":31,"value":16221},"バグが混入する原因は自分(自身が所属するチーム)が書いたコードとは限りません。\n外部ライブラリや、DBアクセス部分など、外部依存を置き換えてテストすることは多くありますが、これは置き換えられたコードが正しく動作する前提であることを忘れてはいけません。\n「置き換えることをやめる」という対策方法はありますが、その分テストコードの保守にかかるコストは増加します。",{"type":25,"tag":313,"props":16223,"children":16225},{"id":16224},"プロダクションコードは正しいのにテストが失敗した",[16226],{"type":31,"value":16227},"プロダクションコードは「正しいのにテストが失敗」した",{"type":25,"tag":33,"props":16229,"children":16230},{},[16231],{"type":31,"value":16232},"この結果はテストコード側に問題がある可能性があることを示しています。テストコードが論理的に間違っている場合は修正すればいいですが、\n特に、次の特徴をもつテストコードはプロダクションコードの変更時に失敗しやすいです。",{"type":25,"tag":53,"props":16234,"children":16235},{},[16236,16241],{"type":25,"tag":57,"props":16237,"children":16238},{},[16239],{"type":31,"value":16240},"プロダクションコードの内部構造に基づいたテスト（ホワイトボックステスト）",{"type":25,"tag":57,"props":16242,"children":16243},{},[16244],{"type":31,"value":16245},"モックを利用しており、モックが持つ処理を「呼び出しているか」をテストしている",{"type":25,"tag":33,"props":16247,"children":16248},{},[16249,16251,16256],{"type":31,"value":16250},"決してこれらが",{"type":25,"tag":103,"props":16252,"children":16253},{},[16254],{"type":31,"value":16255},"完全に悪いものではない",{"type":31,"value":16257},"ですが、テストが壊れやすくなることを理解した上で利用する必要があります。\nテストが壊れやすくなると、リファクタリングも困難になることに注意してください。",{"type":25,"tag":26,"props":16259,"children":16261},{"id":16260},"テストの優先順位",[16262],{"type":31,"value":16260},{"type":25,"tag":33,"props":16264,"children":16265},{},[16266],{"type":31,"value":16267},"テストを実装する際は優先順位を考える必要があります。",{"type":25,"tag":198,"props":16269,"children":16270},{},[16271,16276,16281],{"type":25,"tag":57,"props":16272,"children":16273},{},[16274],{"type":31,"value":16275},"仕様通りに振る舞うか",{"type":25,"tag":57,"props":16277,"children":16278},{},[16279],{"type":31,"value":16280},"異常な値を与えた際に安全か",{"type":25,"tag":57,"props":16282,"children":16283},{},[16284],{"type":31,"value":16285},"必要な処理を正しく呼び出しているか等、内部実装に関するテスト",{"type":25,"tag":33,"props":16287,"children":16288},{},[16289],{"type":31,"value":16290},"大まかな順位ですが、大体は1、2だけ検証すれば事足ります。もしモックを利用したテストやホワイトボックステストをしている際は、\n3の「必要な処理を正しく呼び出しているか等、内部実装に関するテスト」をする必要があるのかをよく検討しましょう。\nテストでは厳密さも大事ですが、柔軟さも大事です。これらはトレードオフの関係なので何を優先してテストしたいかを考えることは非常に重要です。",{"type":25,"tag":26,"props":16292,"children":16294},{"id":16293},"自動テストはテストを維持する観点でも重要",[16295],{"type":31,"value":16293},{"type":25,"tag":33,"props":16297,"children":16298},{},[16299],{"type":31,"value":16300},"自動テストとは、何らかのイベントをトリガーにして自動的にテストが実行される仕組みです。何らかのイベントの例として、GitHubにプッシュしたり、プルリクエストを作成したりすることが挙げられます。自動テストはプロダクションコードの品質を維持するための仕組みとして紹介されることが多いですが、テストコードの品質を維持する観点でも非常に重要な仕組みです。",{"type":25,"tag":313,"props":16302,"children":16304},{"id":16303},"自動テストのメリット",[16305],{"type":31,"value":16303},{"type":25,"tag":33,"props":16307,"children":16308},{},[16309],{"type":31,"value":16310},"一番のメリットは、プロダクションコードの変更時に必ずテスト結果を確認できるようにできることです。\nテストが失敗した場合、プロダクションコードかテストコードのどちらかが誤っていることに気づくことができるため、修正することができます\nただし、「プロダクションコードが正しくないのにテストが成功した」場合を検知することはできないため、注意が必要です。",{"type":25,"tag":26,"props":16312,"children":16313},{"id":15822},[16314],{"type":31,"value":15822},{"type":25,"tag":53,"props":16316,"children":16317},{},[16318],{"type":25,"tag":57,"props":16319,"children":16320},{},[16321],{"type":31,"value":16322},"Vladimir Khorikov著、須田智之訳、「単体テストの考え方/使い方」(2023)",{"title":8,"searchDepth":1149,"depth":1149,"links":16324},[16325,16326,16327,16328,16329,16332,16333,16337,16338,16341],{"id":28,"depth":1149,"text":28},{"id":15953,"depth":1149,"text":15953},{"id":49,"depth":1149,"text":49},{"id":15980,"depth":1149,"text":15980},{"id":16059,"depth":1149,"text":16059,"children":16330},[16331],{"id":16074,"depth":1159,"text":16074},{"id":16104,"depth":1149,"text":16107},{"id":16115,"depth":1149,"text":16115,"children":16334},[16335,16336],{"id":16153,"depth":1159,"text":16156},{"id":16224,"depth":1159,"text":16227},{"id":16260,"depth":1149,"text":16260},{"id":16293,"depth":1149,"text":16293,"children":16339},[16340],{"id":16303,"depth":1159,"text":16303},{"id":15822,"depth":1149,"text":15822},"content:articles:tech:testing:how_the_test_operates_basic.md","articles/tech/testing/how_the_test_operates_basic.md","articles/tech/testing/how_the_test_operates_basic",1776356303302]