LoginSignup
42
41

C#におけるドメイン層での表現方法を考えてみる

Last updated at Posted at 2017-02-09

前提

DDDでは、モデルとコード(実装)を深く結びつけることでできる動くモデルを使ってドメインエキスパート(以下、エキスパート)と開発者が共に理解を深めながら、開発をすすめていきます。

参照:エリック・エヴァンスのドメイン駆動設計
モデルと設計の核心が相互に形成し合う。
モデルと実装が密接に結びつくことによって、モデルはドメインと深く関連したものになり、モデルに対する分析が、最終成果物である実際に動くプログラムに適用されることが保証される。
このモデルと実装の結びつきは、保守や継続的開発においても有効だが、それは、モデルの理解に基づいてコードを解釈できるからである

開発していく中でいつも悩むのは、モデリングとコードを結びつける為にどう実装するかです。
今回はコードの実装、モデルとコードの結び付けを一番強く意識づけられているドメイン層について、C#ではどのような表現をすれば判り易く、かつ時間経過にともなう変更に強くなれるのか考えてみたいと思います。
(モデリングではなく、モデルの表現方法についての考察となります。)

判り易いコードとは?

開発者にとって、判り易いコードというだけではありません。
エキスパートにとっても判り易い、コードの意図が把握できるというのが大事となります。

書籍:実践ドメイン駆動設計に判り易い例が載っています。
「ナースが患者に、インフルエンザワクチンを適量投与する」をコードで表してみましょう。
(※変数・メソッドを書籍から多少変換しています)


//例1
患者.set注射種別(種別.インフルエンザワクチン); 
患者.set投与量(適量); 
患者.set投与者(ナース); 

//例2
患者.giveインフルエンザワクチン(); 

//例3 
var インフルエンザワクチン= ワクチン.インフルエンザワクチン成人適量(); 
ナース.注射投与(患者, インフルエンザワクチン); 

どのコードが、行いたい意図をコードで表現出来ていると思われますか?

私が思うエキスパートがコードを読める事の一番のメリットは、エキスパートとコード(ユビキタス言語)を使ってコミュニケーションが取れるという点です。実装がモデルに即していないときの指摘が、画面・帳票を確認してからよりも早い段階でフィードバックを貰うことができます。
また、仕様変更でモデルのどこが変わるのか、エキスパートから開発者に具体的どの箇所がどのように変化するのか伝えてもらうことができます。

副次的な変化として、コードの性質が変わってきたように感じます。
DDDを実践する前は、コードは開発者のモノという認識があり、「どのように実装するか」に注力を傾けていたのですが、今では「何を作るのか」を強く意識するコードを書くようになりました。

時間経過による変更に強くするには?

「時の試練に耐える」と表現されるように、コードのライフサイクルが長くなれば、必ず遭遇する変更要望にどれ位容易に追随できるのか、何を許可して何に制限するのか。
柔軟性と堅牢性のトレードオフを如何にC#で表現できるかという事になります。

その他もろもろ

そもそもDDDに向いてる向いていないプロジェクト、エキスパートの協力度合、DDD実践にともなう費用対効果などの話も今回の対象外となります
今回はC#で表現方法を考察していますが、Javaでは異なる表現方法になりますし、オブジェクト指向・関数型、静的言語、動的言語、使用するフレームワークなど状況によって表現方法は大きく変わってきます。書籍:実践プログラミングDSLではその点を詳しく紹介されています。
大事なのは、モデルの意図を損なう事なくコードで表現可能かどうかです。

コードの臭い

エキスパートも読めるようにするとなると、ユビキタス言語に伴わないC#固有の文字・構文はノイズとなってしまうため、可能な限り除外するようにします。
そこでC#でよく現れるnew、null、try catchについて考えてみたいと思います。

new から漂うコードの臭い

var 材料 = new 材料();
このコードは材料Classのインスタンスを生成を行っているのですが、モデル的には何を意味しているのでしょうか?
モデルの世界とは、newをすれば、目の前に材料が突然現れるような便利な世界でよいのでしょうか?

現実世界では材料を用意するために、何処からか購入したり、材料Aと材料Bを組み合わせて新しい材料として精製したりなど、何かしらの作業が発生します。 new 材料() にそのような意図を込める必要はないのでしょうか。
今はスコープ外だとして、モデルに表現する必要はなくても、材料生成時に何らかのビジネスルールが必要になる可能性は十分に考えられます。

DDD本ではFactoryパターン、Repositoryパターンが紹介されています。
直接newするのではなく、newにかかわる処理を上手く切り出すことで、モデルの振舞を記する場所を用意します。
(ドメイン層から直接DBを参照させたくないが為だけに有るパターンの訳ではありません。)

また容易にnewできてしまうと、行儀の悪いプログラマーがつい近道をしようとコンテキストを越えてnewしてしまうかもしれません。 容易にnewを行えないようにする制限をかけておくことを忘れないようにしておくことも大事です。(Builderパターンに近いですね)
ドメイン層にnewが現れた場合、それがモデルの何を表しているのか一度考えてみてください。

nullから漂うコードの臭い

nullもモデルにおいて存在無しを示す表現ではありません。
nullの多くはif(●● != null)IsNull(●●)、演算子としては??演算子、Null条件演算子(?.)などの形でコードに現れます。これらもユビキタス言語からみるとノイズとなり、エキスパートに余計な読解能力を求めてしまいます。
それに、null時にどのような振る舞うのかコードで表現する必要性が残ってしまい可読性は下がります。

NullObjectパターンを使用するのがユビキタス言語に自然に適応できますし、null対応としては効果が高いと思います。(注:null許容型ではないですよ)
NullObjectパターンの使用を強いることで、開発者が過剰防衛(なんでもnullチェックをつける)をしてしまう心理的圧迫を緩和することができます。

try catch から漂うコードの臭い

ドメイン層における例外エラーとは何を示すのでしょうか。
個人的な考えになりますが、ドメイン層においてtry catchは、極力ないほうが望ましいと考えています。
モデルにおいて例外エラーと呼ばれる事象は、try catchではなくコードで振る舞いを記述します。(契約による設計
そもそも実装を伴わないドメイン層が原因となる例外は実行前の解析で、事前に対応されているべきという考えであり、実装をともなうアプリケーション層、インフラ層での例外は、発生した層で処理を行い、他層は結果だけを受け取るべきだという考えです。

しかし、ドメイン層で例外がまったく発生しないという訳ではありません。
自分が遭遇したドメイン層における例外は、ドメイン層をDLLにして分けていたのですが、バージョン管理を間違え依存関係が破たんしたとき、ドメイン層で例外が発生しました。
月並みですが、過剰防衛を行うのではなく、ドメイン層を守るための適切なtry catchを設定すべきと言えます。

日本語プログラム

ユビキタス言語で必ず話題に挙がるのが、日本語問題。
私はエキスパートが普段使っている日本語ユビキタス言語は大いに活用すべきだと思っています。

問題となるのは、開発者やツールの問題をどうクリアするかになります。
開発者が、日本語の混じるプログラムを読み書きするには慣れが必要ですし、IntelliSenseが使いづらくなりますので作業効率は下がります。開発メンバーへの同意は必ず得ておく必要があります。
また静的解析ツールなど、使用されるツールが2Byte文字がNGの場合、その対応が必要となります。 Qittaではコードの色分けが英語・日本語の混合ですと余計に見にくくなってしまいますね(汗

名前付き引数で可読性向上

名前付き引数を通常使う場合、定数指定されている省略可能引数がある時や、どの引数を使用するのか明示的に指定するときに使用しますが、可読性向上のために使用することもできます。

//メソッド定義
public static Schedule Create(string タイトル,
                           Date 日付,
                           time 開始時間,
                           TimeSpan 期間,
                           string 場所,
                           Member 同行者)
                           {
                             //省略
                           }

//メソッド使用時、名前付き引数で可読性をあげる
var schedule=Schedule.Create( 
          タイトル: "買い物", 
          日付: new Date(2017, 1, 1), 
          開始時間: 10:00, 
          期間: new TimeSpan(1, 30, 0), 
          場所: "大阪", 
          同行者:"友人A" 
         ); 

GetとSetを分ける

モデルを表現するときに大切な事のひとつに、関連の方向性があります。
モデル図からコードを起こす場合、関連・集約・依存・・・関係性は注意されていると思いますが、もっと単純にデータの読み書きについても、可能な限り制限を課すことで、責務が判りやすくなります。

単純な実現方法として、interfaceで読み書きを別けて、必要な機能だけ継承させる方法があります。

//読み取りInterface
public interface IReader 
{
    string GetValue(); 
} 

//書き込みInterface
public interface IWriter 
{
    void SetValue(string value); 
} 
 
//設定ファイル:読込のみ 
class ConfigReader : IReader 
{
    public void ReadValue() 
    { 
        var data = ConfigManager.Get(); 
        //何らかの処理(省略) 
    } 
} 

//設定ファイル:読込と書込 
class ConfigWriter : IReader,IWriter 
{ 
    private string _data; 
    public void ReadValue() 
    { 
        _data = ConfigManager.Get();
        //何らかの処理(省略)
    } 
    public void WriteValue() 
    { 
        ConfigManager.Set(_data); 
    } 
} 

GetとSetを分けることを意識することは、コマンド/クエリの分離(CQS)への考えにもつながります。
コマンド投げて結果を受け取るのではなく、コマンドとクエリにモデル的な違いが無いか考えるようにしてみて下さい。
コマンドとクエリは、モデル変化度合にも違いが出やすい箇所になります。変わりそうなところを切り分けておくというのも一つの戦略だと思います。

適切なモデルサイズにそろえる

ドメイン層で使用する型はファーストクラスコレクションとして、モデルが表現したい形に即しているかが焦点となります。
この時注意したいのが、表現力が足りているかのチェックとともに、逆に過剰表現になっていないかのチェックです。

例えば、モデルは日付で表現しているのに、コードでDatetimeのように「日付+時刻」の精度をつけてしまうと思わぬ不具合を生むことになります。
もし期間指定の最終日を設定する場合、「日付+時刻」では、「日付+23:59:59」または「(日付+1日)+0:00:00」として、モデルで取り扱う粒度を超えた表現をしなくてはなりません。
モデルを表現するときは、その精度も気に掛ける必要があります。

バブルワード(bubble word)

プログラミングを読みやすくするためだけに追加されるメソッドで、処理には一切影響しないのですが、可読性を向上させる方法として有効な場合があります。

static class String 
{ 
    public static string Info(this string str,string information) 
    { 
        return str; 
    } 
    public static string Is(this string str)
    {
        return str;
    }
} 
 
//例1 infoメッセージ
var length = "かくかくしかじか".Info("文字数取得").Length; 

//例2 Is
var str = "ポチ".Is()+"犬です。"

また可読性向上以外にも、使用用途は色々あります。

  • 処理の備忘録として
  • 将来ビジネスルールが変わりそうなところの目印として
    モデル意図を伝える有効な手法となりえます。

流れるようなインターフェース (Fluent Interface)

2つのプログラムがあります。

//例1
      var schedule=Schedule.Create( 
                タイトル: "買い物", 
                日付: new Date(2017, 1, 1), 
                開始時間: 10:00, 
                期間: new TimeSpan(1, 30, 0), 
                場所: "大阪", 
                同行者:"友人A" 
             ); 
 
//例2 
var schedule=Schedule.Create() 
                .タイトル("買い物") 
                .日付(new Date(2017, 1, 1)) 
                .開始時間(10:00) 
                .期間(new TimeSpan(1, 30, 0)) 
                .場所("大阪") 
                .同行者("友人A"); 

例1はよくある引数で値を指定するメソッドの形なので、開発者は理解しやすいと思います。
例2はメソッドチェーン形式ですが、メソッドを何らかの処理と考えると違和感を感じる方は多いと思われます。
この2つの表現方法の違いが、どのような効果に現れるのか考えてみたいと思います。

例2の書き方は、「流れるようなインターフェース」と呼ばれるもので、エリック・エヴァンス氏とマーティン・ファウラー氏が命名したスタイルです。
http://bliki-ja.github.io/FluentInterface/

メソッドチェーンを使い、処理の流れをわかりやすく示す手法といえます。
上手く使うとモデルを表現する強力な武器になります。

違う例で、「流れるようなインターフェース」をもう一度見てみましょう。
例ばカレーのレシピです。

var curry = Curry.Create() 
    .Add肉("牛肉") 
    .炒める() 
    .Add野菜("じゃがいも") 
    .Add野菜("ニンジン") 
    .Add野菜("玉ねぎ") 
    .炒める() 
    .Add水("1リットル") 
    .沸騰() 
    .Addルー("市販のルー") 
    .Addライス("大盛") 
    .盛り付け(); 

カレーのレシピのように処理の流れを表現したい場合、メソッドに引数を渡すだけでは表現できませんが、「流れるようなインタフェース」を使うことで直観的に判り易い表現を記載することができます。
しかし、「流れるようなインターフェース」の問題点として、市販のルーを加えるのを忘れたり、逆に2回以上加えてしまわないように、チェック機能を考慮する必要があります。

どちらが優れているという問題ではなく、それぞれの特性を考慮して、モデルをどう表現したいか、変更容易性をどこに求めるかが大事になります。

ちなみに例2のソースはこのような形になります。
(単純に自身のインスタンスを返しているだけです)

//例2(再掲) 
var schedule=Schedule.Create() 
                .タイトル("買い物") 
                .日付(new Date(2017, 1, 1)) 
                .開始時間(10:00) 
                .期間(new TimeSpan(1, 30, 0)) 
                .場所("大阪") 
                .同行者("友人A"); 
 
 

public class Schedule 
{ 
    public static Schedule Create() 
    { 
        //スケジュール作成時の処理(省略) 
        return new Schedule(); 
    } 
    public Schedule タイトル(string title) 
    { 
        //タイトル追加の処理(省略) 
        return this; 
    } 
    public Schedule 日付(Date date) 
    { 
        //日付追加の処理(省略) 
        return this; 
    } 
    public Schedule 開始時間(time startTime) 
    { 
        //開始時間追加の処理(省略) 
        return this; 
    } 
    public Schedule 期間(TimeSpan duration) 
    { 
       //期間追加の処理(省略)
        return this; 
    } 
    public Schedule 場所(string area) 
    { 
        //場所追加の処理(省略) 
        return this; 
    } 
    public Schedule 同行者(Member member) 
    { 
        //同行者追加の処理(省略) 
        return this; 
    } 
} 

処理の流れを制限する

処理の流れに順番を指定したい場合、Taskを使うと、

 var task = Task.Run(() => 処理A()) 
                .ContinueWith(t => 処理B(t.Result)) 
                .ContinueWith(t => 処理C(t.Result)) 
                .ContinueWith(t => 処理D(t.Result)); 

のように順送りの表現ができますが、「進行用インターフェース」という手法を使うことで、より細かい処理順序の指定や、処理回数の制限を規定することができます。


//例:処理は順番に1回だけ行いたい場合 

//タイトル追加Interface
public interface IScheduleAdd 
{ 
    IScheduleDay Addタイトル(string title); 
} 

//日付追加Interface
public interface IScheduleDay 
{ 
    IScheduleFrom 日付(DateTime date); 
} 

//開始時刻追加Interface
public interface IScheduleFrom 
{ 
    IScheduleDuration 開始時間(string startTime); 
} 

//期間追加Interface
public interface IScheduleDuration 
{ 
    IScheduleAt 期間(TimeSpan duration); 
} 

//場所追加Interface
public interface IScheduleAt 
{ 
    IScheduleAdd 場所(string area); 
} 

//スケジュールクラス
public class Schedule: IScheduleAdd, IScheduleDay, IScheduleFrom, IScheduleDuration, IScheduleAt 
{ 
    public static IScheduleAdd Create() 
    { 
        return new Schedule(); 
    } 

    public IScheduleDay Addタイトル(string title) 
    { 
        return this; 
    } 
     
    public IScheduleFrom 日付(DateTime date) 
    { 
        return this; 
    } 
     
    public IScheduleDuration 開始時間(string startTime) 
    { 
        return this; 
    } 

    public IScheduleAt 期間(TimeSpan duration) 
    { 
        return this; 
    } 

    public IScheduleAdd 場所(string area) 
    { 
        return this; 
    } 
} 

こうすることで、Addタイトルは1回しか選択できず、Addタイトル後には必ず日付しか選択できなくなります。

例:Addタイトル後は日付しか選択できない状態
img1.jpg

またの下記のように書き換えた場合は、Addタイトルの後は、日付、開始時刻のどちらかを選択させることができます。

public interface IScheduleDay: IScheduleFrom 
{ 
    IScheduleFrom 日付(DateTime date); 
} 

例:Addタイトル後は、日付・開始時刻のどちらかが選択可能
img2.png

コードの書き方によって、処理流れをコントロールすることができます。

流れるようなインターフェースの応用

流れるようなインターフェースを応用して、メソッド内だけで完結する作業・振る舞いを明示的に記述することも出来ます。

下記例では、洋服試着は試着室が空いている場合のみ行えることを示していますが、大筋からは隠蔽させています。


//Shoppingクラス
public class Shopping 
{ 
    //買い物開始
    public static Shopping Start(){ 
      return new Shopping();         
    } 
    
    //商品選択
    public Shopping SelectItem(string items) 
    { 
        return this; 
    } 
    
    //商品試着
    public Shopping 試着(Action<Fitting> action) 
    { 
        using (var 試着室 = FittingRoom.Prepare()) 
        { 
            if(試着室.CanUse()) 
                action(new Fitting()); 
        } 
        return this; 
    } 
    
    //商品購入
    public void 購入() 
    { 
    } 
} 

//試着クラス
public class Fitting 
{ 
    //スタッフの補助あり 
    public void WithHelp(string item) 
    {
    }
    
    //スタッフの補助なし 
    public void Self(string item) 
    {
    } 
} 

//試着室クラス
public class FittingRoom : IDisposable 
{ 
    public static FittingRoom Prepare() 
    { 
        return new FittingRoom(); 
    } 
    
    //試着室が使えるかチェック 
    public bool CanUse() 
    {
        return true; 
    } 
    
     //試着室解放  
    public void Dispose() 
    {
    } 
} 
 
 
//大筋からは試着室の事は隠ぺいし、モデルの俯瞰図的な使用をおこなっている 
var shopping = Shopping.Start() 
        .SelectItem("洋服A") 
        .SelectItem("洋服B") 
        .試着(fittingt => 
        { 
            fittingt.WithHelp("洋服C"); 
        }) 
        .SelectItem("洋服C") 
        .購入(); 
 

コードの書き方によっては、処理の流れを分岐させるなど、より複雑な表現が可能となります。


var shopping = Shopping.Start() 
        .IfSelect(t => 
        { 
            t.SelectItem("洋服A") 
             .SelectItem("洋服C") 
             .購入() 
        }) 
        .IfSelect(t => 
        { 
            t.SelectItem("洋服B") 
             .SelectItem("洋服C") 
             .購入() 
        }) 

流れるようなインターフェースの問題点

「流れるようなインターフェース」は処理の流れをそのまま表現でき、表現力は強力ですが、時にはその表現力が悪い方に働く事があります。
モデルが複雑になってくると、インタフェースも同様に複雑になっていき、どんどん流れるようなインターフェースとは呼べない状態になってしまいます。つまり、複雑なトランザクションスクリプトと同じ状態になり、ドメインモデル貧血症に陥ってしまうのです。

書籍:実践プログラミングDSLには、従うべき指針として下記3つを挙げられています。

複雑さ最小の原則の遵守
ホスト言語で使えるイディオムのうち、解決モデルに適合するものの中で最も単純なものを選択して下さい。

最適な表現力の尊重
DSLの表現力を考え得る最高のレベルにまで高めようとすると、実装の複雑さは保証不可能なレベルに至ってしまいます。言語の表現力は、ユーザが必要とするレベルに留めておいてください。

入念に設計された抽象の原則を弱めない
読みやすさを狙って言語にバブルワードを追加するとカプセル化のレベルが下がりますし、実装の内部が露出してしまいやくなります。(省略)言語の設計とは常にトレードオフと妥協のせめぎあいだということを忘れてはなりません。抽象の設計にあたっては、何を決断し、何を妥協すべきかを、常に批判的に評価してください。設計の原則がDSLユーザの利用用途と合っているか、常に意識する必要があります。

コードにおける表現方法について色々

モデルの表現方法として、ドメイン特化型言語(DSL)を参考にさせていただきましたが、表現に関する議論は古くからあり、1976年にはデイビッド・パーナスがプログラムファミリの概念を示しており、1986年にはジョン ベントリーが、プログラミングとは特定問題を解決する「リトル言語」だと述べられています。

また、文脈を損なわずにプログラム言語に落としこむ手法で有名なところをでは、
ドナルド・クヌースが提唱している「文芸的プログラミング
ウォード・カニンガムのWikiBaseなどがあります。
興味があるかたはこちらも御覧ください。

42
41
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
42
41