ASP.NET ― 2013年06月03日 08時11分27秒
かつては EC サイト構築に活用されていたとか何とか言われていた ASP.NET が、今では企業向け業務用 Web アプリケーションの分野でばかり使われているような気がする現状について考えてみる。
考えるまでもないだろ、とか言われそうな気もしますが…。
とりあえずインターネットに公開する一般向けサイトの開発に向かない、というかそっち方面ではほとんど見向きもされない理由は分かるんです。個人のサイトで使う分にはちょっと仰々しすぎるというか大げさというか、準備しなきゃならないものがあまりにも多すぎてコスト高だと思うし (そも個人がサイトを開くレベルのホスティングサービスで Windows Server が使えるところ自体あんまり聞かない)、企業が作る Web サービスとかだと、仮に人気が出ちゃってアクセス集中しだしたときにスケールアップの難しさ云々以前に ASP.NET の仕組み自体が高トラフィック要求しすぎというか無駄が多すぎで話にならない。
一方で企業がイントラで使う業務用 Web アプリの分野では通信コストのスリムアップはそれほど要求されないし、むしろ要求されたらサーバー増築でさらにお金をせびる口実にもなるし、技術的には決まり切った作法に則って行われる単純作業に集約できてリスクも軽減できそうだしで良いことづくしにも見える。何より自由なソフトウェアはサポートが無くて既存のバグに由来する不具合も全て自前で責任をとる必要がある。何か問題があったラベンダー任せに出来るという安心感が、その辺の業界での採用を下支えしている感もある。
ただ、当のベンダーである Microsoft はその状況をよしとしているのかね? MVC モデルの登場は、むしろそうした状況を打破したいようにも見える。 UA へのアウトプットを概ねコントロール可能にすると言うことは、通信コストの軽減を意味するものでもあり、あるいは他の Web フレームワークと同じ土俵にたつための変更であるとも言える。ホスト OS が IIS という時点で同じ土俵には立ててない感もあるけど。
ASP.NET を利用して開発を行う企業や開発者について言えばもっと心配だ。 MVC に関して言えば、デスクトップアプリで言うところの WPF ぐらいの勢いで敬遠されている節もある。まぁいつだって業務アプリ界隈は保守的な人々の巣窟な訳だけど、 ASP 以外の普通の Web 開発現場ではむしろ MVC モデルを採用したフレームワークの利用が一般的だし、そうでなくても HTML5 や、 JQ でもいいからクライアントサイドスクリプティングが普通にこなせるようでないと潜り込んでいくのは難しい。 ASP.NET で開発やってました= Web 開発経験ありますとは到底言えないのが現状なのではないかと思うがどうか。
ASP.NET でありがちな話 ― 2013年06月20日 06時11分38秒
よーするに今の職場からでは恐らくこんなブログサイトはフィルタリングされて見られやしないだろう事を良いことに書きたい放題愚痴をまき散らしてしまえというお話です^^。
単なる表示上の変化に過ぎないものまでポストバックで実現する
ポストバックというのは ASP.NET 用語で、よーするにフォーム操作で発生する POST 送信とそれに対するサーバーからの応答によって、同一ページ上でのユーザーとシステムとの対話を実現してしまおうというやり方のことです。これは問い合わせ内容に応じて結果表示を切り替える仕組みとして想定されているものですが、どうやら一般的な ASP.NET 開発者は DB への問い合わせが発生しないようなことにも平気でポストバックを使ってしまうようです…。
例えばラジオボタンの選択に応じて入力項目の表示が切り替わるような UI とか、クライアントサイドスクリプトで事足りるのにも係わらず、なぜか AutoPostback="true"
とかにして、 SelectedIndexChanged
イベントハンドラをわざわざ書いて、わざわざ POST が発生するような作りにしてしまう。マスターページを使ってると ID が変化しちゃうから… という声もありそうですが、 runat="server"
にさえしなければ ID も変化しないわけで、だったら <div>
なり <span>
なりで包んで表示/非表示ぐらいは出来るだろうに (それはそれで微妙にダサイんですが…)。
何でもかんでも Page.Session
に突っ込む
DB に対する問い合わせ結果を表にして表示するような UI は業務系の Web アプリでは多いですが、一般的な ASP.NET 開発者の中には DB から取得した問い合わせ結果の DataTable
(配列の豪華版みたいなもん) をそのまんま Page.Session
に突っ込んで、ユーザーがレコードを選択したときの値の突き合わせのために再利用しようとする人が多いみたいです。
ASP.NET では、セッションに紐付いた情報はデフォルトではプロセスのメモリに格納されるとあります (だからラウンドロビン構成にしても同一セッションでのアクセスは常に同一の Web サーバーによって処理されるわけですね)。上記のような実装を想定してセッション状態ストアプロバイダーとやらを独自に実装していたり、 Web サーバのメモリ容量を十分に確保していたりしているなら良いのですが、ちゃんとそこまで想定していらっさる開発者さんってどれくらいいらっさるんでしょうね…。
何でもかんでも View State に突っ込む
ユーザーコントロールを実装する場合、コントロールの表示状態を保持するために、関連する情報を View State と呼ばれる場所に格納しておくものなんだそうです。いや、本当に表示状態に関する情報 *だけ* をここに突っ込むんであれば良いのでしょうが、データグリッドの DataSource
に突っ込むようなでかいデータをそのまま View State に放り込む人とかも結構居て、そのおかげで出力される HTML がすさまじいことになったりするケースもちらほら見かけるようです… あれは BASE64 かなんかでエンコードした文字列をそのまんま hidden に書き出しますからね…。
他にもいろいろと言いたいことはあったような気もしますが、よーするにまとめてしまうと、何でもかんでもブラックボックス化して取っつきやすさばかりを重視した開発ツールなんて、もう初学者の学習教材専用ということにしてしまって、プロの現場に持ち込むのは止めましょうよという話だったりするわけです。ぐんにゃり。
DB 操作の視点からオブジェクト指向を考える ― 2013年06月23日 22時18分58秒
シナリオ
出席簿をつけるアプリケーションを想定してみることにします。まずマスターとして、学生、講師、講義の 3つが存在するものとします。
学生
- 学籍番号 (PK)
- 氏名
- 性別
- 学年
- etc...
講師
- 教員番号 (PK)
- 氏名
- 性別
- 役職
- etc...
講義
- 講義識別番号 (PK)
- 講義名
- 担当講師 (教員番号)
- 場所
- 時間割
- 期間
- 単位数
- 概説
- etc...
それから、マスター以外のテーブルとして、受講者リストと出席記録があるものとしましょう。
受講者リスト
- 講義識別番号 (PK)
- 受講者 (学籍番号) (PK)
- 点数
- 成績 (優|良|可|不可)
- etc...
出席記録
- 講義識別番号 (PK)
- 受講者 (学籍番号) (PK)
- 講義回数 (N回目) (PK)
- 遅刻 (時刻)
- etc...
さて、ログインした講師が受け持つ講義の 1つを選択し、新たに出欠をとる画面を開いたとします (講義回数は自動でカウントされるべきでしょう)。その画面には講義に参加する全ての学生が学籍番号順にリストアップされており、点呼を受けて講師自らが学生たちの出欠状況を入力していくものとします。これをどうプログラムで表現し、実装するべきか。
ありがちな過ち
オブジェクト指向をメタファー (隠喩) の表現のために用いること自体は必ずしも間違いではありませんが、メタファー自体も手段の一つと捉えるべきです。メタファー自体が目的になってしまうと、パフォーマンスが犠牲になることが多くなります。
例えば学生と講師はどちらも人なので、 Person
クラスで抽象化して Student
と Teacher
に派生しよう、などと考えること自体は悪くないかも知れません。
// 人を表す抽象クラス public abstruct class Human { // 人を特定する番号 (学籍番号、教員番号等) public long Number { get; protected set; } // 名前 public string Name { get; protected set; } // 性別 ("M": 男性 / "F": 女性 / "O": その他) public string Sex { get; protected set; } // etc... } // 学生クラス public class Student : Human { // 学年 public int? Year { get; private set; } // ... } // 講師クラス public class Teacher : Human { // 役職 (11:助教 / 16:講師 / 21:准教授 / 26:教授 / 51:名誉教授 / 56:客員講師 / 99:その他) public int Roll { get; private set; } // ... }
あー、ちなみに言語は C# で書いてます。テストしてませんが。
で、ここまでは良いんですが、ありがちな誤りとして、それぞれのクラスのコンストラクタに DB からの読み込み機能を持たせちゃうという試みがあります。
using Db = MyNameSpace.DbAccessor; public class Student : Human { // ... // デフォルトコンストラクタ (マスタ登録時用) public Student() { } // コンストラクタ (学籍番号から情報を取得) public Student(long number) { DataTable table = Db.StudentTable.SelectByPk(number); if (table == null || table.Rows.Count == 0) return; DataRow row = table.Rows[0]; Number = number; Name = row["STUDENT_NAME"] as string; Sex = row["STUDENT_SEX"] as string; Year = row["STUDENT_YEAR"] as int?; // ... } // ... }
さすがに面倒くさいので Teacher
の方の実装例は割愛。
何故にこれがアカンのかと言いますと、まぁ例外との兼ね合いというのもあるのですが、何よりあまりにも気兼ねなく使え過ぎちゃって、知らず知らずのうちに無駄な DB アクセスが増えてしまう要因になりうるからです。あと、実際の使用に際しては実はあまり使われることのない存在になる可能性も小さくありません。
「データ取得用」のクラスは別に設ける
シナリオを読み返しましょう。講師が出欠をとるために開いた画面では、その講義への参加を登録している全ての学生が、学籍番号順にリストアップされるとしています。そのような複数の学生を特定する処理は、DB 側での SQL に任せてしまった方が効率がよいのは当然です。そこで、特定の講義に参加する学生のリストを取得するファクトリクラスを考えます。
using Db = MyNameSpace.DbAccessor; public class Student : Human { // ... // コンストラクタ internal Student(DataRow row) { Number = (long)row["STUDENT_NUMBER"]; Name = row["STUDENT_NAME"] as string; Sex = row["STUDENT_SEX"] as string; Year = row["STUDENT_YEAR"] as int?; // ... } // ... } // 学生情報管理クラス - とりあえずファクトリとして実装 public class StudentManager { // 学籍番号を指定して学生情報を取得 public static Student Get(long number) { DataTable table = Db.StudentTable.SelectByPk(number); if (table == null || table.Rows.Count == 0) return null; return new Student(table.Rows[0]); } // 講義を指定して学生情報のリストを取得 public static Student[] ListUpByLecture(int number) { // 特定の講義番号で登録されている受講者の学籍番号を受講者リストテーブルから // リストアップし、その学籍番号を用いて学生テーブルから学生の情報を引っ張る、 // までを全部 SQL でやっつけてデータを返すメソッドを想定 DataTable table = Db.LectureMembersTable.SelectByLecture(number); if (table == null) return new Student[] {}; var students = new List<Student>(); foreach (DataRow row in table.Rows) students.Add(new Student(row)); return students.ToArray(); } }
Student
のコンストラクタを internal
にするのがミソです。こうすることで、同一ファイル以外からのアクセスがコンパイラによって禁止されます。それによって、 StudentManager
クラスのメソッドを介さなければ Student
のコンストラクタは呼べない、すなわち Student
クラスのインスタンスを生成できないようになるわけです。同様のことは、 Java ならクラス内クラスにして protected
で、 C++ なら friend
を使う等で実現可能でしょう。
デフォルトコンストラクタは省いたのではなく敢えて書いていません。多分、マスタへの変更をする目的でわざわざ Student
インスタンスを生成するべきではありません。理由はいくつかあって、例えばマスタ登録のためだけにアクセサのセッターまで public
にしてしまうとメンテナンス性が損なわれるのではとか、そうではなくて全部の情報を引数に渡すコンストラクタを設けるぐらいならデータ管理用のクラス (まさにここで言うところの StudentManager
クラス) にて全部の情報を引数に渡すメソッドを書くんでも変わらないよねとか。ついでに INSERT はもとより UPDATE するためにわざわざ SELECT で情報を全部引っ張ってきてからそれをコード上で上書きして UPDATE を呼ぶとかするんでなしに最初っから PK だけ指定して UPDATE だけ呼んだ方が速いに決まってるよねとか。
NULL 可能なフィールドとかもあるし全部を引数に渡すメソッドは現実的じゃないだろうとか言うんであれば、内部データだけを持つデータ構造を定義して分離可能にしてしまっても良いと思う。こんな感じで。
public class HumanData { public string name; public string sex; // ... } public class StudentData : HumanData { public int year; // ... } public abstruct class Human { // フィールド構造体アクセサ protected HumanData Fields { get; set; } public long Number { get; protected set; } public string Name { get { return Fields.name; } protected set { Fields.name = value; } } public string Sex { get { return Fields.sex; } protected set { Fields.sex = value; } } // ... } public class Student : Human { public int Year { // こういうダウンキャストは酷くダサイですが… get { return (Fields as StudentData).year; } } // ... internal Student(DataRow row) { Number = (long)row["STUDENT_NUMBER"]; var fields = new StudentData(); fields.name = row["STUDENT_NAME"] as string; fields.sex = row["STUDENT_SEX"] as string; fields.year = (int)row["STUDENT_YEAR"]; // ... Fields = fields; } } public class StudentManager { // ... // 学生の情報をマスタに登録し、学籍番号を返す long RegistNewStudent(StudentData data) { // ... } }
まぁ、この辺は好き好きで…。
マスタデータをプールする
頻繁に見には行くけどそうそう書き換わることのないデータであり、それをプールさせておく時間をそれほど長くないセッション単位に限定するのであれば、マスタデータのメモリ上でのプールはそれほど悪くないアイデアだと思います。
public class StudentManager { // プール用コンテナ Dictionary<long, Student> pool = new Dictionary<long, Student>(); // プールをクリアする public static void ClearPool() { pool.Clear(); } // 学籍番号を指定して学生情報を取得 public static Student Get(long number) { if (pool.ContainsKey(number)) return pool[number]; DataTable table = Db.StudentTable.SelectByPk(number); if (table == null || table.Rows.Count == 0) return null; return pool[number] = new Student(table.Rows[0]); } // 講義を指定して学生情報のリストを取得 public static Student[] ListUpByLecture(int number) { // 特定の講義番号で登録されている受講者の学籍番号を受講者リストテーブルから // リストアップし、その学籍番号を用いて学生テーブルから学生の情報を引っ張る、 // までを全部 SQL でやっつけてデータを返すメソッドを想定 DataTable table = Db.LectureMembersTable.SelectByLecture(number); if (table == null) return new Student[] {}; var students = new List<Student>(); foreach (DataRow row in table.Rows) students.Add(pool[(long)row["STUDENT_NUMBER"]] = new Student(row)); return students.ToArray(); } }
但し、どういうタイミングで StudentManager.ClearPool()
を呼ぶべきかをちゃんと考えて使わないと、サーバー上で起動しっぱなしのプロセスでプールにデータがたまりっぱなし、しかもマスタメンテ画面で学生情報を修正したのに (学年とか) その変更が画面に反映されないぞー何でだーみたいなバグを誘引する原因になったりもするので、気をつけて使う必要はあると思います。特にグループ開発では。
まとめ
そんなわけで、結局のところ何が言いたいのかというと、オブジェクト指向というのは必ずしもメタファーのためだけにあるわけではないよということです。最近ではよく、「オブジェクト=物である、という説明を真に受けてはいけない」などと言われるようになりましたが、それは何でかというと、業務シナリオの中に出てくるような、実在する物だけをオブジェクトとして表現しようとすると、そのオブジェクトのクラスの中だけで何でもかんでもやり遂げなきゃならないような気になってしまうからです。学生を表現したいからと言って、必ずしも学生クラスの中だけで DB アクセスまで完結する必要はないわけです。あくまで、プログラム上での目的に応じたオブジェクト設計を心がけるべきなのです。
最近のコメント