WEB+DB PRESS Vol.115

毎号買っている WEB+DB PRESS の Vol.115 を読み終わったので読書メモ。

特集1 競技プログラミングの過去問で学ぶアルゴリズム

貪欲法の名前は初耳、 動的計画法の名前は聞いたことがあったが、 どちらも普段やっている手法だった。 最近はパフォーマンスに熱心で、 言語的なハックだけでなく、 もっと汎用的な能力を修得したいと思っていたので、 アルゴリズムの能力を競う競技プログラミングはまさにピッタリの教材だ。 早速 AtCoder のアカウントを作成した。 あとは、蟻本も欲しい。

特集2 iOS 13 徹底活用

iOS 13 は、なんといっても SwiftUI。 React や Fultter で火が付いた宣言的 UI フレームワークの潮流に対する、Appleからのアンサー。 React の JSX や Xamarin.Forms の XAML みたいに専用の言語ではなく、 Swift のコードで宣言的に書けるのが良い。ハードから OS、フレームワーク、言語まで一気通貫に開発している Apple だからできる芸当だな。 iOS 開発の未来に対する期待感が半端ない。

小一時間でゲームを作る

HTML と JavaScript で写経できるので、始めるハードルが低くて良い。 また、完成したゲームをさらに発展させれば本格的なゲームが作れそう、 と思わせてくれる良い特集だった。

Web技術解体新書[第1回] Origin解体新書

CORS の解説が非常に分かりやすくて、 「CORS完全に理解した」という気分にさせてくれたのと同時に、 CORS に対する理解が足りていなかったことを反省。 これから開発しようとしている Web API に知識を反映させていきたい。

WEB+DB PRESS Vol.115

WEB+DB PRESS Vol.115

  • 発売日: 2020/02/22
  • メディア: Kindle

Xamarin.Android で ActionBar に戻るボタンを表示する

Xamarin.Android で開発している、画面遷移をフラグメントの入れ替えで実装しているアプリで、ActionBar の左上に←(戻る)ボタンを表示できたので方法をメモしておく。

using System;
using Android.App;
using Android.OS;
using Android.Support.V7.App;
using Android.Views;

namespace HelloAndroid
{
    [Activity(Label = "@string/app_name", Theme = "@style/AppTheme.NoActionBar", MainLauncher = true)]
    public class MainActivity : AppCompatActivity
    {
        protected override void OnCreate(Bundle savedInstanceState)
        {
            base.OnCreate(savedInstanceState);
            Xamarin.Essentials.Platform.Init(this, savedInstanceState);
            SetContentView(Resource.Layout.activity_main);

            var toolbar = FindViewById<Android.Support.V7.Widget.Toolbar>(Resource.Id.toolbar);
            SetSupportActionBar(toolbar);

            if (savedInstanceState == null)
            {
                SupportFragmentManager.BeginTransaction()
                    .Replace(Resource.Id.container, new FirstFragment())
                    .Commit();
            }
            SupportFragmentManager.BackStackChanged += HandleBackStackChanged;
        }

        // BackStack が変わったタイミングで←ボタンの表示・非表示を切り替える
        void HandleBackStackChanged(object sender, EventArgs e)
        {
            var showUpButton = SupportFragmentManager.BackStackEntryCount > 0;
            SupportActionBar?.SetDisplayHomeAsUpEnabled(showUpButton);
            SupportActionBar?.SetHomeButtonEnabled(showUpButton);
        }

        public override bool OnOptionsItemSelected(IMenuItem item)
        {
            // ←ボタンがタップされたら BackStack をポップして 1 つ前の Fragment に戻る
            if (item.ItemId == Android.Resource.Id.Home)
            {
                SupportFragmentManager.PopBackStack();
                return true;
            }
            return base.OnOptionsItemSelected(item);
        }
    }
}

これで、←ボタンをタップすると1つ前の画面に戻るし、Android 端末の Home ボタンを押した場合も前の画面に戻る。 これ以上戻れないときは←ボタンを表示しない。

ドメイン駆動設計入門

「実践ドメイン駆動設計」を読んでドメイン駆動設計を学んだが、 いざ実践するにあたり、もっとパターンの具体例が知りたかった。 本書「ドメイン駆動設計入門」は、ドメイン駆動設計に登場するパターンに焦点を当てていて、 自分が補いたいと思っていた部分にバチっとハマった。

色々と新しい知識を得ることができたので、 読んでいて思ったことを読書メモとして残しておくことにする。

Chapter2 システム固有の値を表現する「値オブジェクト」

「そこにルールが存在しているか」「それ単体で取り扱いたいか」を重要視している著者の判断基準は、 自分が値オブジェクトを作るときに考えていることが明文化されたようで、スッと腑に落ちた。

Chapter3 ライフサイクルのあるオブジェクト「エンティティ」

ライフサイクルが存在し、そこに連続性が存在するならエンティティ。 ライフサイクルを持たない、またはシステムにとってライフサイクルを表現することが無意味な場合には、値オブジェクト。 例えばユーザーは、作成されて生を受け、削除されて死を迎えるので、まさにライフサイクルをもっているからエンティティになる。

Chapter4 不自然さを解決する「ドメインサービス」

値オブジェクトやエンティティに定義すると不自然に感じる操作をドメインサービスに定義していたので、 自分がやっていたことは間違いじゃなかった。

一方で、迷いが生じたらまずはエンティティや値オブジェクトに定義し、可能な限りドメインサービスは利用しない、 というのは中々強いルールだと思った。 ただ、何でもかんでもドメインサービスに持っていけてしまうので、 ドメイン欠乏症を避けるためには、こういったルールが必要だな。

Chapter6 ユースケースを実現する「アプリケーションサービス」

アプリケーションサービスが結果を返すとき、 幸いにして、データ転送用オブジェクトにデータを移し変えて返却していた。 ただ、徹底しているわけではない。 ドメインオブジェクトを返すかどうかに関しては無頓着だったな。

確かに、クライアントにドメインオブジェクトを返してしまったら、 クライアントがドメインオブジェクトを自由に操作できてしまう。 一人プロジェクトが多かったから問題にならなかったが、 もしチームで開発していたら、 プレゼンテーション層にドメインオブジェクトをふるまいを呼び出すコードが書かれてしまう可能性があったのか。 危なかった。

あとは、アプリケーションサービスに渡す引数はコマンドオブジェクトにすることを徹底しよう。

Chapter8 ソフトウェアシステムを組み立てる

ASP.NET Core MVC のコントローラーが受け取るオブジェクトは、 そのままアプリケーションサービスに渡していたな。 フロントから引き渡されるデータの入れ物と、 アプリケーションサービスの振る舞いを実行するためのコマンドは用途が違うので、 別のオブジェクトにした方が良い、 というのは言われて納得した。

Chapter9 複雑な生成処理を行う「ファクトリ」

ファクトリもオブジェクトなのか。 スタティックなクラスじゃないのね。 採番処理のためにデータベースにアクセスするなら、オブジェクトの方が良いのは確か。 過去にリポジトリに採番をさせたこともあるが、最近は ULID 使っているのでやらなくなった。

なので、採番処理が必要かどうかよりは、 コンストラクタ内で他のオブジェクトを生成するかどうかが、 ファクトリを作る指標になりそう。

Chapter10 データの整合性を保つ

ユースケースを実装するアプリケーションサービスでは、 整合性を保つ役目も担っているが、 C# なら TransactionScope を使えばいいというのは確かにと思った。 Entity Framework Core と Dapper、どちらを使う場合でも TransactionScope で対応できる。

今個人開発でやっているプロジェクトでは、ユニットオブワークにリポジトリを保持させているが、 ユニットオブワークの実装が結構複雑になってしまった。 TransactionScope が使えるなら、それがシンプルでいいか。

Chapter12 ドメインのルールを守る「集約」

集約にゲッターを用意せずに済むならそれが理想だけど、 永続化はどうすればいいんだろうと疑問だった。 通知オブジェクトというパターンがあるのか。

集約は通知オブジェクトのインターフェースを受け取り、 内部の情報を通知することで、 永続化が可能になる。 通知オブジェクトに関連するコードを用意するのは面倒だけど、 生成する開発者用補助ツールを用意し、 ルールではなくツールで縛る、 もといサポートする方針いいな。

Chapter13 複雑な条件を表現する「仕様」

エンティティや値オブジェクトに持たせるには複雑過ぎる条件を「仕様」オブジェクトとして抜き出すのかな。 仕様をリポジトリに渡して条件に合致する集約を取得する手法は、 FindBy 系のメソッドを乱立させずに済むので導入してみたい。 C# なら Expression Tree を上手く使えば色々表現できそうだ。

Chapter15 ドメイン駆動設計のとびらを開こう

ドメイン駆動設計に登場するパターンだけを取り入れる手法を「軽量DDD」と呼ぶなんて知らなかった。 自分の場合、仕事と個人開発、 どちらもDDDを採用できているプロジェクトは一人プロジェクトなので、 軽量DDDとあまり変わりないか…。

Appendix 付録 ソリューション構成

ソリューション構成では、MVVM や MVC の M、 つまりはモデルに当たるのがアプリケーション層とドメイン層なので、 プレゼンテーション層とは別プロジェクトにしていたが、 アプリケーション層とドメイン層は別プロジェクトにしたりしなかったり。

確かに、アプリケーション層とドメイン層を同じプロジェクトにしておけば、 ドメインオブジェクトのメソッドを呼び出せるクライアントをアプリケーションサービスに限定できるな。 次の開発から意識してみよう。

ドメイン駆動設計入門 ボトムアップでわかる! ドメイン駆動設計の基本

ドメイン駆動設計入門 ボトムアップでわかる! ドメイン駆動設計の基本

  • 作者:成瀬 允宣
  • 出版社/メーカー: 翔泳社
  • 発売日: 2020/02/13
  • メディア: 単行本(ソフトカバー)

焼肉プリンス&喫茶

新天町の近くにあった喫茶店プリンスが閉店してはや数年。プリンスのナポリタンを食べてみたかったなと、ずっと思っていたら、日本銀行の向かいに復活した。なぜか焼肉屋になっていたけど、ナポリタン出しているみたいだったので行ってみた。

お目当ての鉄板ナポリタンを注文。スープと、ライスまたはパンがセットで付いてくる。スープはシンプルなコンソメ。最近冬が本気出して寒くなったので、温かいスープはホッとする。

ご飯党な自分でも、さすがにパスタではいけないので、パンを選択。軽くトーストしてあって気が利いていた。

そして主役の鉄板ナポリタン登場。結構アッサリした味付けのナポリタンだった。もっとケチャップ効かせてもいいと思うんだけどな。 ソーセージも、もっとジューシーなのが好み。 上にかかっているチーズの溶け具合が甘いのも気になってしまった。

復活前のナポリタンを食べたことがないので、 復活前の味なのかどうかの判断ができない。 プリンスの閉店を惜しむ声は多くて、 中洲にはプリンスのナポリタンを出す店もあるらしいので、 そっちと食べ比べたらちゃんとした評価ができるかもしれない。

関連ランキング:焼肉 | 天神駅西鉄福岡駅(天神)天神南駅

奥座敷てん新

妻がずっと「ウニ丼を食べたい」と言っていたので、長崎県大村市に行った際、『奥座敷てん新』に立ち寄ってみた。 奥座敷は大村の高級割烹店で、てん新はその奥座敷に隣接する比較的リーズナブルな店、らしい。

お目当のウニ丼は茶碗蒸し付き。 テレビで見る北海道とかのウニ丼と比べたら、器はやや小ぶり。 九州だとこんな感じになるのか。 茶碗蒸しがあるからトントン。

ウニはほのかに磯の香りがして、舌ざわりは滑らかだった。クリーミー。粒が際立つほど新鮮というわけではなさそう。ウニの味としては及第点かな。

一人前結構なお値段がするだけに、期待値が高くなり過ぎていた感は否めない。ただ現実はこんなところかな。ぜひまたもう一度かと聞かれると、うーんと首をひねる。値段が値段だったので、結構辛口評価になるのは仕方ない。

関連ランキング:割烹・小料理 | 大村駅

アオアシ (19)

船橋学院戦で攻守コンプリートを掴もうと焦るアシトは、 トリボネの世界基準の圧倒的なフィジカルの前にあえなく完敗した。 Aチームに上がってからここまで順調に来すぎだったから、 展開的にこのあたりで躓くだろうなとは思っていたので予想通り。

ハナがアシトに「福田と重なる」と言っていたので、 選手生命が断たれるとまではいかないまでも、 負傷退場してしまうのではと思っていただけに、 ケガで挫折とかじゃなくてよかった。 ただ、今までやってきたことが否定されるような圧倒的な敗北感っぽいので、 流石のアシトも心折れたかもな。

アシトの船橋学院戦は終わったわけだが、 栗林はむしろ火がついたみたいで、 ここからエスペリオンは反撃できるのか見ものだ。 それに船橋学院戦以降、 アシトがどうやって立ち直っていくのかも気になる。 船橋学院戦前のハナに対するアシトの態度は酷かったので、 ハナに応援してほしいと言っていたころの関係に戻ってほしいところ。

アオアシ(19) (ビッグコミックス)

アオアシ(19) (ビッグコミックス)

ASP.NET Core MVC のモデルバインドで null が明示的に渡されたものかどうか判断したい

はじめに

ASP.NET Core MVC のモデルバインドで、 JSON をバインドした後にモデルのプロパティの値が null だったとして、 デフォルト値の null なのか、 それともクライアント側から明示的に null を渡されたのか判断したい。

そして、null を設定できるプロパティを更新する場合に、 クライアント側が明示的に JSON で null を指定していたら null を設定し、 指定していなかったらそのままにしたい。 要は部分更新がしたい。

具体例

例えば、こんな入力用モデルとコントローラーがあるとする。

public class ComicInputModel
{
    public string Title { get; set; }

    public string Author { get; set; }
}

[ApiController]
[Route("[controller]")]
public class ComicsController : ControllerBase
{
    [HttpPatch("{id}")]
    public ActionResult<Comic> Update(string id, [FromBody] ComicInputModel model)
    {
        // 省略
    }
}

クライアントが下記の JSON を送信。

{
    "title": "かぐや様は告らせたい",
    "author": null
}

この JSON が ComicInputModel にバインドされると、Author は null になる。 一方で、下記のような author 属性自体が存在しない JSON をクライアントが送信した場合。

{
   "title": "かぐや様は告らせたい"
}

こちらも ComicInputModel にバインドされると、Author が null になる。

ComicInputModel にバインドされた後では、JSON で明示的に null が指定されたのか、それとも属性を省略されたのかが分からない。リクエストのボディを見れば分かると思うが、そのために追加のコストを払いたくない。

とりあえずの対策

下記のようなベースクラスを作成する。

public abstract class PatchRequest
{
    readonly HashSet<string> properties = new HashSet<string>();

    public bool HasProperty(string propertyName) =>
        properties.Contains(propertyName);

    protected void SetProperty<T>(
        ref T field,
        T value,
        [CallerMemberName] string propertyName = "")
    {
        field = value;
        properties.Add(propertyName);
    }
}

このクラスを継承するように、ComicsInputModel を修正。

public class ComicInputModel : PatchRequest
{
    string title;

    public string Title
    {
        get => title;
        set => SetProperty(ref title, value);
    }

    string author;

    public string Author
    {
        get => author;
        set => SetProperty(ref author, value);
    }
}

モデルバインドではプロパティを介して値を設定してくれる。 JSON に属性が存在しない場合は、プロパティのセッターは使われない。 そのため、コントローラーでは下記のように確認できる。

[HttpPatch("{id}")]
public ActionResult<Comic> Update(string id, [FromBody]ComicInputModel model)
{
    if (model.HasProperty(nameof(model.Title)))
    {
        // title が明示的に指定されていたら更新する
    }
    if (model.HasProperty(nameof(model.Author)))
    {
        // author が明示的に指定されていたら更新する
    }
    // 省略
}

サンプルコード全体

Web API
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace PatchSample
{
    public class Program
    {
        public static void Main(string[] args)
        {
            CreateHostBuilder(args).Build().Run();
        }

        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureWebHostDefaults(webBuilder =>
                {
                    webBuilder.UseStartup<Startup>();
                });
    }

    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllers();
        }

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            app.UseRouting();

            //app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
            });
        }
    }

    [ApiController]
    [Route("[controller]")]
    public class ComicsController : ControllerBase
    {
        static readonly List<Comic> comics = new List<Comic>()
        {
            new Comic
            {
                Id = "kaguyasama",
                Title = "かぐや様は告らせたい",
                Author = "赤坂アカ",
            },
            new Comic
            {
                Id = "5hanayome",
                Title = "五等分の花嫁",
                Author = "春場ねぎ",
            },
            new Comic
            {
                Id = "karakai",
                Title = "からかい上手の高木さん",
                Author = "山本崇一朗",
            },
        };

        [HttpGet]
        public ActionResult<IEnumerable<Comic>> GetAll()
        {
            return comics;
        }

        [HttpPatch("{id}")]
        public ActionResult<Comic> Update(string id, [FromBody]ComicInputModel model)
        {
            var comic = comics.FirstOrDefault(x => x.Id == id);
            if (comic == null)
            {
                return NotFound();
            }
            if (model.HasProperty(nameof(model.Title)))
            {
                comic.Title = model.Title;
            }
            if (model.HasProperty(nameof(model.Author)))
            {
                comic.Author = model.Author;
            }
            return comic;
        }
    }

    public abstract class PatchRequest
    {
        readonly HashSet<string> properties = new HashSet<string>();

        public bool HasProperty(string propertyName) =>
            properties.Contains(propertyName);

        protected void SetProperty<T>(
            ref T field,
            T value,
            [CallerMemberName] string propertyName = "")
        {
            field = value;
            properties.Add(propertyName);
        }
    }

    public class ComicInputModel : PatchRequest
    {
        string title;

        public string Title
        {
            get => title;
            set => SetProperty(ref title, value);
        }

        string author;

        public string Author
        {
            get => author;
            set => SetProperty(ref author, value);
        }
    }

    public class Comic
    {
        public string Id { get; set; }

        public string Title { get; set; }

        public string Author { get; set; }
    }
}
クライアント側
using System;
using System.Text;
using System.Net.Http;
using System.Threading.Tasks;

namespace PatchClient
{
    class Program
    {
        static async Task Main(string[] args)
        {
            var client = new HttpClient()
            {
                BaseAddress = new Uri("http://localhost:5000")
            };

            Console.WriteLine("===== 初期状態 =====");
            {
                var result = await client.GetStringAsync("/comics");
                Console.WriteLine(result);
            }

            Console.WriteLine("===== 部分更新 =====");
            {
                var response = await client.PatchAsync(
                    "/comics/kaguyasama",
                    new StringContent(
                        @"{
                            ""author"": ""アカ""
                          }",
                        Encoding.UTF8,
                        "application/json"));
                response.EnsureSuccessStatusCode();

                var result = await client.GetStringAsync("/comics");
                Console.WriteLine(result);
            }

            Console.WriteLine("===== 全部更新 =====");
            {
                var response = await client.PatchAsync(
                    "/comics/kaguyasama",
                    new StringContent(
                        @"{
                            ""title"": ""かぐや"",
                            ""author"": null
                          }",
                        Encoding.UTF8,
                        "application/json"));
                response.EnsureSuccessStatusCode();

                var result = await client.GetStringAsync("/comics");
                Console.WriteLine(result);
            }

            Console.ReadLine();
        }
    }
}
実行結果

f:id:griefworker:20200123101514p:plain

おわりに

ベースクラスを用意する方法で、とりあえず目的は達成することができた。 ただ、正直スマートな方法じゃないので、まったく満足していない。 もっと良い方法がある気がするし、あってほしい。

一応 Microsoft Docs には、ASP.NET Core で JSON パッチを行うドキュメントがある。

docs.microsoft.com

ただ、ここまで求めてないんだよねぇ。 やりたいのは null が明示的に指定されたものかどうかの判断であって、 それに対して JSON パッチはオーバーキル過ぎる。