むっちゃん万十 城南店

テレビで紹介されたりして認知はしていた「むっちゃん万十」。一度は食べてみたいなと思っていたところ、城南区役所の前に城南店があるみたいだったので、子どもと一緒に早良市民プールに行った帰りに立ち寄ってみた。

一番人気の「ハムエッグ」を購入。中身は刻んだハムが少々と、とろり半熟玉子、そしてたっぷりのマヨネーズだった。具の隙間をこれでもかってくらいマヨネーズが埋め尽くしていて、こんなに一度にマヨネーズを摂取したのは初めてだ。マヨネーズ好きなら狂喜乱舞するのかもしれない。まろやかというよりは酸味がしっかりしていて、好みは分かれるところかも。

一度は食べてみたい、とずっと思っていたから実績解除。自分はたい焼きの方が好みかな、うん。マヨネーズは食べられるけど、大好きというわけではないので。

関連ランキング:たい焼き・大判焼き | 別府駅六本松駅

システム設計の面接試験

ちょっと話題になったときに購入して積んでいたのを、ようやく読んだ。

下記のシステムを題材に、面接試験をシミュレートできる。外資の面接は本書みたいな感じなんだろうか。

  • レートリミッター
  • コンシステントハッシュ
  • キーバリューストア
  • ユニーク ID ジェネレータ
  • URL 短縮サービス
  • Web クローラー
  • 通知システム
  • ニュースフィードシステム
  • チャットシステム
  • 検索オートコンプリートシステム
  • YouTube
  • Google ドライブ

手法として、ロードバランサーやメッセージキューはしょっちゅう出てくる。システムを疎結合にしつつ、スケールアウトしやすく、リトライもしやすくするなら定石。データベースのシャーディングも頻出したけど、シャーディングは超大変なので、NewSQLが当たり前になって欲しい。

既存のシステムは本書で扱った題材に類似するものが多いと思う。面接対策でなく、業務でこれらに類するシステムを設計するときのリファレンスにできそう。実際、論文やエンジニアブログの裏付けはあるようだし。

.NET MAUI でナビゲーションバーの左側にアイテムを配置する

.NET MAUI も Xamarin Forms と同様に、標準ではナビゲーションバーの左側に ToolbarItem を配置できない。Xamarin Forms の頃は、カスタム Renderer で対応していた。

tnakamura.hatenablog.com

MAUI ではどうすればいいかというと、カスタム Handler で対応できた。

using Microsoft.Maui.Handlers;
#if IOS
using System.Reflection;
using UIKit;
using ContentView = Microsoft.Maui.Platform.ContentView;
#endif

namespace HelloMaui.Handlers;

public class CustomContentPageHandler : PageHandler
{
#if IOS
    private Page? _page;

    protected override void ConnectHandler(ContentView platformView)
    {
        if (VirtualView is Page page)
        {
            _page = page;
            _page.Loaded += HandleLoaded;
        }
        base.ConnectHandler(platformView);
    }

    private void HandleLoaded(object? sender, EventArgs e)
    {
        if (_page is not null)
        {
            _page.Loaded -= HandleLoaded;
            _page = null;

            var navigationController = ViewController?.NavigationController;
            if (navigationController?.NavigationBar is not null)
            {
                CustomizeNavigationBar(navigationController);
            }
        }
    }

    private void CustomizeNavigationBar(UINavigationController navigationController)
    {
        var navigationItem = navigationController.TopViewController.NavigationItem;
        var leftNativeButtons = (navigationItem.LeftBarButtonItems ?? []).ToList();
        var rightNativeButtons = (navigationItem.RightBarButtonItems ?? []).ToList();
        var newLeftButtons = new List<UIBarButtonItem>();
        var newRightButtons = new List<UIBarButtonItem>();
        foreach (var nativeItem in rightNativeButtons)
        {
            var field = nativeItem.GetType().GetField("_item", BindingFlags.NonPublic | BindingFlags.Instance);
            if (field is null)
            {
                return;
            }
            if (field.GetValue(nativeItem) is not ToolbarItem info)
            {
                return;
            }
            if (info.Priority < 0)
            {
                newLeftButtons.Add(nativeItem);
            }
            else
            {
                newRightButtons.Add(nativeItem);
            }
        }
        foreach (var nativeItem in leftNativeButtons)
        {
            newLeftButtons.Add(nativeItem);
        }
        navigationItem.RightBarButtonItems = newRightButtons.ToArray();
        navigationItem.LeftBarButtonItems = newLeftButtons.ToArray();
    }
#endif
}

作成したカスタム Handler は登録しておく必要がある。

using HelloMaui.Handlers;
using Microsoft.Extensions.Logging;

namespace HelloMaui;

public static class MauiProgram
{
    public static MauiApp CreateMauiApp()
    {
        var builder = MauiApp.CreateBuilder();
        builder
            .UseMauiApp<App>()
            .ConfigureMauiHandlers(handlers =>
            {
                handlers.AddHandler<ContentPage, CustomContentPageHandler>();
            })
            .ConfigureFonts(fonts =>
            {
                fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
                fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
            });
#if DEBUG
        builder.Logging.AddDebug();
#endif

        return builder.Build();
    }
}

ペンギンベーカリー橋本駅前店

北海道初の「ペンギンベーカリー」が福岡市の橋本駅近くにオープンして1ヶ月以上経過。そろそろ行列も落ち着いた頃かと思って、お昼のパンを買いに行ってみた。

お目当てだったペンギンパンは既に売り切れ。補充されるかどうかも不明だけど、オーブンの前にはペンギンパンらしき姿は無し。仕方ないので他のパンを選ぶことにした。

子どもはミニメロンパン。小さいふっくらしたパンをクッキー生地が覆っていて、普通のメロンパンとは違った。子どもは可愛さにまんまと釣られて2個買っていた。

北海道の牛乳を使ったであろうクリームパンは、あっさりしたカスタードで食べやすかった。

スティックチョコ。サクッとしたパイ生地にチョコレートの組み合わせは、想像通りの味。ハズレることはない。

ベルギーチョコロールは、チョコが生地に織り込まれているだけでなく、チョコチップも上にふりかかっていて、チョコの過剰摂取ぎみ。甘すぎず、ちょうどいい美味しさ。

カレーパンフォンデュは、中辛よりはやや辛口寄りのカレーをチーズがまろやかにしていて、今回買った中では一番気に入った。次も選ぶ可能性は高い。

子どもはミニメロンパンを気に入ったみたい。ペンギンパンやザンギを買えなかったから、次回は開店時間に再チャレンジしてみよう。

関連ランキング:パン | 橋本駅次郎丸駅

世界一流エンジニアの思考法

読書メモ。

  • 事実(データ)を一つ見つける→いくつかの仮説を立てる→その仮説を証明するための行動をとる
    • スタックトレースから該当箇所を見に行ってはいたが、仮説を立てるまでには至っていなかったな
  • ドキュメントはコードを書く前に書く
    • コード書いた後にドキュメントだけ書くなんて退屈
    • ドキュメントを書くことで自分の頭が整理される
    • 抜け落ちていた視点などに気づくことができる
    • 考えているときに書けば、自動的にドキュメントになるので、それをシェアするだけですむ。
      • 自分もプロトタイプ作って実現可能かどうか検証した後は、Design Docs書いている
  • 最初に会社と合意したゴール、つまり大まかなKPI(重要業績評価指標)を達成していたら、途中で失敗しようが、人より不器用だろうが何だろうがとくに問題にはならない
    • まずはその人を信用する
    • KPIが達成できなければ、1年の評価のタイミングで、給料が下がったりクビになったりする
      • 期待値をすり合わせて合意することが重要
      • 自分がマネージャーやる場合は必ずやろう
      • KPIが達成できなかったら給料が下がったりクビ、というのが日本だとマネできない
  • もしあなたがマネージャなら、本人の評価はあくまで約束したKPIの達成の可否であり、日々の業務における小さな成功や失敗ではない
  • 火急の依頼は「マネジメント能力の欠如」と見なされる
    • 肝に銘じる
  • KPIは定時で無理なく楽に達成できる程度のものであるべき
  • WIP=1
    • 今手を付けている仕事を一つに限定するべき
  • カナダのウォータールー大学の、24時間以内に10分間復習すれば記憶は100%戻るという研究発表もある
    • 忘却曲線は知っていたけど、こちらの研究は初耳
      • 記憶に定着するんだろうか
  • 「自分が知らないときほどリプロするんだよ。効率がいいから」
    • 自分が知らないことの問合せがきたら成長の機会
  • 「自分の意見では」とか「自分の意見を述べさせてもらいますね」
    • 日本と変わらないな
  • できたときにプロジェクトは終了する
    • バックログ(今後やるべきことリスト)と、大きな予定だけはある
    • あまり成長がなかったり向いてなかったりすると、違う仕事にアサインされたりはする
      • 給料が下がったりクビになったり、と比べると、日本でもやりようがありそう
  • 進捗報告は「スピードチャート、開発進捗サマリー、課題」程度
  • 生産性を上げるためには学習
    • 木を切ってばかりではなく、ちゃんと斧を研がないと
  • 「タイムボックス」制
    • 一日に一つのことに集中できるのは4時間。4時間過ぎて疲れたら、単に休むのではなく、違うことをするのが良い。
      • つまりは時間割
      • ライフハックにはまっていた時期に時間割を取り入れようとしたけど、結局やらなくなったことを思い出した…
  • 「○○をやめる」──身を軽くすることに真髄がある。
    • 昔読んだ「減らす技術」に共通するものがあるな
      • 「減らす技術」は自分が読んだ自己啓発本で一番身になった一冊
  • 本書を通して、Microsoft の Azure を開発するチームで働くとこんな感じなんだろうな、と想像できた
  • 世界一流って、「ワールドクラス」のことか
    • ワールドクラスの方がしっくりくる

dotnet-run-script

Node.jsではpackage.jsonにちょっとしたスクリプトを書いて、npm runで実行できるけど、同じことがdotnetでもやりたい。

dotnetコマンド標準ではサポートしていなかったけど、dotnet-run-scriptを使えばできた。

github.com

まずはインストール。

dotnet new tool-manifest
dotnet tool install run-script

プロジェクトのルートディレクトリに global.jsonを作成し、その中にスクリプトを記述。

{
  "scripts": {
    "dev": "dotnet build ./HelloMaui/HelloMaui.csproj -t:Run -f net8.0-ios"
  }
}

global.json があるディレクトリで実行。

dotnet r dev

.NET MAUI の ListView でツリー表示

MAUI の CollectionView を使ってツリーを表示するために、 SelectedIndex を取得する添付ビヘイビアを作ったりして頑張ったが、ListView を使えばそんな努力不要だった。

tnakamura.hatenablog.com

ListView には SelectedItemIndex プロパティがあるので、これを普通に使えば済む。

using System.Collections.ObjectModel;
using CommunityToolkit.Maui.Markup;
using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.Extensions.Logging;

namespace TreeSample;

public static class MauiProgram
{
    public static MauiApp CreateMauiApp()
    {
        var builder = MauiApp.CreateBuilder();
        builder
            .UseMauiApp<App>()
            .UseMauiCommunityToolkitMarkup()
            .ConfigureFonts(fonts =>
            {
                fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
                fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
            });

#if DEBUG
        builder.Logging.AddDebug();
#endif

        return builder.Build();
    }
}

public class App : Application
{
    public App()
    {
        MainPage = new MainPage();
    }
}


public class MainPage : ContentPage
{
    private readonly MainViewModel _viewModel;

    public MainPage()
    {
        BindingContext = _viewModel = new MainViewModel();

        // サンプルデータ
        for (var i = 0; i < 5; i++)
        {
            var node = new TreeNode
            {
                Level = 0,
                Name = $"{i}",
            };
            _viewModel.TreeNodes.Add(node);

            for (var j = 0; j < 5; j++)
            {
                var subNode = new TreeNode
                {
                    Level = 1,
                    Name = $"{i}.{j}",
                };
                node.Children.Add(subNode);

                for (var k = 0; k < 5; k++)
                {
                    var subSubNode = new TreeNode
                    {
                        Level = 2,
                        Name = $"{i}.{j}.{k}",
                    };
                    subNode.Children.Add(subSubNode);
                }
            }
        }

        Content = new ListView
        {
            SelectionMode = ListViewSelectionMode.Single,
            ItemsSource = _viewModel.TreeNodes,
            ItemTemplate = new DataTemplate(() =>
            {
                return new ViewCell
                {
                    View = new HorizontalStackLayout
                    {
                        new Label()
                            .Bind(
                                Label.TextProperty,
                                path: nameof(TreeNode.IsOpened),
                                convert:(bool x)=> x ? "-" : "+")
                            .Bind(
                                Label.IsVisibleProperty,
                                path: nameof(TreeNode.IsLeaf),
                                convert: (bool x) => !x),
                        new Label()
                            .Bind(
                                Label.TextProperty,
                                path: nameof(TreeNode.Name)),
                    }
                    .Bind(
                        HorizontalStackLayout.PaddingProperty,
                        path: nameof(TreeNode.Level),
                        // ネストが深いほど左のマージンを増やす
                        convert: (int x) => new Thickness(left: 10 + 10 * x, right: 10, top: 10, bottom: 10)),
                };
            }),
        }
        .Invoke(x => x.ItemSelected += (sender, e) =>
        {
            _viewModel.Select(e.SelectedItemIndex);
            if (sender is ListView listView)
            {
                listView.SelectedItem = null;
            }
        });
    }
}

public partial class MainViewModel : ObservableObject
{
    public ObservableCollection<TreeNode> TreeNodes { get; } = new();

    public void Select(int index)
    {
        if (index < 0 || TreeNodes.Count <= index)
        {
            return;
        }

        var node = TreeNodes[index];
        if (node.IsOpened)
        {
            // 選択したアイテムより後かつ、
            // 自分より深いノードを削除することで、
            // 閉じたように振舞う。
            var i = index + 1;
            while (i < TreeNodes.Count)
            {
                var subNode = TreeNodes[i];
                if (subNode.Level > node.Level)
                {
                    subNode.IsOpened = false;
                    TreeNodes.RemoveAt(i);
                }
                else
                {
                    break;
                }
            }
            node.IsOpened = false;
        }
        else
        {
            // 自分の後ろにノードを挿入することで
            // 開いたように振舞う。
            for (var i = 0; i < node.Children.Count; i++)
            {
                var insertIndex = index + 1 + i;
                TreeNodes.Insert(insertIndex, node.Children[i]);
            }
            node.IsOpened = true;
        }
    }
}

public partial class TreeNode : ObservableObject
{
    [ObservableProperty]
    private int _level;

    [ObservableProperty]
    private bool _isOpened;

    [ObservableProperty]
    private string? _name;

    public ObservableCollection<TreeNode> Children { get; } = new();

    public bool IsLeaf =>
        Children.Count == 0;
}

どうしても CollectionView が必要な場面でもない限りは、これでいいや。随分と廻り道をしてしまったな。