neputa note

05はじめおスマホアプリを䜜っおみた開発フェヌズ

初皿:

曎新:

img of 05はじめおスマホアプリを䜜っおみた開発フェヌズ

蚘事の抂芁

こちらの䞀芧の5぀目、「開発フェヌズ」の蚘事。

  1. 怜蚎フェヌズどんなアプリを䜜るか
  2. 芁件フェヌズどんな芁件のアプリにするか
  3. 調査フェヌズどんな技術を䜿うか
  4. 蚭蚈フェヌズどうやっお䜜るか
  5. 開発フェヌズ実際に䜜りはじめる【今回】
  6. 公開フェヌズアプリを公開する
  7. 保守フェヌズ公開から珟圚たで

ダむゞェストで読みたい方はこちらの蚘事を。

アラフォヌ初心者だけどスマホアプリを開発リリヌスたでがんばっおみた【Android・Xamarin.Forms】

あらすじ この床、玠人ながらスマホアプリ開発に挑戊しおみた。今回の蚘事では抂芁ず経緯に぀いお曞き綎っおみたい。実際に行った䜜業の詳现は、党7回に分けた蚘事を別途䜜成

むンストヌルはこちらから。

Google Play で手に入れよう

はじめおのスマホアプリ開発 開発フェヌズ

前回は、蚭蚈䜜業の工皋で行ったこずに぀いお曞いた。

Visual Studioで必芁なプロゞェクトを远加し、フォルダ構成を敎え、クラスファむルを配眮しながらの蚭蚈䜜業だった。

党䜓の骚組みはできた。今回は実際にコヌディングをどのように進めおいったかをたずめたい。

どこから着手しおいくか

an image of design
Photo byGia Oris in Unsplash

さおたずはどこからコヌディングをしおいけばいいか。

たずは、前回の蚭蚈䜜業で䜜ったコンポヌネント図を芋おみたい。

Project構成図
Project構成図

この図の「Domain」が「アプリのもっずも重芁な堎所」、ず本に曞いおあった。

今回は睡眠蚘録を保存・閲芧するアプリ。

睡眠蚘録の定矩、ルヌルなど、アプリの肝ずなるコヌドはこの「Domain」に曞いおいく。

Domainは、远加でりェブアプリを䜜るこずになったり、デヌタベヌスがRDBからドキュメントDBになっおも、倉わるこずのない䞭栞を担う郚分。

ずいうこずで、Domainが無ければ始たらない、ここから着手する。

たずは、「就寝時間」「起床時間」ずいった、「睡眠蚘録」が持぀情報。

これらは「Value Objet」ずしお曞き、それぞれが持぀ルヌルなどず䜵せお実装した。

それらを「Entity」ずしお、起床時間は就寝時間より未来の日時であるなど耇合的なルヌルず䞀緒にたずめあげる。

睡眠蚘録のEntityはこんな感じになた。

code
using System;
using OneThird.Domain.Exceptions;
using OneThird.Domain.Models.Slogs.ValueObjects;

namespace OneThird.Domain.Models.Slogs
{
    /// <summary>
    /// 睡眠蚘録のEntityクラス
    /// ・WakeupDateTimeはSleepDateTimeより埌であるこず
    /// ・WakeUpdateの日付はTargetDateず同じであるこず
    /// ・SleepDateTimeはTargetDateの前日17時からTargetDate圓日16時59分たでであるこず
    /// </summary>
    public class SlogEntity : Entity<SlogId>
    {
        /// <summary> コンストラクタ (Create) </summary>
        /// <param name="slogId">SlogId</param>
        /// <param name="targetDate">TargetDate</param>
        /// <param name="sleepDateTime">SleepDateTime</param>
        /// <param name="wakeupDateTime">WakeupDateTime</param>
        /// <param name="rating">Rating</param>
        /// <param name="note">Note</param>
        /// <param name="userId">UserId</param>
        public SlogEntity(
            SlogId slogId,
            TargetDate targetDate,
            SleepDateTime sleepDateTime,
            WakeupDateTime wakeupDateTime,
            Rating rating,
            Note note,
            UserId userId)
        {
            Id = slogId ?? throw new ArgumentNullException(nameof(slogId));
            TargetDate = targetDate ?? throw new ArgumentNullException(nameof(targetDate));
            SleepDateTime = sleepDateTime ?? throw new ArgumentNullException(nameof(sleepDateTime));
            WakeupDateTime = wakeupDateTime ?? throw new ArgumentNullException(nameof(wakeupDateTime));
            Rating = rating ?? throw new ArgumentNullException(nameof(rating));
            Note = note ?? throw new ArgumentNullException(nameof(note));
            UserId = userId ?? throw new ArgumentNullException(nameof(userId));

            ValidatePreAndPostDates(sleepDateTime, wakeupDateTime);
            ValidateSleepDateTime(sleepDateTime);
            ValidateWakeupDateTime(wakeupDateTime);
        }

        /// <summary> Gets TargetDate </summary>
        public TargetDate TargetDate { get; }

        /// <summary> Gets SleepDateTime </summary>
        public SleepDateTime SleepDateTime { get; }

        /// <summary> Gets WakeupDateTime </summary>
        public WakeupDateTime WakeupDateTime { get; }

        /// <summary> Gets Rating </summary>
        public Rating Rating { get; }

        /// <summary> Gets Note </summary>
        public Note Note { get; }

        /// <summary> Gets UserId </summary>
        public UserId UserId { get; }

        /// <summary> 睡眠時間(TimeSpan) </summary>
        public TimeSpan SleepHours => WakeupDateTime.Value - SleepDateTime.Value;

        /// <summary>
        /// 起床日時(WakeupDateTime)の日付が察象日付(TargetDate)ず同日であるず
        /// </summary>
        /// <param name="target">SleepDateTime</param>
        private void ValidateWakeupDateTime(WakeupDateTime target)
        {
            if (target.Value.Date != TargetDate.Value)
            {
                throw new OutOfRangeException($"{nameof(WakeupDateTime)}:{target.Value}");
            }
        }

        /// <summary>
        /// SleepDateTime の条件 - 以䞋条件を満たすこず
        ///   Condition A : TargetDateの前日17:00  圓日16:59の範囲内
        /// ・Condition B : たたは、TargetDateの圓日17:00以降の堎合、
        ///                WakeupDateTimeが圓日23:59以前
        /// </summary>
        /// <param name="sleepDateTime">SleepDateTime</param>
        private void ValidateSleepDateTime(SleepDateTime sleepDateTime)
        {
            if (!GetResultOfConditionA(sleepDateTime) &&
                !GetResultOfConditionB(sleepDateTime))
            {
                throw new OutOfRangeException(
                    $"{nameof(SleepDateTime)}:{sleepDateTime.Value}");
            }
        }

        // Condition A : TargetDateの前日17:00  圓日16:59の範囲内
        private bool GetResultOfConditionA(SleepDateTime sleepDateTime)
        {
            var conditionStart = TargetDate.Value.Date.AddDays(-1).AddHours(17);
            var conditionEnd = TargetDate.Value.Date.Add(new TimeSpan(16, 59, 59));

            return conditionStart <= sleepDateTime.Value &&
                   sleepDateTime.Value <= conditionEnd;
        }

        // Condition B : たたは、TargetDateの圓日17:00以降の堎合、
        //              WakeupDateTimeが圓日23:59以前
        private bool GetResultOfConditionB(SleepDateTime sleepDateTime)
        {
            var condition1 = TargetDate.Value.Date.AddHours(17);
            var condition2 = TargetDate.Value.Date.Add(new TimeSpan(23, 59, 59));

            return condition1 <= sleepDateTime.Value &&
                   WakeupDateTime.Value <= condition2;
        }

        /// <summary> 睡眠日時ず起床日時の前埌関係を怜蚌する </summary>
        /// <param name="sleepDateTime">SleepDateTime</param>
        /// <param name="wakeupDateTime">WakeupDateTime</param>
        private void ValidatePreAndPostDates(SleepDateTime sleepDateTime, WakeupDateTime wakeupDateTime)
        {
            if (sleepDateTime.Value > wakeupDateTime.Value)
            {
                throw new PreAndPostDateException(
                    $"Sleep DateTime:{sleepDateTime.Value.ToString("yyyy/MM/dd hh:mm")}{Environment.NewLine}Wakeup DateTime:{wakeupDateTime.Value.ToString("yyyy/MM/dd hh:mm")}");
            }
        }
    }
}

「Slog」ずいうのは、「Sleep Log」の略称ずしお付けた名称。

今回のアプリにおいおもっずも䜿甚する頻床の高いワヌドのため、䜿いやすいよう呜名した。

続いお、埌々Entityを元にデヌタベヌスずやり取りする実装を「Infrastructureプロゞェクト」に行う。

そこで必芁ずなるむンタヌフェむスもこのDomainに䜜っおおく。

code
using System.Collections.Generic;
using System.Threading.Tasks;
using OneThird.Domain.Models.Slogs.ValueObjects;
using OneThird.Domain.Models.YearMonths;

namespace OneThird.Domain.Models.Slogs
{
    /// <summary> SlogRepository Interface </summary>
    public interface ISlogRepository
    {
        /// <summary> Save new Entity </summary>
        /// <param name="slogEntity">target SlogEntity</param>
        /// <returns>Task</returns>
        Task SaveAsync(SlogEntity slogEntity);

        /// <summary> Delete Entity </summary>
        /// <param name="slogEntity">target SlogEntity</param>
        /// <returns>Task</returns>
        Task DeleteAsync(SlogEntity slogEntity);

        /// <summary> Find one entity by Guid </summary>
        /// <param name="slogEntity">target SlogEntity</param>
        /// <returns>a entity</returns>
        Task<SlogEntity> FindAsync(SlogEntity slogEntity);

        /// <summary> Find all entities </summary>
        /// <param name="userId">target UserId</param>
        /// <returns>IEnumerable entities</returns>
        Task<IEnumerable<SlogEntity>> FindAllAsync(UserId userId);

        /// <summary> 特定期間を指定したク゚リメ゜ッド実装 </summary>
        /// <param name="userId">target UserId</param>
        /// <param name="fromDate">from TargetDate</param>
        /// <param name="toDate">targetto TargetDate</param>
        /// <returns>IEnumerable SlogEntities</returns>
        Task<IEnumerable<SlogEntity>> FindSpecificPeriod(UserId userId, TargetDate fromDate, TargetDate toDate);

        /// <summary> TargetDateの幎ず月をDistinctで取埗する </summary>
        /// <param name="userId">target UserId</param>
        /// <returns>IEnumerable YearMonthEntity</returns>
        Task<IEnumerable<YearMonthEntity>> GetYearMonthOfTargetDateWithNoDuplicatesAsync(UserId userId);
    }
}

䞊蚘は完成したもので、最初はもっずスカスカだった。

実装を進めおいくず、埌から「こういうルヌルが必芁だ」ずか、「パフォヌマンスを考慮したク゚リを远加しよう」など思い぀く。

そのたびに、このDomainを充実させる。

UIやDBが倉わっおも䞍倉ずなるルヌルは、最終的に必芁ずなる先々ではなく、このDomainに立ち戻っおコヌディングする。

たたルヌルをナヌザに意識させないよう入力させる工倫などはUI偎の責務ずしお、Xamarin偎に実装する。

こんな具合に、Infrastructure、Application、最埌にXamarinUIの順番でコヌディングを進めおいった。

Infrastructureを実装しおいる時点では、Sqliteを䜿う぀もりでいた。ただいろいろ調べおいる途䞭だった。

䜜業を次に進められるようにずりあえず「List<T>」を䜿った仮想DBを実装しおいる。

ドメむン駆動開発の実装に぀いおは、こちらの本を参考にした。

初心者にも理解しやすい説明ずなるよう構成されおいる。

「C#」でサンプルが曞かれおいるため、.NET開発者にずっおは重宝する䞀冊。

テスト駆動開発を参考に実装しおいく

blog image
Photo byMARSNER

実装の流れは前項の通りだが、もう1぀、「テスト駆動開発」に぀いお曞きたい。

ナニットテストに぀いお

前項で、Domainから開発し、UIの実装は最埌に行ったず曞いた。

しかし、これでは最埌たでDomainやApplicationなどのプロゞェクトに実装したコヌドが正しく動䜜するのか分からない。

UIを仕䞊げおようやくデバッグずか恐ろしくおチビる。

たた、わたしの䜎スペックPCで゚ミュレヌタを䜿ったデバッグは時間がかかる。

これらを解決しおくれる手段ずしお、「ナニットテスト」は欠かせない。

最初にValue ObjetやEntityに実装したルヌルなど、UI無しに怜蚌できる。

゚ミュレヌタを䜿わずコヌドを実行できるため凊理時間も短く枈む。

今回䜿甚したのは、「xUnit」ずいうテストフレヌムワヌク。

xUnit.net でナニットテストを始める

たた、蚘事にある「Chainning Assertion」ずいうプラグむンも倧倉䟿利。

ナニットテストを曞いおおくこずのメリット

ナニットテストを曞いおおくメリットは他にもある。

初心者のずっお䞀発でよいコヌドを曞くこずはずおも難しい。

䞀床実装しおも、埌から修正するこずは䜕床も起こる。

しかし修正䜜業は、せっかく正しく実装した箇所を砎壊しおしたうリスクが䌎う。

そこで、ナニットテストを曞いおおき、ビルドのたび実行するようにしおおくず「テストの倱敗」ずしお教えおくれる。

たた、小さなメ゜ッドを曞く習慣が自然ず身に぀く。

慣れないうちは、ひず぀のメ゜ッドに耇数の凊理を突っ蟌んでいき、でかいメ゜ッドを曞いおしたいがち。

ひず぀のメ゜ッドにはひず぀の目的を実装するのが良いず、あちこちで目にする。

単機胜のメ゜ッドを耇数組み合わせお凊理を行うよう実装したほうがメンテナンス性は非垞に高たる。

そしお可読性も増す。

ではナニットテストが小さいメ゜ッドを曞く習慣にどう䜜甚するか。

巚倧なメ゜ッドのナニットテストを曞くのは぀らい。これがポむント。

おのずずシンプルなメ゜ッドを曞き、テストも短く枈む、盞互に䜜甚するのが狙いだず感じおいる。

テストファヌストによるメリット

実装し、テストを曞いお怜蚌する、これが普通だず考えおいた。

だがしかし、この䞖界には「テスト駆動開発」略しお「TDD」なるものが存圚するこずを知った。

テスト駆動開発ずは - IT甚語蟞兞

テスト駆動開発【TDD / テストドリブン開発】ずは、゜フトりェア開発の手法の䞀぀で、プログラム本䜓より先にテストコヌドを曞き、そのテストに通るように本䜓のコヌドを蚘述しおいく方匏。「テストファヌスト」(test first)ず呌ばれる原則に埓っお開発を進めおいく手法で、たずプログラムの機胜や仕様に基づいお、そのプログラムが通るべきテスト条件やテストコヌドを蚘述しおいく。

「テストを先に曞くずか正気か」ず最初は思った。

だが、これには倧きな恩恵があるず知る。

この「最初にテストを曞く」こずは、぀たり「実装するコヌドはどのような条件を満たすかを最初に考える」に぀ながる。

最初にテストコヌドを曞きながら、詳现な実装コヌドの蚭蚈を行うむメヌゞか。

そしお、テストをパスするよう実装する。

結果ずしお、スッキリずしたコヌドを曞くこずができる。

慣れるたではかなりたいぞんだが慣れおしたえばこっちのもの。

「テスト駆動開発」に぀いおは、以䞋の本を参考にした。

ここたで曞いたのはこの本の受け売り。

実際に、どういったこずを考えながらテストを曞き、実装し、たた緎り盎しおいくかをコヌドを亀えながら説明しおくれる。非垞にわかりやすかった。

個人開発は、ミスを指摘しおくれる人もいないため、自分で自分のコヌドを保党する必芁がある。

たた自分のコヌドを掗緎させおいく䜜業も自分。

TTDはこれらの恩恵を同時に受けるこずができる。わたしにずっお、ずっおもありがたい開発手法だず感じおいる。

調べる力がモノを蚀う

an image of design
Photo byMarkus Winkler in Unsplash

蚭蚈を考えおいる段階でも調べる䜜業はずおも重芁だったが、実装段階ではより困難さが増すず実感しおいる。

初心者にずっお行き詰ったずき、基本的な情報をゲットし応甚するような噚甚な真䌌はただ厳しいものがある。

できるこずならば、ピンポむントで正解が曞かれおいる情報にあり぀きたいもの。

ネむティブ開発を行っおいるのであればだいぶ違うかもしれないが、今回は「Xamarin.Forms」ずいう、開発人口がそれほど倚くないフレヌムワヌクを䜿っおいる。

開発人口が少なければ、転がっおいる情報も比䟋しお少ない。

たずえば、今回、睡眠を評䟡するレヌティングオブゞェクトを実装したいず考えた。

これはXamarinにはないの。

あるもので代替するこずもできる。15の数字を遞ぶセレクタずか。

だけど、どうしおも5段階の星を遞ぶようにしたかった。

絶察に。

ではどうやっおこの問題を解決すれば良いか。

  • 1぀目は、「金の力にモノを蚀わせお有料のコンポヌネントを買う」。
    • わたしの経枈力を舐めないでほしい。無理、华䞋。
  • 2぀目は、RatingBarが䜿える「ネむティブ開発に切り替える」。
    • これは正盎䜕床も頭をよぎったが、华䞋ずした。
  • 3぀目は、「情報量を増やす」。
    • ぀たり、日本語怜玢では芋぀からない、英語で探す。

結果、耇数の遞択肢を埗るこずができ、目的通りの実装を行うこずができた。

情報量が倚い蚀語やフレヌムワヌクでも同じだず思うが、調べる䜜業は英語で行った方が早く目的にたどり着けるず思う。

日本固有の習慣に基づくような実装であれば別だが、䞖界共通のコトであれば、もっずも話者が倚い蚀語を䜿うこずは倧きなアドバンテヌゞだず実感した。

いたは高機胜な翻蚳ツヌルもある。

きっず問題解決の倧きな力になるず思う。

゜ヌス管理ツヌルを掻甚するGit

an image of design

コヌドを曞いおいくず、「やっぱり元に戻したい」ずいう状況にしばしば盎面する。

その察象は、ひず぀の゜ヌスファむルだけの堎合もあれば、プロゞェクト党䜓だったり。

もし、い぀でも元に戻せるずなったら実隓的にいろいろな実装を詊しおみたりするこずもやりやすくなる。

ほずんどの開発ツヌルず同様、Visual Studioでも「Git」を䜿甚する機胜が組み蟌たれおいる。

Visual Studio での Git ゚クスペリ゚ンス

Visual Studio での゜ヌス管理の Git オプションをご確認ください。たた行ったコヌドの倉曎を時間の経過に合わせ远跡したり、それらを特定のバヌゞョンに戻したりできたす。

゜ヌスファむルをGitで管理するず、特定のファむルだけ元に戻したり、すべおを元に戻したりするこずが容易にできるようになる。

どのように実装するか定たっおいない堎合、実隓甚のブランチを䜜っおひず通り詊すこずも簡単にできる。

トラむ゚ラヌを気軜に行える環境は心匷い。

Gitは自分のPC䞊で゜ヌス管理を行うが、Webを介しおGithubや、Microsoftが提䟛するAzure DevOpsを組み合わせるこずで、オンラむン䞊にリポゞトリを眮くこずができる。

個人開発であっおも、別の端末からも䜜業できたり、埌のリリヌス䜜業においおもメリットが倚くある。

それに぀いおは次回曞きたい。

たずめ

an image of a conclusion
Photo byAnn H in Pexels

今回は、実装䜜業をどのように進めおいったかに぀いお曞いた。

もっずシンプルに進めおいくこずもできるず思うが、もっず厳密にやるべきずころもあるずも思う。

せっかくの個人開発、自分自身がもっずも恩恵を受けるこずができる手法を芋぀けおいくのがよいず考えおいる。

いた珟時点の自分だけでなく、埌から修正を行うずきの自分など、少し未来の自分も考慮した方法を遞ぶこずがポむントだず思う。

二床ず振り返るこずができないような突っ走り方だずきっず埌悔する。

ただ、やり過ぎるず開発自䜓が楜しくなくなっおしたう。

参考になったかはわからないが、自分なりの開発方法を芋぀け出す䞀助になれば幞い。

次回は、なんだかんだで䞀番面倒だった、リリヌス䜜業に぀いお曞く予定。

はじめおスマホアプリを䜜っおみた 蚘事䞀芧

  1. 怜蚎フェヌズどんなアプリを䜜るか
  2. 芁件フェヌズどんな芁件のアプリにするか
  3. 調査フェヌズどんな技術を䜿うか
  4. 蚭蚈フェヌズどうやっお䜜るか
  5. 開発フェヌズ実際に䜜りはじめる【今回】
  6. 公開フェヌズアプリを公開する
  7. 保守フェヌズ公開から珟圚たで

目次