奇特なブログ

「殊勝に値する行いや心掛け」を意味する、奇特な人になる為のブログです

MVCのMのクラス設計草案など

いや~、一時期どこでも「DDD」の話題が出たり、

また、実際に現場で導入していたりということで、

ちょっと、考えてみますかねと思いまして。

DDDといっても、範囲が広すぎるので、今回はタイトル付近の話ですが。


まずは、参考元だったり、引用元だったりを。

それぞれの記事の「日時」が注目でしょうか。

やってる所は、昔からやってるでしょうからね。


DAOの作成単位について(2006-04-13
https://qa.atmarkit.co.jp/q/564
ドメインオブジェクトとテーブルのマッピング(2009-11-27)
http://masuda220.jugem.jp/?eid=318
[ドメイン駆動設計] ウェブアプリケーションの構造について(2010-09-27)
http://d.hatena.ne.jp/j5ik2o/20100927/1285609764
ドメイン駆動設計のリポジトリパターンをプロジェクトへ持ち込む時の話(2014-6-17)
http://phpmentors.jp/post/88996330583/ddd-repository
DDDにおいてリポジトリとDBのトランザクションは切り離せないのか?(2015-09-14)
http://pospome.hatenablog.com/entry/20150914/1442210241
DDDと高負荷サービス(というか I/Oアクセス)は相性が悪いのか?(2016-06-28)
http://pospome.hatenablog.com/entry/20160628/1467121942
やはりお前たちのRepositoryは間違っている(2018-05-21)
https://qiita.com/mikesorae/items/ff8192fb9cf106262dbf
オブジェクト指向と10年戦ってわかったこと(2018-08-05)
https://qiita.com/tutinoco/items/6952b01e5fc38914ec4e


あとは、↓にソース(の一部)が出てきますが、

そのソースの全容は、以下に入っています。

「草案」だから、まだ途中ですけどね。


other/model_test at master · kitoku-magic/other · GitHub


さて、大きく分けて、3本立てでお送りしたいと思います。

1.色々なIO処理をまとめる

いや、これはモデルを物凄く抽象化して考えた時に、
「ある所から、データを取得する」「ある所に、データを保存する」となり、
その、ある所に、以下の様なストレージが入るんじゃないの?と。


主なストレージ:
RDBMS
KVS
ファイル(HDD)
メモリ
S3


例えば、経験は無いですが、プロジェクト初期において、

「いや~、データを何処から取って来るかは、RDBMSとは限らなくて、まだ決まってないんだよね」

的な話が出ても、別におかしくはないわけで。

なので、以下の様なクラス設計も、あり得るのではないかと。

interface storage_handler
{
	// 該当のストレージからデータを取得する
	public function get();

	// 該当のストレージにデータを保存する
	public function set();
}
// 汎用RDBMS
abstract class rdbms_storage_handler
{
	// トランザクションを開始する
	abstract public function begin();

	// トランザクションをコミットする
	abstract public function commit();
}
// MySQL版
class mysql_storage_handler extends rdbms_storage_handler implements storage_handler
{
	public function get()
	{

	}

	public function set()
	{

	}

	public function begin()
	{

	}

	public function commit()
	{

	}
}
// PostgreSQL版
class postgresql_storage_handler extends rdbms_storage_handler implements storage_handler
{
	public function get()
	{

	}

	public function set()
	{

	}

	public function begin()
	{

	}

	public function commit()
	{

	}
}
// 汎用KVS
abstract class kvs_storage_handler
{
	// 独自のメソッドが必要なら書く
}
// Memcache版
class memcache_storage_handler extends kvs_storage_handler implements storage_handler
{
	public function get()
	{

	}

	public function set()
	{

	}
}
// Redis版
class redis_storage_handler extends kvs_storage_handler implements storage_handler
{
	public function get()
	{

	}

	public function set()
	{

	}
}

ファイル(HDD)・メモリ・S3は省略。

と、こんな感じのクラス構成で。

で、処理の流れとしては、以下の様な感じで。

コントローラー(各処理の開始の起点。サービスを呼び、最後は画面を表示する)

サービス(業務ロジックを書く)

モデル(各ストレージに対する処理を書く)

controller内で

{
	$service = new {サービス名}(new mysql_storage_handler());
	$service->exec();
}

service内で

{
	private $storage_handler;

	public function __construct(storage_handler $storage_handler)
	{
		$this->storage_handler = $storage_handler;
	}

	public function exec()
	{
		// ここで、上記のstorage_handlerを使ってモデルに対して操作を行う
	}
}

みたく書くと、サービスのテストケースを書く時に、
コンストラクタの引数を変えれば、ストレージが変えれますよと。
要は、以下の記事の話とか。

やはりお前たちのRepositoryは間違っている


例えばDBコネクションやストレージのパス等はReposiotoryのインターフェースからは隠蔽され、Repositoryのユーザは永続化ストレージが何であるか(例えばMySQLやRedis等)を意識することなく保存や検索の操作を行うことができるようになります。

これによりRepositoryを利用するロジックは業務的な操作に集中できるようになる他、データベースの移行等の永続化層の変更が発生した際にロジックへの影響を切り離すことができるようになります。

ええ、ただ「実際に変われば」ですけどね。

なので、確率的な話と、

あと、変わった時期が、極端ですが「プロジェクト初期」なのか「納期間近」なのか。

いや、上記の通り、プロジェクト初期なら全然アリでしょうけど。


でも、これをやる動機が、

「ソースの作り的に変更に強いから、仕様は決めなくて良い」的な話だったら、

やめた方が良いですね。


変わったのが、これ「だけ」だったら、そこまでコストかからないかもしれないですけど、

こういうのは、雪崩の様に、押し寄せたりしますし。


あと、昔から思っていたんですが、

「仕様が決まらない状態」って、「無駄コード」が多くなるんですよね。

まさに、↑のソースがそうなるかも的なんですけど。


なので、別に上のコードを書いちゃダメとは思わないですけど、

それはそれとして、「仕様は決めましょう」&「納期直前にMySQL→Redis(全然違うケース)に」とか言われたら、

その分のお金と時間も頂きましょうって所じゃないでしょうかね。

これは、↓の話でも同じですけどね。

2.アクセスするリポジトリの単位

ある単位(ここがドメインか?)にまとめてリポジトリを作るか、テーブル単位で作るか?ってのが、
以下でも書かれていました。

DAOの作成単位について - QA@IT

じゃあ、どんな感じなのか、実際書いてみると、

・ある単位毎

interface order_repository
{
	// 商品を注文する
	public function order();

	// 商品のステータスを変更する(発送済みにしたり、キャンセルしたり)
	public function change_order_status($order_id);

	// 注文IDから注文した商品を取得する
	public function get_item_by_order_id($order_id);

	// 注文した全ての商品を取得する(自分の注文に限定した方が良いでしょうが)
	public function get_all_item();
}
// これもインタフェース実装しても良いけど。あとストレージはDBに固定している
class base_repository
{
	private $db_storage_handler;

	public function __construct(storage_handler $db_storage_handler)
	{
		$this->db_storage_handler = $db_storage_handler;
	}

	// 継承先のクラスからしかアクセスさせない
	protected function select()
	{
		// SELECT文を実行する
		// get()の中に、SQLを実行する処理を書く
		$this->db_storage_handler->get();
	}

	// 継承先のクラスからしかアクセスさせない
	protected function insert()
	{
		// INSERT文を実行する
	}

	// 継承先のクラスからしかアクセスさせない
	protected function update()
	{
		// UPDATE文を実行する
	}

	// 継承先のクラスからしかアクセスさせない
	protected function delete()
	{
		// DELETE文を実行する
	}
}
// order関連のデータ操作を行うリポジトリ
class order_repository_impl extends base_repository implements order_repository
{
	public function order()
	{
		// orderテーブルとorder_itemテーブルにレコードを追加する
		$this->insert();
	}

	public function change_order_status($order_id)
	{
		// orderテーブルのstatusを変更する
		$this->update();
	}

	public function get_item_by_order_id($order_id)
	{
		// order_itemテーブルからorder_idに該当するレコードを取得する
		$this->select();
	}

	public function get_all_item()
	{
		// order_itemテーブルから全てのレコードを取得する(ユーザーIDを指定する方が良いでしょうが)
		$this->select();
	}
}

・テーブル単位毎

interface order_repository
{
	// 商品を注文する
	public function order();

	// 商品のステータスを変更する(発送済みにしたり、キャンセルしたり)
	public function change_order_status($order_id);
}
interface order_item_repository
{
	// 商品を注文する
	public function order_item();

	// 注文IDから注文した商品を取得する
	public function get_item_by_order_id($order_id);

	// 注文した全ての商品を取得する(自分の注文に限定した方が良いでしょうが)
	public function get_all_item();
}

base_repositoryは、おそらく上記と同じなので省略。

// orderテーブルのデータ操作を行うリポジトリ
class order_repository_impl extends base_repository implements order_repository
{
	public function order()
	{
		// orderテーブルにレコードを追加する
		$this->insert();
	}

	public function change_order_status($order_id)
	{
		// orderテーブルのstatusを変更する
		$this->update();
	}
}
// order_itemテーブルのデータ操作を行うリポジトリ
class order_item_repository_impl extends base_repository implements order_item_repository
{
	public function order_item()
	{
		// order_itemテーブルにレコードを追加する
		$this->insert();
	}

	public function get_item_by_order_id($order_id)
	{
		// order_itemテーブルからorder_idに該当するレコードを取得する
		$this->select();
	}

	public function get_all_item()
	{
		// order_itemテーブルから全てのレコードを取得する(ユーザーIDを指定する方が良いでしょうが)
		$this->select();
	}
}

ここは、以下の意見が、一番しっくりきたんですけどね。

https://qa.atmarkit.co.jp/q/564/a/3995/revisions

だから、テーブル単位の方がしっくりきます。

勿論、デメリット(↑のリンク先でも書かれていますが)もあるんですけど、

特に、気にならないレベルかなと思いましたし。

で、呼び出し側からは、これはINSERTの場合ですが、

controller内で

{
	// DBに限定してますが
	$db_storage_handler = new mysql_storage_handler();
	$service = new {サービス名}(
		new order_repository_impl($db_storage_handler),
		new order_item_repository_impl($db_storage_handler)
	);
	$service->exec();
}

service内で

{
	private $order_repository;

	private $order_item_repository;

	public function __construct(order_repository $order_repository, order_item_repository $order_item_repository)
	{
		$this->order_repository = $order_repository;
		$this->order_item_repository = $order_item_repository;
	}

	public function exec()
	{
		$this->order_repository->begin();

		try
		{
			$this->order_repository->order();
			$this->order_item_repository->order_item();

			$this->order_repository->commit();
		}
		catch (Exception $e)
		{
			$this->order_repository->rollback();
		}
	}
}

となるかなと。

いやいや、それだと「$this->order_item_repository->order_item();」を忘れたら(order()と必ず一緒に行う前提)、

バグるじゃないかと。

確かにそうなので、じゃあ以下かなと。

// orderテーブルのデータ操作を行うリポジトリ
class order_repository_impl extends base_repository implements order_repository
{
	private $order_item_repository;

	public function __construct(order_item_repository $order_item_repository)
	{
		$this->order_item_repository = $order_item_repository;
	}

	public function order()
	{
		// orderテーブルにレコードを追加する
		$this->insert();
		// order_itemテーブルにレコードを追加する
		$this->order_item_repository->insert();
	}

	// 以下、省略
}

そして、controller内で

{
    $db_storage_handler = new mysql_storage_handler();
    $service = new test_service(
      // ちょっと歪かなとは思うけど
      new order_repository_impl($db_storage_handler, new order_item_repository_impl($db_storage_handler))
      // ↓が元コード
      // new order_item_repository_impl($db_storage_handler)
    );
    $service->exec();
}

service内で

{
	private $order_repository;

	// private $order_item_repository;

	// public function __construct(order_repository $order_repository, order_item_repository $order_item_repository)
	public function __construct(order_repository $order_repository)
	{
		$this->order_repository = $order_repository;
		// $this->order_item_repository = $order_item_repository;
	}

	public function exec()
	{
		$this->order_repository->begin();

		try
		{
			$this->order_repository->order();
			// $this->order_item_repository->order_item();

			$this->order_repository->commit();
		}
		catch (Exception $e)
		{
			$this->order_repository->rollback();
		}
	}
}

で、いけるんじゃないかと。

じゃあ、テーブル単位だと、JOINがあるSELECTはどうするのか?

class order_repository_impl extends base_repository implements order_repository
{
  private $order_item_repository;

  public function __construct(storage_handler $db_storage_handler, order_item_repository $order_item_repository)
  {
    parent::__construct($db_storage_handler, 'order', array('order_id'), array('order_item' . parent::ENTITY_CLASS_SUFFIX => null));
    $this->order_item_repository = $order_item_repository;
  }

  ・・・省略

  // 注文IDに該当する商品名を取得する
  public function get_order_item_name($order_id)
  {
    $db_storage_handler = $this->get_db_storage_handler();
    $db_storage_handler->set_columns('oi.item_name');
    $db_storage_handler->set_from('`order` o INNER JOIN `order_item` oi ON o.order_id = oi.order_id');
    $db_storage_handler->set_where('o.order_id = :order_id');
    $db_storage_handler->set_bind_params(array(
      array(
        'name' => ':order_id',
        'value' => $order_id,
        'data_type' => PDO::PARAM_INT,
      )
    ));
    $result = $this->select();
    
    return $result;
  }
}
class base_repository
{
  // 継承先のクラスからしかアクセスさせない
  protected function select()
  {
    // SELECT文を実行する
    // get()の中に、SQLを実行する処理を書く
    return $this->db_storage_handler->get();
  }
}
class mysql_storage_handler extends rdbms_storage_handler implements storage_handler
{
  public function get()
  {
    $sql = 'SELECT ' . $this->columns . ' FROM ' . $this->from;
    if ('' !== $this->where)
    {
      $sql .= ' WHERE ' . $this->where;
    }
    // 以下、GROUP BYなどが続く

    $handle_instance = $this->get_handle_instance();
    $this->statement = $handle_instance->prepare($sql);

    foreach ($this->bind_params as $bind_param)
    {
      $this->statement->bindValue($bind_param['name'], $bind_param['value'], $bind_param['data_type']);
    }

    $result = $this->statement->execute();

    return $result;
  }
}

で、問題無し。

で、サービスからは、

class test_service
{
  public function exec()
  {
    $order_id = 1;
    $result = $this->order_repository->get_order_item_name($order_id);
    var_dump($result);
  }
}

という感じで。

「JOINの部分など」を直書きするってのがどうかって所なんでしょうけどね。

↑の「ある単位」毎にってのも、「業務分析」したら、

おそらくある単位が出来てくるとは思いますけど、

これに限らず、「費用対効果」ってのがあるかなと。

保守性を重視ってのが、勿論間違ってるとは思わないけど、

その保守性が活かされる前に、サービスが終了するとか。

「中長期を気にし過ぎて、逆に短期の方がおろそかになった」と言い換えても良いかと思いますが。

逆のケースを「山程」知っているので、そういう発想が出てくるのもアリなんですけど、

「短期が存在しないと、中長期は絶対に存在しない」わけですし。

3.戻り値の情報の持つ単位(テーブル単位?)

ここも、DDDだと「集約」とかがあり、悩ましい所ですね。

で、これはこれで、何度も書いている通り、別に何かがおかしいわけではないけど、でも「費用対効果」が。

なので、以下の様な感じはどうなのかと。

あと、いわゆるGeneration Gapパターンを使ってます。

デザインパターン紹介

// エンティティの基底クラス
abstract class entity
{
  public function is_property_exists($field_name)
  {
    return array_key_exists($field_name, $this->get_table_columns());
  }

  abstract public function get_table_columns();
}
// これは自動生成されるクラス
class order_entity_base extends entity
{
  private $order_id;

  private $order_status;

  // order_id
  public function get_order_id() { return $this->order_id; }
  public function set_order_id($order_id) { $this->order_id = $order_id; }

  // order_status
  public function get_order_status() { return $this->order_status; }
  public function set_order_status($order_status) { $this->order_status = $order_status; }

  public function get_table_columns()
  {
    return array(
      'order_id' => null,
      'order_status' => null,
    );
  }
}
// これも自動生成されるクラス
class order_item_entity_base extends entity
{
  private $order_item_id;

  private $order_id;

  private $item_name;

  // order_item_id
  public function get_order_item_id() { return $this->order_item_id; }
  public function set_order_item_id($order_item_id) { $this->order_item_id = $order_item_id; }

  // order_id
  public function get_order_id() { return $this->order_id; }
  public function set_order_id($order_id) { $this->order_id = $order_id; }

  // item_name
  public function get_item_name() { return $this->item_name; }
  public function set_item_name($item_name) { $this->item_name = $item_name; }

  public function get_table_columns()
  {
    return array(
      'order_item_id' => null,
      'order_id' => null,
      'item_name' => null,
    );
  }
}
// ここを、自分で実装する
class order_entity extends order_entity_base
{
  private $order_item_entities;

  // order_item_entities
  public function get_order_item_entities() { return $this->order_item_entities; }
  public function set_order_item_entities($order_item_entities) { $this->order_item_entities = $order_item_entities; }
  public function get_order_item_entity($index) { return $this->order_item_entities[$index]; }
  public function add_order_item_entity(order_item_entity_base $order_item_entity) { $this->order_item_entities[] = $order_item_entity; }
}
// 何もないけど(笑)
class order_item_entity extends order_item_entity_base
{
}

で、さっきのJOINでSELECTしたデータを取ってみましょう。

class order_repository_impl extends base_repository implements order_repository
{
  private $order_item_repository;

  public function __construct(storage_handler $db_storage_handler, order_item_repository $order_item_repository)
  {
    // 第2引数は、テーブル名。第3引数は、主キー(複合に対応する為、配列)。第4引数は、関連するエンティティクラス名(文字列)
    parent::__construct($db_storage_handler, 'order', array('order_id'), array('order_item' . parent::ENTITY_CLASS_SUFFIX => null));
    $this->order_item_repository = $order_item_repository;
  }

  public function get_order_item_name($order_id)
  {
    // 全件取る様に変えました
    $db_storage_handler = $this->get_db_storage_handler();
    $db_storage_handler->set_columns('*');
    $db_storage_handler->set_from('`order` o INNER JOIN `order_item` oi ON o.order_id = oi.order_id');
    $db_storage_handler->set_where('');
    $db_storage_handler->set_bind_params(array(
      /*array(
        'name' => ':order_id',
        'value' => $order_id,
        'data_type' => PDO::PARAM_INT,
      )*/
    ));
    $result = $this->select();

    $entities = array();
    if (true === $result)
    {
      $entities = $this->fetch_all_associated_entity();
    }

    return $entities;
  }
}

で、この「$this->fetch_all_associated_entity()」って所は、

class base_repository
{
  const ENTITY_CLASS_SUFFIX = '_entity';

  private $db_storage_handler;

  private $table_name;

  private $primary_keys;

  private $associated_entities;

  public function __construct(storage_handler $db_storage_handler, $table_name, $primary_keys, $associated_entities)
  {
    $this->db_storage_handler = $db_storage_handler;
    $this->table_name = $table_name;
    $this->primary_keys = $primary_keys;
    $this->associated_entities = $associated_entities;
  }

  protected function fetch_all_associated_entity()
  {
    $this->db_storage_handler->set_repository_class($this);
    $this->db_storage_handler->set_entity_class_name($this->table_name . self::ENTITY_CLASS_SUFFIX);
    // 第2引数にfalseを設定すると、主キーが同じレコードでも、別の配列の要素となる
    //return $this->db_storage_handler->fetch_all_associated_entity($this->get_associated_entities(), false);
    return $this->db_storage_handler->fetch_all_associated_entity($this->get_associated_entities());
  }
}
class mysql_storage_handler extends rdbms_storage_handler implements storage_handler
{
  const ENTITY_CLASS_DIRECTORY = '../../model/entity/';

  public function fetch_all_associated_entity($associated_entities, $unique_primary_key_data = true)
  {
    $entities = array();

    if (0 === count($associated_entities))
    {
      return $entities;
    }

    if (null !== $this->statement)
    {
      // TODO: 当然、存在チェックもする
      require_once(dirname(__FILE__) . DIRECTORY_SEPARATOR . self::ENTITY_CLASS_DIRECTORY . $this->entity_class_name . '.php');
      $main_entity = null;
      while (false !== ($row = $this->statement->fetch(PDO::FETCH_ASSOC)))
      {
        $entity_created = false;
        // 主キーが、既に設定済みのエンティティがある時に、インスタンスを再生成しない為の処理
        if (true === $unique_primary_key_data &&
            null !== $main_entity)
        {
          $row_values = array();
          $entity_values = array();
          $main_entity_index = array_search($main_entity, $entities, true);
          $primary_keys = $this->repository_class->get_primary_keys();
          foreach ($primary_keys as $primary_key)
          {
            if (true === isset($row[$primary_key]))
            {
              $row_values[] = $row[$primary_key];

              if (false !== $main_entity_index)
              {
                $method_name = 'get_' . $primary_key;
                $entity_values[] = call_user_func_array(array($entities[$main_entity_index], $method_name), array());
              }
            }
          }
          // 取得したカラムに、全ての主キーが含まれている &&
          // 既に設定済みのエンティティの全ての主キーの値に、nullが含まれていない &&
          // 取得したカラムの主キーの値と、既に設定済みのエンティティの主キーの値が同じ
          if (count($primary_keys) === count($row_values) &&
              false === in_array(null, $entity_values, true) &&
              $entity_values === $row_values)
          {
            $entity_created = true;
          }
        }
        if (false === $entity_created)
        {
          $main_entity = new $this->entity_class_name;
        }
        // 関連するエンティティにデータを入れていく
        foreach ($associated_entities as $associated_entity_class_name => $value)
        {
          require_once(dirname(__FILE__) . DIRECTORY_SEPARATOR . self::ENTITY_CLASS_DIRECTORY . $associated_entity_class_name . '.php');
          $associated_entity_class = new $associated_entity_class_name;
          foreach ($row as $column_name => $column_value)
          {
            // 関連するエンティティのフィールドに、取得したカラムが存在するか?
            if (true === $associated_entity_class->is_property_exists($column_name))
            {
              $method_name = 'set_' . $column_name;
              call_user_func_array(array($associated_entity_class, $method_name), array($column_value));
            }
            if (false === $entity_created &&
                true === $main_entity->is_property_exists($column_name))
            {
              $method_name = 'set_' . $column_name;
              call_user_func_array(array($main_entity, $method_name), array($column_value));
            }
          }
          // 関連するエンティティに入ったデータを、メインのエンティティの配列に入れていく
          $method_name = 'add_' . $associated_entity_class_name;
          call_user_func_array(array($main_entity, $method_name), array($associated_entity_class));
        }
        if (false === $entity_created)
        {
          $entities[] = $main_entity;
        }
      }
    }

    return $entities;
  }
}

で、最後にサービス。

class test_service
{
  public function exec()
  {

    $order_id = 1;
    $result = $this->order_repository->get_order_item_name($order_id);
    var_dump($result);
  }
}

このコードで、以下のテーブルの状態の時に。

mysql> select * from `order`;
+----------+--------------+
| order_id | order_status |
+----------+--------------+
|        1 |            1 |
|        2 |            1 |
+----------+--------------+
2 rows in set (0.00 sec)
mysql> select * from `order_item`;
+---------------+----------+---------------------------------+
| order_item_id | order_id | item_name                       |
+---------------+----------+---------------------------------+
|             1 |        1 | order_item::item_nameテスト1    |
|             2 |        1 | order_item::item_nameテスト2    |
|             3 |        2 | order_item::item_nameテスト3    |
|             4 |        2 | order_item::item_nameテスト4    |
|             5 |        2 | order_item::item_nameテスト5    |
+---------------+----------+---------------------------------+
5 rows in set (0.00 sec)

以下の様な戻り値になりますと。

array(2) {
  [0]=>
  object(order_entity)#8 (3) {
    ["order_item_entities:private"]=>
    array(2) {
      [0]=>
      object(order_item_entity)#9 (3) {
        ["order_item_id:private"]=>
        string(1) "1"
        ["order_id:private"]=>
        string(1) "1"
        ["item_name:private"]=>
        string(31) "order_item::item_nameテスト1"
      }
      [1]=>
      object(order_item_entity)#10 (3) {
        ["order_item_id:private"]=>
        string(1) "2"
        ["order_id:private"]=>
        string(1) "1"
        ["item_name:private"]=>
        string(31) "order_item::item_nameテスト2"
      }
    }
    ["order_id:private"]=>
    string(1) "1"
    ["order_status:private"]=>
    string(1) "1"
  }
  [1]=>
  object(order_entity)#11 (3) {
    ["order_item_entities:private"]=>
    array(3) {
      [0]=>
      object(order_item_entity)#12 (3) {
        ["order_item_id:private"]=>
        string(1) "3"
        ["order_id:private"]=>
        string(1) "2"
        ["item_name:private"]=>
        string(31) "order_item::item_nameテスト3"
      }
      [1]=>
      object(order_item_entity)#13 (3) {
        ["order_item_id:private"]=>
        string(1) "4"
        ["order_id:private"]=>
        string(1) "2"
        ["item_name:private"]=>
        string(31) "order_item::item_nameテスト4"
      }
      [2]=>
      object(order_item_entity)#14 (3) {
        ["order_item_id:private"]=>
        string(1) "5"
        ["order_id:private"]=>
        string(1) "2"
        ["item_name:private"]=>
        string(31) "order_item::item_nameテスト5"
      }
    }
    ["order_id:private"]=>
    string(1) "2"
    ["order_status:private"]=>
    string(1) "1"
  }
}

まだINSERT・UPDATE・DELETE等もありますが、といっても実装出来ない感じはしないですけどね。
あと、これだと、エンティティに限らず「子クラスをいじっている分(各機能を実装している時)」には、
そんなに「複雑」じゃないと思うんですよ、「集約」に比べれば。
といっても、その複雑ってのが、人によってマチマチと分かるのが、以下なのですが。

ウェブアプリケーションの構造について - じゅんいち☆かとうの技術日誌

コメント欄の方です。


気持ちはわかるのですが、こんなFATな設計を実装できるほど予算のあるプロジェクトなんて今どきないんですよね…
そんな時代だし、設計思想や言語だってどんどん変わって行っちゃうからこそ、設計的にも言語的にもライトなものが全盛なんじゃないかと思うのですが、いかがでしょうか。

もうすこし現実的な「現代版ドメイン駆動設計」を披露していただけると嬉しいです。

「予算のあるプロジェクト」ってのがポイントかなぁと。

でも、予算を取るのって、当然「成功の確度」が高くないと無理で、

で、昨今やる前から、その辺がわかるかっていうと・・・

だから、スタートアップなんかは、間違いなく合わないと思いますね。

逆に、実績があるならアリなわけですが、その場合は当然「時間に余裕がある」はず(何故かないってのも見ますが)で、

DDDのデメリット部分も緩和する事が出来るんじゃないかなと。

逆に、肯定的な意見もあり、


何を持って FAT なのかってことだと思うので、人によっては FAT に見えるのは理解できます。
でもその FAT って部分的にトラウマ(色眼鏡)が含まれていることってありえませんか?

個人的には FAT には見えないですね。私の理解では、個別のソースはかなりすっきりすると思います。

実は FAT ってコードだけでなく設計やマネジメントなど全般的に考えないといけないテーマかもしれませんね。

どこかにゆがみがあるから、FAT に見えてしまうと思います。

「実は FAT ってコードだけでなく設計やマネジメントなど全般的に考えないといけないテーマかもしれませんね。」

ええ、そうじゃないですかね、まさに↑がその辺の話かなと。

あと、クラスが多いから、IDEがないとキツイとかも考える必要があるかなと。

あと、「でもその FAT って部分的にトラウマ(色眼鏡)が含まれていることってありえませんか?」って所。

うん、EJB(幸い、業務で触ることは無かったですが)とかね(苦笑)

あと、気になったコメントは、


UserTableにユーザプロファイル情報を含めることはできます。
しかし、カラムの数が多い場合は目的の情報を探すのが困難になるので、
個人情報に関連する情報はプロファイルという別のテーブルに切り出した方が管理しやすいというモチベーションですね。
う~ん、テーブルは、正規化目的じゃないなら、細かくしない方が良いと思いますけどね。

以下に、その辺が言及されてましたが。

ドメインオブジェクトとテーブルのマッピング | システム設計日記

DDDと高負荷サービス(というか I/Oアクセス)は相性が悪いのか? - pospomeのプログラミング日記

あと、クラス数が増えるって事については、以下の観点もあるかなと思いますね。

ドキュメントが充実(ここも時間がかかる)していればアリかとは思いますけど。

マジックナンバー7の誤解 : みんな、他人の頭を過大評価しすぎているんじゃないだろうか。それって不幸だ。 - たたたた。

あと、乱用って意味だと、以下かなぁと。

「全部」のswitch文にstateパターンを適用するの?的な。

switch文を使ってはいけない: Architect Note

いや、stateパターン、以下とか、むしろ好きですけどね(笑)

https://github.com/kitoku-magic/final_magic/blob/master/view.php#L141

あと、複雑なSELECT文についてって所だと、

やはりお前たちのRepositoryは間違っている


恐らく、ユーザのIDをキーに購入履歴テーブルをジョインし、購入日で絞り込んだ件数をcountするクエリをORMで頑張って実現しようとするでしょう。
これらのクエリは大抵プログラム的にもメンテナンスしづらいものとなり、更にユーザ一覧画面などで複数件実行するとパフォーマンス上の問題を引き起こしたりします。


2) CQS(Command Query Separation)を適用してクエリを切り出す

上の問題に対して、下を適用すれば解決するって所に、因果関係があるのか?という。

別のクラスに切り出しただけでは?と。

なんか、「複雑化を排除しようとしたら、また別の形で複雑化した」感がありますね。

「複雑なものを単純に」って響きは良いですが、出来たら天才クラスだと思うので、

「複雑なものは複雑」で良いと思ってるんですけどね。

あと、オブジェクト指向全般としては、以下が大切かなと。

オブジェクト指向と10年戦ってわかったこと


今回例に取り上げたのは、ファーストフードのシステムなので「食品以外のものを販売する」というような変更が発生することは、あまり考えられません。「もしかしたらガソリンやタイヤを販売するかもしれないじゃないか!可能性はゼロではないのだから柔軟に対応できるように設計するべきだ!」と思うかもしれません。しかし、将来的に変更のない箇所を無駄に柔軟に設計することは過剰実装であり、プロジェクトの設計を複雑なものに変えてしまうため、行わない方が良いこともあるのです。
「どこが変わりそうか?」ってのは、「経験値」が問われそうな所だと思いますけどね。

なので、「そう簡単に白黒付けられる話ではない(白黒付けるのは否定的じゃないですよ)」と思ってますけどね。

これで終わりです。

あと、GitHubにも上がってますが、UMLツールを探していました。

Ozlog: BOUML

Miracle Jungle.: フリーのUMLツールをいくつか試してみた

katapedia/リバースエンジニアリングツール.md at master · yutakatay/katapedia · GitHub

「umbrello」が良いんじゃないかと思ったんですが、継承先が複数ある時に、何故か線が引けないんですよね。

BOUMLの方が良いのかもしれない。

あと、まだあのソースは途中なので、完成したら再度告知すると思います(笑)

あと、全然関係ないですが、椎名林檎アイデンティティ http://j-lyric.net/artist/a00450a/l000c60.html 流しながら書いてました(笑)

Let's Encryptを導入したりHTTP2に対応したりIPv6に対応したりなど

acme-challengeやIPv6とか、ちょっと怪しい箇所がありますけどね。

以下、設定した内容の羅列です。

OS:CentOS6.3


1.IPv6の有効化

## 以下のさくらVPSの設定方法の通り

https://help.sakura.ad.jp/hc/ja/articles/115000070782

## 上記が成功したら、各レジストラなどのDNS設定で、AAAAレコードに上記のIPv6アドレスを登録する


2.OpenSSLの導入

既に、1.1.0bを導入済みの為、省略


3.Nginxの最新化

$ sudo yum install gcc-c++

$ cd ~/src

$ curl -O ftp://ftp.csx.cam.ac.uk/pub/software/programming/pcre/pcre-8.42.tar.gz

$ tar zxvf pcre-8.42.tar.gz

$ cd pcre-8.42

$ ./configure

$ make

$ sudo make install

$ cd ~/src

$ curl -O http://nginx.org/download/nginx-1.15.3.tar.gz

$ tar zvxf nginx-1.15.3.tar.gz

$ cd nginx-1.15.3

## OpenSSLのパスは、インストールしたバイナリの方ではなく、ダウンロードしたソースの方を指定する
## また、「openssl version」で表示されるバージョンと同じになっているかも確認
$ echo './configure --prefix=/opt/nginx1.15.3 --sbin-path=/opt/nginx1.15.3/sbin/nginx --pid-path=/opt/nginx1.15.3/logs/nginx.pid --lock-path=/opt/nginx1.15.3/logs/nginx.lock --error-log-path=/opt/nginx1.15.3/logs/error.log --http-log-path=/opt/nginx1.15.3/logs/access.log --with-openssl={OpenSSLのダウンロード先}/openssl-1.1.0b --user=nginx --group=nginx --with-http_v2_module --with-http_ssl_module --with-http_realip_module' > nginx_ccc

$ sh ./nginx_ccc

$ make

$ sudo make install

$ echo '/usr/local/lib' | sudo tee -a /etc/ld.so.conf.d/pcre.conf

$ sudo ldconfig

$ sudo rm -rf /opt/nginx

$ sudo ln -s /opt/nginx1.15.3 /opt/nginx

$ sudo vim /etc/profile

## 変更前
export PATH=$PATH:/opt/php/bin:/opt/git/bin:/opt/openssl/bin:/opt/vim/bin

## 変更後
export PATH=$PATH:/opt/php/bin:/opt/git/bin:/opt/openssl/bin:/opt/vim/bin:/opt/nginx/sbin

$ . /etc/profile

$ sudo vim /opt/nginx/conf/nginx.conf

## 変更前:

#user  nobody;
worker_processes  1;

#error_log  logs/error.log;

#pid        logs/nginx.pid;

#access_log  logs/access.log  main;

server_name  localhost;

location / {
    root   html;
    index  index.html index.htm;
}

## 変更後:

user  nginx;
worker_processes  2;

error_log  logs/error.log;

pid        logs/nginx.pid;

access_log  logs/access.log;

server_name  {ホスト名};

location / {
    root   {ドキュメントルートのディレクトリ};
    index  index.html index.htm;
}

## 以下をhttpディレクティブ内に追加
server_tokens off;

$ sudo mkdir -p {ドキュメントルートのディレクトリ}/.well-known/acme-challenge

$ sudo chmod -R 777 {ドキュメントルートのディレクトリ}/.well-known

$ sudo service nginx restart


4.証明書取得ツールを導入

$ sudo su -

# curl https://dl.eff.org/certbot-auto -o /usr/local/bin/certbot-auto

# chmod 700 /usr/local/bin/certbot-auto


5.root権限のあるユーザーで証明書取得ツールを実行して証明書を取得

## テスト実行
# certbot-auto certonly --webroot -w {ドキュメントルート} -d {ドメインFQDN)} --email {連絡先メールアドレス} --agree-tos -n --test-cert

## 本番実行
# certbot-auto certonly --webroot -w {ドキュメントルート} -d {ドメインFQDN)} --email {連絡先メールアドレス} --agree-tos -n --force-renewal


6.PFSのためのDHパラメータの生成

# mkdir -p /opt/nginx/ssl

# openssl dhparam 2048 > /opt/nginx/ssl/dh2048.pem


7.取得した証明書をWebサーバーに適切に設定する

# vim /etc/sysconfig/iptables

## 以下を追加

:HTTPS - [0:0]

-A HTTPS -m hashlimit --hashlimit 1/m --hashlimit-burst 10 --hashlimit-name httpscon --hashlimit-mode srcip --hashlimit-htable-expire 120000 -j RETURN
-A HTTPS -j LOG --log-level info --log-prefix "[ HTTPS attack ]: "
-A HTTPS -j DROP

-A SERVICE -p tcp --dport 443 -j HTTPS

-A SERVICE -p tcp --dport 443 -j ACCEPT

# vim /etc/sysconfig/ip6tables

## 基本iptablesと同じだが、ICMP関連(以下の他にもある)に注意

-A INPUT -p icmpv6 -j ACCEPT

# vim /opt/nginx/conf/nginx.conf

## 以下を追加

server {
  server_name {ドメイン名};
  # IPv4とIPv6の両方でHTTPリクエストを受け付ける。
  listen *:80;
  listen [::]:80;
  # HTTPからのアクセスをHTTPSへリダイレクトする。
  return 301 https://$host$request_uri;
}

server {
  listen 443 ssl http2 default_server;
  listen [::]:443 ssl http2 default_server;

  server_name {ドメイン名};

  ssl_certificate     /etc/letsencrypt/live/{ドメイン名}/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/{ドメイン名}/privkey.pem;

  ssl_dhparam /opt/nginx/ssl/dh2048.pem;

  ssl_protocols TLSv1 TLSv1.1 TLSv1.2;

  ssl_prefer_server_ciphers  on;
  ssl_ciphers  'EECDH+AESGCM+AES128 EECDH+AESGCM EECDH+AES128 EECDH+AES256 !aNULL !eNULL !SSLv2 !SSLv3';

  # OCSPステープリングを有効にする。
  ssl_stapling on;
  ssl_stapling_verify on;
  ssl_trusted_certificate /etc/letsencrypt/live/{ドメイン名}/chain.pem;

  ssl_session_cache   shared:ssl:10m;
  ssl_session_timeout 10m;

  # 31536000は365日
  add_header  Strict-Transport-Security "max-age=31536000; includeSubdomains";

  location ^~ '/.well-known/acme-challenge' {
    default_type "text/plain";
    root   {ドキュメントルートのディレクトリ};
  }

  ・・・その他
}


8.Nginxなどの再起動

## 以下のコマンドを実行する前に、上記を設定したターミナルを閉じずに、

## 別に1~3つターミナルを起動してから動作確認すること
# service iptables restart

# service ip6tables restart

## nginx.confのシンタックスを確認
# nginx -t

## 問題無ければ再起動
# service nginx restart


9.動作確認

## HTTPSのリクエストをテスト
# curl -I https://{ドメイン名}/

## HTTPのリクエストをテスト(HTTPSにリダイレクトされる)
# curl -I http://{ドメイン名}/

## HTTP/2のリクエストをテスト(curlのバージョンがHTTP2に未対応の為、失敗したので、最新のブラウザからアクセスし、アクセスログで確認)
# curl --http2 -I https://{ドメイン名}/

## OCSPステープリングのテスト
# openssl s_client -connect {ドメイン名}:443 -CAfile /etc/pki/tls/certs/ca-bundle.crt -status

  • CAfileを付けないと、以下のエラーが表示され、ちょっとハマった
verify error:num=20:unable to get local issuer certificate
Verification error: unable to get local issuer certificate
Verify return code: 20 (unable to get local issuer certificate)

## IPv6のテスト(以下コマンドで、「Trying {IPv6アドレス}... connected」となるのは確認)
# curl --verbose -I https://{ドメイン名}/


10.証明書更新用のスクリプトなどをcronなどに仕掛ける

# vim /etc/crontab

# 奇数日の3時に実行
00 03 1-31/2 * * root /usr/local/bin/certbot-auto renew -q --no-self-upgrade --deploy-hook "/sbin/service nginx restart"


以上です。

IPv6で、以下が上手くいかないんですけど、まだDNSが浸透(って言っちゃいかんのでしょうけどw)してないからですかね。
何日か待ってみますけど。

# curl --verbose -g 'https://[{IPv6アドレス}]/'

以下では、IPv6DNS(これは、レジストラ側が対応してないのか?)を除いて、上手くいったから大丈夫そうですがね。

IPv6 test - web site reachability

あと、以下のSSLのテストサイトなんですけど、
ファイアーウォールでガチガチにしていると、そもそもチェックの為の接続に失敗するという(笑)
接続出来ないのが一番堅いんじゃないの?(笑)(チェック観点が違うから、そんな事は無いと思いますが)

SSL Server Test (Powered by Qualys SSL Labs)

あと、以下は、公共性の高いサイトだと、
古いOSやブラウザにも対応しないといけなさそうで、結構悩ましいでしょうね。
僕のホームページは、まぁ良いだろう(笑)

ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers  'EECDH+AESGCM+AES128 EECDH+AESGCM EECDH+AES128 EECDH+AES256 !aNULL !eNULL !SSLv2 !SSLv3';

ただのHTTPS化だけで終わらそうと思ったら、意外と欲が出てしまった(笑)

参考:
Webサーバー nginx における SSL証明書設定の安全性向上 ~SSL Server Test で A+ 判定を目指して~ | SaintSouth.NET
https://qiita.com/moutend/items/a14afc83f256af8543aa
無償SSLサーバー証明書Let's Encryptの普及とHTTP/2および常時SSL化 | OSDN Magazine
じゃあ ssl_ciphers (SSLCipherSuite) は何を指定したらいいの!? って話 | おふろめ
Let's EncryptのSSL証明書更新時にサービスを再起動する - Qiita
Nginx の location ブロックのパターン化
OpenSSLコマンドで証明書のチェック | Pistolfly
Use a bought SSL certificate

続・ダブルブッキングを防ぐ予約追加プログラムを考えた

前回:
kitoku-magic.hatenablog.com

さぁ、思ったよりも早く、再開の時期がやってきました。

今回は、以下の差分を載っけました。

github.com

で、まず今回の差分で、特記しておきたい所を。


$retry_count = 5;
このリトライ回数なんですけど、これが特に1回だと、ダブルブッキングしていない正常なレコードでも入らない事が多く、

5回だと、殆ど発生しない(0%ではないですが)という結果でしたね。

どっちかというと、PostgreSQLの方がレコードが入らない事が多い感じ。

次は、


array(
PDO::ATTR_EMULATE_PREPARES => false,
// エラーモードを指定しないと、エラー時に例外にならないので必ず指定
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
)
ここが、今回一番驚きましたかね。

PDOって、そういう動きだったのね。

どうも、これに限らないですが、いわゆる「デフォルト値には、センスが出る」と思いましたね。

「ATTR_EMULATE_PREPARES 」も、デフォルトはtrueなんですよね・・・違和感全開ですね。

で、上記エラーモードを追加した事で、前回MySQLでよく分からない動きをしていた所は明確になりました。

単に、例外を投げていたという(笑)

なので、後で出てきますが、例外処理の所が、かなり賑やかになっています(笑)

次は、


if ('postgresql' === $db_system)
{
// BEGINの後に書かないと、有効にならない
$pdo->exec('SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;');
}
PostgreSQLは、REPEATABLE READだとダブルブッキングしてしまう(ネクスキーロックの有無の差かな?)ので、

こうしたんですがね。

BEGINの後って事なので、逆に言えば元(デフォルトはREAD COMMITED)に戻す必要はないと思いますが。

あと、SERIALIZABLEの事をどうやら勘違いしてまして、

直列化っていうから、てっきり前のトランザクションが終わるまで、処理が開始しないと思ったら、

普通に、並列で処理が走るんですね(笑)

なんですが、並列で処理は走るけれども、レコードの状態が直列化している前提を守ろうとするみたいで、

それが守れなくなると、エラー(これは後述)になるっぽいです。

次は、


SELECT :conference_room_id_select_column, :reservation_date_select_column, :reservation_time_start_select_column, :reservation_time_end_select_column
ここは、前回は「from dual」があったんですが、

PostgreSQLがあったらダメで、MySQLも無くてもOKだったので、こうなりました。

あんまり、こういうSQLは、デバッグがしづらい(SELECTが悪いのか、INSERTが悪いのか分かりにくい)ので、

書きたくないんですが、別々にする事で、どうしてもSQL間で「隙間」が出来てしまい、並列でレコードを入れにいくので。

次は、


if ('mysql' === $db_system)
{
if ('40001' === $e->errorInfo[0] && 1213 === $e->errorInfo[1])
{
// デッドロックの時は、リトライする
}
else if ('23000' === $e->errorInfo[0] && 1062 === $e->errorInfo[1])
{
// ユニーク制約エラーの時は、すぐに終了する
break;
}
else if ('HY000' === $e->errorInfo[0] && 1205 === $e->errorInfo[1])
{
// ロック取得のタイムアウトの時は、すぐに終了する(秒数の設定によっては、リトライでも良いが)
break;
}
else
{
// 上記以外のエラーの時は、すぐに終了する
break;
}
}
else if ('postgresql' === $db_system)
{
if ('40001' === $e->errorInfo[0] && 7 === $e->errorInfo[1])
{
// シリアライズエラーの時は、リトライする
}
else if ('23505' === $e->errorInfo[0] && 7 === $e->errorInfo[1])
{
// ユニーク制約エラーの時は、すぐに終了する
break;
}
else if ('55P03' === $e->errorInfo[0] && 7 === $e->errorInfo[1])
{
// ロック取得のタイムアウトの時は、すぐに終了する(秒数の設定によっては、リトライでも良いが)
break;
}
else
{
// 上記以外のエラーの時は、すぐに終了する
break;
}
}
ちょっと長いですが、catchの中のコードですね。

特に、MySQLデッドロック時と、PostgreSQLシリアライズエラーの時のリトライ。

MySQLの方は、前述のPDOのエラーモードを指定していなかった時のエラーが、

どうやらコレになっていただけの様で。

で、デッドロックなのにリトライって所ですが、デッドロックってロック待ちになるわけでもなく、

下手にレコードが入ってダブルブッキングになるわけでもなく、あと少なくとも今回の場合は避けれそうにないので、

別にしても良いのでは?と。というより、リトライしなかったら、単にレコードが入らないんですけどね。

なので、リトライをしています。

PostgreSQLの方は、おそらく直列化した状態が維持出来なくなった時に前述のエラーが起きているっぽい。

ただ、こっちはエラーの発生タイミングが、COMMITしようとする時の様で、

おそらくそれが原因だと思うんですが、AUTO_INCREMENTなカラム(SERIALの事)の値が、

歯抜けになってしまいます(苦笑)

歯抜け気にしないなら良いわけですが、MySQLはならないのでねぇ。

コードからの引用は以上。

MySQLPostgreSQLの比較でいうと、

1.速度は、PostgreSQL(登録だけなら0.2秒ぐらい)、MySQL(0.5秒ぐらい)と、意外にもPostgreSQLに軍配。

2.ただ、PostgreSQLは、歯抜けレコードが出来る(これはもう、テーブルロックしかないか?)

3.正常なレコードが入る確率は、どっちかといえばMySQLの方が高そう(でも、たった今、入らなかったけど)

他の、アプローチとしては、「事前に、レコードを入れておく」でしょうかね。

これなら、そもそもSELECTの空振りがしないので。

ただ、時間帯が固定される(極論、1分単位とかでレコードを作れば、ほぼ防げますが)のと、

レコード数が多くなるので、それはそれで問題が。

今回は、こんな所でしょうか。

それにしても、WEBでの予約システムってのは、最近意外と多くて、

1.↑の社内の会議室予約

2.音楽ライブチケットの予約

3.寿司の予約(くら寿司

4.飛行機の座席予約

5.美容室とかの予約

6.公共施設の予約(ちょっとビックリ)

7.レストランの予約

と、まだまだあるんでしょうけど、排他制御が不要なWEBの予約システムは、きっと無いだろうなと。

なので、また再開する事でしょう、きっと(笑)

そういえば、全然関係ないですが、この実験をしていた時、

B'zのFeverっていう曲 http://j-lyric.net/artist/a00067d/l004249.html などを、流しながらやってましたね(笑)

Gitでディレクトリ名を変更してリネームを検知する


$ git status
On branch master
Changes to be committed:
(use "git reset HEAD ..." to unstage)

renamed: a/test.php -> A/test.php

要は、↑の状態を「case-"in"sensitive」でどうやるのかという(笑)
意外と、ググっても出てこなかった(じゃなきゃ書かないが)ですよ。

git version 2.19.0.windows.1の、GitBash上で行っています。


$ ls -al
total 36
drwxr-xr-x 1 ? 197121 0 9月 14 21:56 ./
drwxr-xr-x 1 ? 197121 0 9月 14 21:48 ../
drwxr-xr-x 1 ? 197121 0 9月 14 21:56 .git/
drwxr-xr-x 1 ? 197121 0 9月 14 21:56 a/


$ cp -r a a_


$ git rm -r a
rm 'a/test.php'


$ mv a_ A


$ git add A


$ git status
On branch master
Changes to be committed:
(use "git reset HEAD ..." to unstage)

renamed: a/test.php -> A/test.php

以上。

環境によるのか、「git add A」は、実行しなくても、その前にrenamedになっているケースもありましたね。

case-sensitiveなら、言うまでもなく


$ git mv a A

で終了(笑)
case-insensitiveなOS(というかファイルシステム)なんて無くなれば良いのに(笑)

2018年8月11日のダイジェスト

1. サマータイムは簡単、という記事があまりにあれな件 - novtanの日常


この記事と関係ないんですけど、ふと思ったのは、
終了の時には、「同じ時間が再び訪れる」わけですよね。
なので、「テレビで、一度放送した番組を、また放送するのか?」とか、
「新聞のテレビ欄の、時間を表示している箇所は、どう表示するのか?」とか、
「26時間(22時間も)だと、新聞のテレビ欄のレイアウトを変えないといけないのでは?」とか、
思いましたね。「マスコミにも、影響ありますよ」と。


2. 直感的に分かりにくい気が


というのも、開始時には「1時間ジャンプする」のに、
「午前7時スタート予定のマラソンが、午前5時スタートになる」というのが、
パッと見、「なんで?時間が進むのに」と。

サマータイムの開始切り替え時に、
元々の時間→午前2時(時間は適当)
切り替え後のサマータイム時間→午前4時

元々の時間→午前5時
切り替え後のサマータイム時間→午前7時

あぁ、「元々の5時の時間の時に、マラソンがスタートするから」って事ですか。
だから、「日(当たり前だが、ここは変わらないので)が昇って暑くなる前に終わる」と。

じゃあ、
「(サマータイム切り替え後での)午前7時スタート予定のマラソンが、(元々の時間の)午前5時スタートになる」
って事ですね。


3. アスペルガー症候群の彼を、殺そうとした私 | peek a boo - ピークアブー


う~ん、むしろこの記事自体を上げるって話の時に、最初の方に書いてあった、
「ソースは?根拠は?主観じゃないの?」というのを言った方が良い印象なんですが。
その方が、一貫しているわけですし。
上げる事でメリットがあると、どうして思ったのかが不思議ですね。
メリットあったとしても、一時的かなと。


4. Hagex氏の訃報とネットへの向き合い方について - novtanの日常


「リアルとネットで、言葉遣いを変えない」のが大事なんじゃないでしょうかね。


5. オウム真理教の麻原彰晃がビートたけし、とんねるずに語ったこと


そうそう、当時出てましたね。
といっても、別に「ふ~ん」という印象だった気がしてますけど。


6. 他人を信用できない「ROM専」日本人のせいで経済が伸びない?(ニューズウィーク日本版) - Yahoo!ニュース


この記事へのコメント自体が、この記事の内容の正当性を担保している感が。


7. LGBTへの日本の行政支援は「度が過ぎる」のか | ワールド | 最新記事 | ニューズウィーク日本版 オフィシャルサイト


誰も反論を書かなければと思ってましたが、そんなことはやっぱり無かったですね。
あと、全文も読みましたが、あぁだからリスクの取り方が・・・
「元々は悪い意味などなかったが、度重なる悪用に伴い、イメージが悪くなった」って所でしょうか。


8. 新宿駅で「女性にぶつかる男」の動画が拡散 「理由がわかるかも」と男性の本音も – しらべぇ | 気になるアレを大調査ニュース!


この記事よりも、この記事の後半からリンクが貼られている原因の方が興味深かったですね。
以前に読んだ、痴漢の本の中に書いてあった内容と似てる印象。


9. どちらが美徳? 日本人に根づく「恥の文化」と諸外国の「罪の文化」|U-NOTE [ユーノート]


う~む、文化ですか。
シチュエーションによる様に思いますね。
恥は恥で良いシチュエーションの時もあると思っているので。


10. IT勉強会に行かなくなった


人がちらほらいらっしゃる様ですが、そういえば僕も最近はでしたね。
いや、むしろ一時期が行き過ぎだったのか(笑)
スライドをアップしているから、ブログに書くのとさほど変わらないですし。
なので、逆に「スライドを公開しない」とか、
参加者同士での有意義な交流とかが無いと、
行くインセンティブがないと感じやすくなるかもしれないですね。


11. 公正中立な技術などない


あぁ、これは名言ですね。
公正中立っぽく見えますけど、
技術は道具である以上、何がしかの解決に特化しており、
特化している以上、哲学(包丁は、料理をする為のものであり、動物を傷付ける為のものではないとか)が、
含まれているって感じでしょうかね。


12. いわゆる「お前が言うな」

について、言った人の社会的な責任だったり、信用出来ないといった側面は勿論ありますが、
「発言自体」は考える必要がある様に思いましたね。
「難易度があまりにも高過ぎて、誰が言ってもブーメランになる」様な内容もあるでしょうし。


13. GTO


まん喫は勿論、いわゆるスパでも、結構漫画コーナーというのがあるんですが、
そこで読んで、流石は名作と言われているなと。

【名言Best30】GTO 名言ランキング(ネタバレ注意) | GTOコミュ - funnel(ファンネル)

全部、買うべきかどうか・・・


14. 貞子


知名度が、高いんだか低いんだかって感じなんですが、
どうも、ネタキャラに成り下がった感はありますね(笑)
と思っていたら、おお小説での新作が出ていたのか。


15. バリデーションは、フロント側 or サーバー側?


って、目的が違うので、orで表現するのが違う感が。
フロントは「UI・UX的に」で、
サーバーは「仕様的に期待していないデータが入らない為」じゃあないかと。


16. トランザクション分離レベルは、セッション毎に変更可能?


なんだと・・・

PHP + PDO + MySQL のトランザクション制御方法まとめ | Webセキュリティの小部屋

あぁ、なるほど。
といっても、という気もするわけですが、これは以下の続きをしてみましょう(笑)

ダブルブッキングを防ぐ予約追加プログラムを考えた - 奇特なブログ


17. https://nikkan-spa.jp/1489186


シチュエーションは、好きな作品(ドラマ・映画・小説・音楽の詞・ゲーム・アニメ・漫画など)や、
リアルの散歩や旅行から、
「この場所やシチュエーションなら、どうか?」と考えてみるのが良いと思いますね。
あとは、自分のフェチを知っていると、ターゲットの絞り込みが早くなったりと、やっぱり有利。

ところで、セックスレスというと、少子化との相関が・・・あるような無いような。


18. 掘り出し物音楽


今後は、マイナーの割合が上がりそうですので。

https://www.amazon.co.jp/dp/B000002UHG

これより、スルメ作品あったかなって感じのレベルでしたけどね。
どうも、時々聴きたくなる。

https://www.amazon.co.jp/dp/B01CCBJQ4K

これは、レンタルしたので、まだ買ってないですけどね。
やっぱり、ベースに「歌謡」があるなぁ、この人は。

https://www.amazon.co.jp/dp/B00JMDV11I

これも、レンタル。
ツォーマス、やっぱり恐ろし過ぎますね。

ところで、AmazonAPIを使って、
「自分がカートに入れている商品の価格が、指定した価格以下になったらメールする」って、
出来ないかなぁと、ふと。


19. ジョギングとサイクリング


併用する事にしました。
しかし、どうやら予想以上に近所を知らなかった様です(笑)


20. アンチ?


と判断は安易にはしない様にしてますが、これは状況証拠が揃い過ぎな気もしますしね・・・
ところで、「弱者が格差拡大を望む」ってケースってあるんですかね。


21. すばらしい新世界


ピンク・フロイドのウォーターズ経由で知り、読みましたが、
現状に、もう少し感謝して生きた方が良いと思いましたね。

それにしても、10年ですか。長かった・・・

キリが良いので、これまでの現時点でのランキングを。


10:魔法少女プリティ☆ベルすばらしい新世界
9:PSYCHO-PASS サイコパス、B'z、ピンク・フロイド中島みゆき真・女神転生真・女神転生
8:ミリオンダラーベイビー、インタビューウィズヴァンパイア、Nightwish、Missing(ラノベ)、ファイアーエムブレム新・紋章の謎、ペルソナ2罰
7:ウィッチ(映画)、ダークナイト三部作、魔法少女まどか☆マギカKOKIAディシディア デュオデシム ファイナルファンタジー、428 ~封鎖された渋谷で~
6:ペルソナ2罪、シアトリズム ファイナルファンタジー カーテンコール
5:ルーム ROOM(映画)、ファイアーエムブレム新・暗黒竜と光の剣ストライク・ザ・ブラッド
4:
3:
2:
1:


ただ、こういう風に書くと、どうしても点数が低いのがパッとしなく見えるのが欠点ですね(笑)