回答編集履歴

4

質問2への回答を追記

2017/01/24 09:59

投稿

tamoto
tamoto

スコア4110

test CHANGED
@@ -78,6 +78,8 @@
78
78
 
79
79
 
80
80
 
81
+ 追記:01/20
82
+
81
83
  想像でコード例を書いてみました。
82
84
 
83
85
  IFixedLogがログの実体だと仮定しています。
@@ -262,6 +264,8 @@
262
264
 
263
265
 
264
266
 
267
+ 追記:01/21
268
+
265
269
  自アプリケーションが管理する例外は必ずログを持つという構造にしてみました。
266
270
 
267
271
  ```csharp
@@ -629,3 +633,87 @@
629
633
 
630
634
 
631
635
  かなりシンプルかつ明瞭になったと思うんですが、どうでしょう?
636
+
637
+
638
+
639
+ ---
640
+
641
+
642
+
643
+ 追記:01/24_02
644
+
645
+ 質問2のほうにも回答していきます。
646
+
647
+
648
+
649
+
650
+
651
+ まずは、質問内容以前の話なんですが。「ファイルは開始時にオープンして終了時にクローズすることになりました。」という設計がまずおかしいです。ファイルを常時オープンしっぱなしにする理由はなんでしょうか?外部リソースは「使用する時だけ開いて、すぐに閉じる」が安全に使用するための鉄則です。コードを見る限り、常時ファイルを開いておかなければならない理由は全く無いように見えます。終了時まで一切クローズしないのであれば、そもそもIDisposableの実装も冗長ということになります。質問のコードから読み取れない追加情報があったとしても、リソースの保持というのはかなり危ないコーディングなので、推奨しません。なんとかしてリソースの保持を行わないように可能な限り設計を見直します。
652
+
653
+ 例えば、自分なら以下のように書きます。これ以外はないです。
654
+
655
+
656
+
657
+ ```csharp
658
+
659
+ // データをファイルに送信するISendDataの実装
660
+
661
+ class PlainFileSender : ISendData<IEnumerable<Something>>
662
+
663
+ {
664
+
665
+ private readonly string _path;
666
+
667
+
668
+
669
+ public PlainFileSender(string path)
670
+
671
+ {
672
+
673
+ this._path = path;
674
+
675
+ }
676
+
677
+
678
+
679
+ public void SendData(IEnumerable<Something> data)
680
+
681
+ {
682
+
683
+ try
684
+
685
+ {
686
+
687
+ using (var writer = new StreamWriter(this._path, true))
688
+
689
+ {
690
+
691
+ // データをテキストファイルに追加
692
+
693
+ }
694
+
695
+ }
696
+
697
+ catch
698
+
699
+ {
700
+
701
+ var log = FileAppendErrorLog.Instance;
702
+
703
+ throw new SendDataException(FileAppendErrorLog.Instance);
704
+
705
+ }
706
+
707
+ }
708
+
709
+ }
710
+
711
+ ```
712
+
713
+
714
+
715
+ 一応、質問のほう、コンストラクタで発生させる例外についても書きますね。
716
+
717
+ エントリポイントを丸ごとtryで囲んでいるんですよね?であれば、インスタンスの生成時に「アプリケーションを継続できない状態」が発生するなら、コンストラクタ内でログを作成してFatalErrorをスローしてやればいいです。例外に「処理の流れ」なんていうものはなくて、その例外的状況「アプリケーションを継続できない」に相当するほどの異常事態が発生した際に、自分の作業をその場でぶん投げるためにあるのです。例外を書くときに「処理フロー」を考え始めるとドツボにはまるので注意して下さい。「やってみてなんかダメそうだったからもう投げる」「自分はこの例外が来るパターンは知ってるから、catchする体制だけ整えてある」という、直接相手のことを意識しないコーディングを心がけると良いです。
718
+
719
+

3

MapData再設計例追記

2017/01/24 09:59

投稿

tamoto
tamoto

スコア4110

test CHANGED
@@ -447,3 +447,185 @@
447
447
  この方法であれば、Doが「Getが失敗したこと」までしか把握していない状況で「ファイルオープン失敗」のログを正しく吐かせることができます。
448
448
 
449
449
 
450
+
451
+
452
+
453
+ ---
454
+
455
+
456
+
457
+ 追記:01/24
458
+
459
+ MapData回りの再設計例です。
460
+
461
+
462
+
463
+ ```csharp
464
+
465
+ // IParseDataのリネーム
466
+
467
+ interface IConverter<in T, TResult>
468
+
469
+ {
470
+
471
+ Tuple<bool, TResult> Convert(T data);
472
+
473
+ }
474
+
475
+
476
+
477
+ interface IValidateData<in T>
478
+
479
+ {
480
+
481
+ bool ValidateData(T data);
482
+
483
+ }
484
+
485
+
486
+
487
+ class CollectionParser<TRawItem, TMappedItem> : IConverter<IEnumerable<TRawItem>, IEnumerable<TMappedItem>>
488
+
489
+ {
490
+
491
+ private readonly IConverter<TRawItem, TMappedItem> _mapper; // 変数名はわかりやすさのため無変更で
492
+
493
+
494
+
495
+ public CollectionParser(IConverter<TRawItem, TMappedItem> mapper)
496
+
497
+ {
498
+
499
+ this._mapper = mapper;
500
+
501
+ }
502
+
503
+
504
+
505
+ public Tuple<bool, IEnumerable<TMappedItem>> Convert(IEnumerable<TRawItem> rawItems)
506
+
507
+ {
508
+
509
+ var mapped = rawItems
510
+
511
+ .Select(_mapper.Convert)
512
+
513
+ .Where(result => result.Item1) // 失敗をフィルタし、
514
+
515
+ .Select(result => result.Item2) // Dataを取り出し、
516
+
517
+ .ToArray(); // 配列化
518
+
519
+
520
+
521
+ return Tuple.Create(mapped.Length == rawItems.Count(), mapped.AsEnumerable());
522
+
523
+ }
524
+
525
+ }
526
+
527
+
528
+
529
+ // CollectionParserもSimpleMapperもどっちもIConverter。それぞれ意味は違うのでこうやって二重に使える。
530
+
531
+ class XXXSimpleMapper : IConverter<string, Something>
532
+
533
+ {
534
+
535
+ private readonly IValidateData<string> _validator; // validatorはMapperの中に持たせる
536
+
537
+
538
+
539
+ public XXXSimpleMapper(IValidateData<string> validator)
540
+
541
+ {
542
+
543
+ this._validator = validator;
544
+
545
+ }
546
+
547
+
548
+
549
+ public Tuple<bool, Something> Convert(string raw)
550
+
551
+ {
552
+
553
+ if (!this._validator.ValidateData(raw))
554
+
555
+ return Tuple.Create(false, default(Something));
556
+
557
+
558
+
559
+ // Validateが成功した以上、変換は必ず成功することを期待する。
560
+
561
+ // 検証したい項目を増やすなら、Validatorを増やせばいい。
562
+
563
+
564
+
565
+ var name = raw.Substring(0, 10).TrimStart();
566
+
567
+ var message = raw.Substring(10);
568
+
569
+
570
+
571
+ return Tuple.Create(true, new Something(name, message));
572
+
573
+ }
574
+
575
+ }
576
+
577
+
578
+
579
+ // Validatorは特に変更してない
580
+
581
+ class XXXXLogDataValidatorWithLog : IValidateData<string>
582
+
583
+ {
584
+
585
+ private readonly IFixedLogger _logger;
586
+
587
+
588
+
589
+ public XXXXLogDataValidatorWithLog(IFixedLogger logger)
590
+
591
+ {
592
+
593
+ this._logger = logger;
594
+
595
+ }
596
+
597
+
598
+
599
+ // ValidateDataは「Convert可能なデータである」ことを検証する。
600
+
601
+ // 例えば、Substringを使うConverterを想定するなら、Lengthのチェックも必要。別のValidatorを新たに実装しても良い。
602
+
603
+ public bool ValidateData(string data)
604
+
605
+ {
606
+
607
+ var result = data == string.Empty;
608
+
609
+ // 失敗したらログを出力
610
+
611
+ if (!result)
612
+
613
+ {
614
+
615
+ _logger.Log(UnavailableDataDetectedErrorLog.GetInstance(data, "文字列が空文字"));
616
+
617
+ }
618
+
619
+
620
+
621
+ return result;
622
+
623
+ }
624
+
625
+ }
626
+
627
+ ```
628
+
629
+
630
+
631
+ かなりシンプルかつ明瞭になったと思うんですが、どうでしょう?

2

例外の例

2017/01/24 00:59

投稿

tamoto
tamoto

スコア4110

test CHANGED
@@ -255,3 +255,195 @@
255
255
 
256
256
 
257
257
  Doメソッドが自分のやるべきことだけをこなして、それ以外は上位に投げればいいというコードになっているところに注目して下さい。
258
+
259
+
260
+
261
+ ---
262
+
263
+
264
+
265
+ 自アプリケーションが管理する例外は必ずログを持つという構造にしてみました。
266
+
267
+ ```csharp
268
+
269
+ // アプリケーション例外のベースとなる抽象例外クラス
270
+
271
+ public abstract class MyNantokaApplicationException : Exception
272
+
273
+ {
274
+
275
+ public IFixedLog Log { get; }
276
+
277
+
278
+
279
+ protected MyNantokaApplicationException(IFixedLog log) : base()
280
+
281
+ {
282
+
283
+ this.Log = log;
284
+
285
+ }
286
+
287
+
288
+
289
+ protected MyNantokaApplicationException(IFixedLog log, string message) : base(message)
290
+
291
+ {
292
+
293
+ this.Log = log;
294
+
295
+ }
296
+
297
+
298
+
299
+ protected MyNantokaApplicationException(IFixedLog log, string message, Exception inner) : base(message, inner)
300
+
301
+ {
302
+
303
+ this.Log = log;
304
+
305
+ }
306
+
307
+ }
308
+
309
+
310
+
311
+ // 例えば、アプリケーションを強制終了するほどの致命的なエラーを表す
312
+
313
+ public class FatalErrorException : MyNantokaApplicationException
314
+
315
+ {
316
+
317
+ public FatalErrorException(IFixedLog log) : base(log)
318
+
319
+ { }
320
+
321
+
322
+
323
+ public FatalErrorException(IFixedLog log, string message) : base(log, message)
324
+
325
+ { }
326
+
327
+
328
+
329
+ public FatalErrorException(IFixedLog log, string message, Exception inner) : base(log, message, inner)
330
+
331
+ { }
332
+
333
+ }
334
+
335
+
336
+
337
+ // GetDataがGetに失敗したときに出す例外とする
338
+
339
+ public class GetDataException : MyNantokaApplicationException
340
+
341
+ {
342
+
343
+ public GetDataException(IFixedLog log) : base(log)
344
+
345
+ { }
346
+
347
+ }
348
+
349
+
350
+
351
+ // Doメソッドの前半だけ
352
+
353
+ public void Do()
354
+
355
+ {
356
+
357
+ var raw = default(TData);
358
+
359
+ try
360
+
361
+ {
362
+
363
+ raw = _getter.GetData();
364
+
365
+ }
366
+
367
+ catch (GetDataException exception) // GetDataがGetに失敗したということしか分からない
368
+
369
+ {
370
+
371
+ var log = exception.Log; // GetDataExceptionから実際に失敗した理由のLogを取り出す
372
+
373
+ throw new FatalErrorException(log); // Get失敗は致命的なエラー扱いとして、そのログを入れてスロー
374
+
375
+ }
376
+
377
+
378
+
379
+ // ...
380
+
381
+ }
382
+
383
+
384
+
385
+ // ファイルを開くIGetDataのGetDataメソッドの例
386
+
387
+ public TData GetData()
388
+
389
+ {
390
+
391
+ try
392
+
393
+ {
394
+
395
+ /* ファイルオープン */
396
+
397
+ return data;
398
+
399
+ }
400
+
401
+ catch
402
+
403
+ {
404
+
405
+ var log = FileOpenErrorLog.Instance; // IFixedLogオブジェクトがこんな感じのシングルトンだと仮定
406
+
407
+ throw GetDataException(log); // ファイルオープン失敗のIFixedLog入りでGetDataExceptionをスロー
408
+
409
+ }
410
+
411
+ }
412
+
413
+
414
+
415
+ // メモリから読み込むIGetDataのGetDataメソッドの例
416
+
417
+ public TData GetData()
418
+
419
+ {
420
+
421
+ try
422
+
423
+ {
424
+
425
+ /* メモリ読み込み */
426
+
427
+ return data;
428
+
429
+ }
430
+
431
+ catch
432
+
433
+ {
434
+
435
+ var log = MemoryReadErrorLog.Instance;
436
+
437
+ throw GetDataException(log); // 読み込み失敗のIFixedLog入りでGetDataExceptionをスロー
438
+
439
+ }
440
+
441
+ }
442
+
443
+ ```
444
+
445
+
446
+
447
+ この方法であれば、Doが「Getが失敗したこと」までしか把握していない状況で「ファイルオープン失敗」のログを正しく吐かせることができます。
448
+
449
+

1

コード例を作成

2017/01/21 07:29

投稿

tamoto
tamoto

スコア4110

test CHANGED
@@ -71,3 +71,187 @@
71
71
  まず、`Do`メソッドが「アプリケーション全体の終了を管理できる」というのは権限を持ちすぎです。
72
72
 
73
73
  `Do`がやるのは「DataをGetしてParseしてSendする」だけであるべきで、取得できなかった場合の処理は`Do`メソッドが扱える範囲でしか記述してはいけません。アプリケーションの終了までを行いたいのであれば、各タスクの失敗を`Do`メソッドが認識した時点で例外を投げるしかないです。`Do`が「失敗したらアプリケーションを終了させたい」のであれば、それは`Do`の身に余る行為なので、こういう場合は予め定義しておいた独自例外をスローし、上位レイヤ(場合によってはエントリポイントまで遡る)でその例外をcatchし処理する方針になるはずです。このときの`Do`メソッドは、「ログを出力してから例外をスロー」よりは、エラーログを持たせる作りの独自例外を用意し、catchした場所で異常終了の事由をログに出力する形でも良いと思います。このエラーログは「アプリケーションがしぬほどの事由」に相当するので、それ相応のレイヤで扱うほうがスマートに見えるのです。
74
+
75
+
76
+
77
+ ---
78
+
79
+
80
+
81
+ 想像でコード例を書いてみました。
82
+
83
+ IFixedLogがログの実体だと仮定しています。
84
+
85
+ 部分的に適当なので、頑張って読み替えてください。
86
+
87
+
88
+
89
+ ```csharp
90
+
91
+ static int Main() // エントリポイント
92
+
93
+ {
94
+
95
+ try
96
+
97
+ {
98
+
99
+ test.Do();
100
+
101
+ return 0; // 正常終了
102
+
103
+ }
104
+
105
+ catch (FatalErrorException exception) // 致命的なエラーによる強制終了の場合は
106
+
107
+ {
108
+
109
+ var log = exception.Log; // 例外からLogを取り出す
110
+
111
+ logger.Write(log); // ログを出力
112
+
113
+ return -1;
114
+
115
+ }
116
+
117
+ catch (Exception exception) // 予期しない例外の場合は
118
+
119
+ {
120
+
121
+ var log = CreateLog("予期しないエラー" + exception);
122
+
123
+ logger.Write(log);
124
+
125
+ return -1;
126
+
127
+ }
128
+
129
+ }
130
+
131
+
132
+
133
+ // 独自例外
134
+
135
+ class FatalErrorException : Exception
136
+
137
+ {
138
+
139
+ public IFixedLog Log { get; } // 例外にLogを持たせる
140
+
141
+
142
+
143
+ public FatalErrorException(IFixedLog log) : base()
144
+
145
+ {
146
+
147
+ this.Log = log;
148
+
149
+ }
150
+
151
+ }
152
+
153
+
154
+
155
+ // 仮にこんなインターフェースで
156
+
157
+ interface IGetData<T>
158
+
159
+ {
160
+
161
+ // 必ずGetできる。失敗するのは例外的状況とする。
162
+
163
+ T GetData();
164
+
165
+ }
166
+
167
+ interface IParseData<TRaw, TParsed>
168
+
169
+ {
170
+
171
+ // 与えるrawによっては失敗する。成功した場合はDataの中に結果が入る。
172
+
173
+ (bool Success, TParsed Data) ParseData(TRaw raw);
174
+
175
+ }
176
+
177
+ interface ISendData<T>
178
+
179
+ {
180
+
181
+ // 失敗しない。
182
+
183
+ void Send(T data);
184
+
185
+ }
186
+
187
+
188
+
189
+
190
+
191
+ class TestClass<TData, TParsed>
192
+
193
+ {
194
+
195
+ public Test(IGetData<TData> getter, IParseData<TData, TParsed> parser, ISendData<TParsed> sender)
196
+
197
+ {
198
+
199
+ // ...
200
+
201
+ }
202
+
203
+
204
+
205
+ public void Do()
206
+
207
+ {
208
+
209
+ var raw = default(TData);
210
+
211
+ try
212
+
213
+ {
214
+
215
+ raw = _getter.GetData(); // GetDataは基本成功
216
+
217
+ }
218
+
219
+ catch (GetDataException) // GetDataがGet失敗した時に出す例外「だけ」をcatch
220
+
221
+ {
222
+
223
+ var log = CreateLog("get失敗");
224
+
225
+ throw new FatalErrorException(log); // 致命的なエラーを例外で表現する
226
+
227
+ }
228
+
229
+
230
+
231
+ var parsed = _parser.ParseData(raw); // ParseDataは例外を出さない
232
+
233
+
234
+
235
+ if (!parsed.Success) // parseが失敗した場合は
236
+
237
+ {
238
+
239
+ var log = CreateLog("parse失敗");
240
+
241
+ throw new FatalErrorException(log); // ログ入りスロー
242
+
243
+ }
244
+
245
+
246
+
247
+ _sender.Send(parsed.Data);
248
+
249
+ }
250
+
251
+ }
252
+
253
+ ```
254
+
255
+
256
+
257
+ Doメソッドが自分のやるべきことだけをこなして、それ以外は上位に投げればいいというコードになっているところに注目して下さい。