The Dabsong Conshirtoe

技術系の話を主にします。

pythonの正規表現における先頭・末尾マッチ

こんなエントリーを見ました。

正規表現によるバリデーションでは ^ と $ ではなく \A と \z を使おう | 徳丸浩の日記

へー、知らなかったなー、pythonではどうなんだろうなー、と思って試してみたメモ。バージョンは3.3。

\z(小文字)ではなく\Z(大文字)

Rubyと同じように\zを指定してみたら思うように動かなかったのでなんでだろー、と思ってドキュメント見たら\Zでした。

>>> import re
>>> re.match('\Aabc\Z', 'abc')
<_sre.SRE_Match object at 0x7f12f3d371d0>
>>> re.match('\Aabc\Z', 'abc\ndef')

ちなみにRubyでは\Zと\zは分かれている模様。\Zは末尾の改行を無視して判定するみたい。

デフォルトは複数行モードではない

Rubyはデフォルトで複数行モードになっているから罠になるよ、という話でしたがpythonはそうではなかったので安心です。`re.MULTILINE`オプションをつけると複数行モードになります。

>>> re.match('^abc$', 'abc\ndef')
>>>
>>> re.match('^abc$', 'abc\ndef', re.MULTILINE)
<_sre.SRE_Match object at 0x7f12f3d37168>

末尾の改行の扱い

複数行モードでなくとも$は末尾の改行を無視してマッチするので注意が必要ですね。\zは末尾の改行は無視しません。

>>> re.match('^abc$', 'abc\n')
<_sre.SRE_Match object at 0x7f12f3d371d0>
>>> re.match('\Aabc\Z', 'abc\n')
>>>

javascriptでは・・・

どうなんだろうと思って調べてみましたがどうやら\Aや\zはない模様。(ちょこっと調べただけなので本当はあるのも。)

Haskell占い

あけましておめでとうございます。
去年はpython占いしたので今年はHaskell占いします。

-- ghciで実行(letとか省いてます)
import System.Random
choice :: [String] -> Int -> String
choice choices seed = (choices !!) . fst $ randomR (0, length choices - 1) $ mkStdGen seed
putStrLn $ choice ["大凶", "凶", "大吉", "中吉", "小吉", "吉", "マジ吉"] 2014
> 中吉

いいですね!
今年もよろしくお願いします。

cliffとその実装

python advent calendar 15日目の記事です。

以前から気になっていたcliffについて調べてみました。

ライブラリやフレームワークのコードを読むのが最近のマイブームなので、使い方とかは適当に流してcliffがどのような実装構成になっているか見てみたいと思います。

詳細が気になる方はドキュメントがちゃんとしてるので読んでみてください。 https://cliff.readthedocs.org/en/latest/demoapp.html

cliffってなに

pythonのコマンドラインアプリケーションフレームワーク。サブコマンドを登録したり引数を解析したりログを出したり、コマンドラインアプリに共通する部分を担ってくれます。

私は知りませんでしたが、gearboxというツールの実装に使われているそうです。

使用方法

標準出力になにか出すだけでの簡単なアプリケーションを作りながら、どのようにcliffを使うのか見てみましょう。

必要なもの

  • cliff.app.Appを継承したクラス

    このクラスのrunメソッドを実行することでアプリが起動されます。インスタンス化してrunメソッドを叩くための関数も用意しておきましょう。(main.py)

      # -*- coding: utf-8 -*-
    
      import sys
      from cliff.app import App
      from cliff.commandmanager import CommandManager
    
      class MyDemoApp(App):
          def __init__(self):
              super(MyDemoApp, self).__init__(
                  description     = 'my demo app',
                  version         = '0.1',
                  command_manager = CommandManager('myapp.commands'),
                  )
    
      def main(argv=sys.argv[1:]):
          # エントリーポイントなる関数
          myapp = MyDemoApp()
          return myapp.run(argv)
    
      if __name__ == '__main__':
          sys.exit(main(sys.argv[1:]))
    

    CommandManagerは各コマンドを管理するクラスで、コンストラクタ引数にはコマンドが登録されているエントリーポイントグループ名を渡します。

  • cliff.command.Commandを継承したクラス

    このクラスが各コマンドを定義するクラスとなります。例えば標準出力になにか出すだけのコマンドだったらこんな感じ。(say.py)

      # -*- coding: utf-8 -*-
    
      from cliff.command import Command
    
      class Say(Command):
          "A say command that prints a message."
          def take_action(self, parsed_args):
              self.app.stdout.write('hey!\n')
    
  • setup.py

    バッチプログラムをインストールするスクリプトです。

    詳細は省きますが、entry_pointsの定義にだけ触れておきます。

    console_scriptsにバッチの起動コマンドとなる関数を定義し、CommandManagerの初期化時に渡していたエントリーポイントグループに各コマンドのエントリーポイントを書きます。

          entry_points={
              'console_scripts': [
                  'mycliffapp = app.main:main'
              ],
              'myapp.commands': [
                  'say = app.say:Say',
              ],
          },
    

    これをpython setup.py installすることで、

      $ mycliffapp say
      hay!
    

    というコマンドを実行できるようになります。(setuptoolsは知らないとこが多い・・。)

だいたいの使い方はこんなとこです。

オプション

特にアプリケーション側で何もすることなく、ヘルプやdebugなどのよくあるオプションをサポートしてくれます。

usage: mycliffapp [--version] [-v] [--log-file LOG_FILE] [-q] [-h] [--debug]

my demo app

optional arguments:
  --version            show program's version number and exit
  -v, --verbose        Increase verbosity of output. Can be repeated.
  --log-file LOG_FILE  Specify a file to log output. Disabled by default.
  -q, --quiet          suppress output except warnings and errors
  -h, --help           show this help message and exit
  --debug              show tracebacks on errors

インタラクティブモード

引数なしで実行すると、インタラクティブモードに入ります。インタラクティブモードでは、コマンド名のみでコマンドを実行できたりします。

$ mycliffapp
(mycliffapp) say
hay!

この実装にはcmd2というライブラリが使われています。

実装

ではcliff本体の実装がどうなってるか見ていきます。

クラス構成

ざっくりとこんな感じです。

f:id:Attsun_1031:20131202220639p:plain

  • Appクラスがコマンドの流れを制御するクラス
  • CommandManagerクラスが登録されたサブコマンドをロードしたり検索したりするクラス
  • Commandクラスが各サブコマンドの動作を規定するクラス

cliffのユーザーはAppクラスとCommandoクラスを継承することになります。 簡単ですね。

※クラス図をastahで作ったんですが、pythonなひとたちはUML書くときに何使ってんでしょ?

それではひとつひとつ見て行きましょう。

cliff.app.App

アプリケーションのエントリーポイントとなるクラス。__init__ロケールの設定やArgumentParserの生成を行い、runメソッドによりアプリケーションを起動します。

runメソッドが重要なので、処理の流れを見ていきます。 なお、runは長いのでコードは貼りません。気になる人は以下を見てください。 https://github.com/dreamhost/cliff/blob/master/cliff/app.py

runメソッド

  1. ArgumentParser.parse_known_argsによりコマンドライン引数の解析を行う。--verbose--debugなどのオプションがデフォルトでセットされる。
  2. ロガーを生成する。
  3. 初期化後のhookポイントであるself.initialize_appメソッドを呼び出す。デフォルトでは何もしない。
  4. コマンド名が引数で渡されている場合、self.run_subcommandへ。コマンド名が渡されていない場合はインタラクティブモードに以降(詳細割愛)
  5. self.command_manager.find_commandによりコマンドをロード。
  6. コマンド実行前のhookポイントであるself.prepare_to_run_commandを実行。デフォルトでは何もしない。
  7. コマンドオブジェクトのget_parserによりコマンド固有引数解析用Parserを取得しパース。
  8. パースした結果を引数にコマンドオブジェクトのrunを実行。
  9. コマンド実行後のhookポイントであるself.clean_upを実行。デフォルトでは何もしない。このメソッドはコマンドで例外が発生した場合でも実行される。

hookポイントを提供することでアプリケーション固有処理をポイントポイントで入れられるようになっています。テンプレートメソッドパターンというやつですね。

cliff.command.Command

サブコマンドを表すクラスです。

run

Appクラスからコールされます。

コードはこんな感じ

def run(self, parsed_args):
    self.take_action(parsed_args)
    return 0

戻り値はexit codeとなり得るので(実装次第)、変更したい場合はオーバーライドする必要があるでしょう。

take_action

サブコマンドの実際の処理を書くところです。abc.abstractmethodになっているので、継承クラスでこのメソッドをオーバーライドする必要があります。

cliff.commandmanager.CommandManager

サブコマンドの管理をするクラスです。ロードしたり検索したり。 より役割をはっきりさせた名前にするなら、CommandRepositoryといったところでしょうか。

_load_commands

initの最後に_load_commandsというメソッドをコールしています。このメソッドは以下のような実装になっています。

def _load_commands(self):
    for ep in pkg_resources.iter_entry_points(self.namespace):
        LOG.debug('found command %r', ep.name)
        cmd_name = (ep.name.replace('_', ' ')
                    if self.convert_underscores
                    else ep.name)
        self.commands[cmd_name] = ep
    return

pkg_resourcesを使ってentry_pointを頼りにコマンドをロードしています。entry_pointへのコマンドの登録はsetup.pyで行います。

find_command

_load_commandsadd_commandで追加されたコマンドから、引数argv[0]に一致するコマンド名をもつコマンドを探します。

戻り値ではコマンドクラスを返しており、この段階ではインスタンス化は行いません。

コマンドクラスのインスタンス化はAppクラスがfind_commandを呼んでコマンドクラスを取得したあと、optionsを元にすぐさま行っているのですが、どういう考慮でしょうね。

コマンドの生成ロジックはCommandManagerに隠蔽するのが素直な気がするのですが。

感想

バッチフレームワークの設計にも応用できそうな気がしました。

あんまりバッチやコマンドラインアプリケーション系のフレームワークって使ったことないのですが、だいたいがこういう設計になってるのかな。

明日はdrillbitsさんです!

venusianライブラリの実装

デコレータの実行を遅延させる仕組み。デコレートされた関数のテストがしやすくなったりする。 メタ情報を付与するだけのアノテーションのようにデコレータを使うことができそう。

pyramidのview_config関数で使用されている。 http://docs.pylonsproject.org/projects/venusian/en/latest/

使い方

デコレータとなる関数の作成

vensuian.attachにより、あとでスキャンしたときに実行される関数が登録される。

# theframework.py

import venusian

def jsonify(wrapped):
    def callback(scanner, name, ob):
        def jsonified(request):
            result = wrapped(request)
            return json.dumps(result)
        scanner.registry.add(name, jsonified)
    venusian.attach(wrapped, callback)
    return wrapped

デコレータ関数を使用

# theapp.py

from theframework import jsonify

@jsonify
def logged_in(request):
    return {'result':'Logged in'}

この時点では、jsonify関数の実態(callback関数)はまだ呼ばれていない。

スキャンする

import venusian
import theapp

class Registry(object):
    def __init__(self):
       self.registered = []

    def add(self, name, ob):
       self.registered.append((name, ob))

registry = Registry()
scanner = venusian.Scanner(registry=registry)
scanner.scan(theapp)

この時点で、callback関数が呼ばれ、scanner.registryにjsonfiy関数の中で定義されたjsonified関数が登録される。 カテゴリごとにわけてscanすることなどもできる。(ドキュメント参照)

実装(ざっくり)

attach

スキャン対象としてオブジェクトを登録する関数。

パッケージ

venusian/init.py

実装

def attach(wrapped, callback, category=None, depth=1):
    """ Attach a callback to the wrapped object.  It will be found
    later during a scan.  This function returns an instance of the
    :class:`venusian.AttachInfo` class."""

wrappedの__venusian_callbacks__メンバに、callback関数を登録する。 callback関数のホルダーにはCategoriesクラスを使っている。

# カテゴリ名をキー、コールバック関数のリストを値として持つクラス。
class Categories(dict):
    def __init__(self, attached_to):
        super(dict, self).__init__()
        if attached_to is None:
            self.attached_id = None
        else:
            self.attached_id = id(attached_to)

    def attached_to(self, obj):
        if self.attached_id:
            return self.attached_id == id(obj)
        return True

流れ

  1. sys._getframeしてwrappedが関数なのかクラスなのかなどを判定
  2. wrappedの__venusian_callbacks__メンバに値が設定されていなければ、Categoriesインスタンスを新たに生成し、設定。
  3. Categoriesインスタンスに対して、引数categoryをキーにcallbackを登録する。複数登録できるようlistになっている。
  4. Categoriesインスタンスやwrappedの情報を含んだAttachInfoクラスを生成して返す。

Scanner.scan

attachにて登録されたオブジェクトをスキャンし、callback関数を実行する。

パッケージ

venusian/init.py

実装

class Scanner(object):
    ...
    def scan(self, package, categories=None, onerror=None, ignore=None):
        """ Scan a Python package and any of its subpackages.  All
        top-level objects will be considered; those marked with
        venusian callback attributes related to ``category`` will be
        processed.
        ...

attachされたオブジェクトに登録されたcallback関数を呼び出す。どれを対象とするかはcategoryやignoreで指定可能。

流れ

  1. packageに対してinspect.getmembersし、packageに所属するメンバーを取得。
  2. 各メンバーオブジェクトに対して内部関数invokeを適用。ここでcallbackが呼ばれる。

    python def invoke(mod_name, name, ob): """ mod_name: scan中のモジュール名 name: getmembersで得られたオブジェクト名 ob: nameに対応するオブジェクト """

    1. ignoreに指定されたオブジェクトでないかチェック。指定されたオブジェクトならreturn。
    2. __venusian_callbacks__メンバ(attachで登録されたCategoryインスタンス)があり、なおかつそのCategoryインスタンスが持つattached_toメンバがobと等しければ次に進む。
    3. callback呼び出しの対象となるcategoryを決める。scan関数のcategoriesパラメータが指定されていなければ、登録されているcategoryをすべて対象に含める。
    4. 各categoryに紐付いたcallback関数を取得し、実行する。
  3. packageが__path__を持つか検証。持っていなければreturn
  4. packageに対してwalk_package関数を呼び出し、そのパッケージに属するモジュールのモジュールローダーのイテレータを取得する。 ※この関数はpkgutil.walk_packagesとほぼ同じだが、ignoreを指定させたいので実装している。
  5. walk_packageの結果新しい未importのモジュールが見つかればimportし、そのモジュールに対してinspect.getmembers, invokeを行う。

感想

デコレートされたオブジェクトのメタ情報を結局はその関数自身にもたせている。(__venusian_callbacks__メンバ)

この辺は動的にメンバーを追加できる言語ならではか。言語として関数にメタ情報を含ませる仕組みがあるといいと思うのだけど。

[追記] python3のアノテーションの仕組みと共存できるようなやり方ができればいいかも。 func.annotationsに関数自身のメタ情報を持てるようにする、とか。 python3のアノテーションは引数と戻り値に関するメタ情報は持てるが、それ以外の情報(例えばjavaの@Deprecatedとか)は持てない。

ミクロな可読性とマクロな可読性

コードにおいて最も重要なことは何か?と問われれば、「可読性」だと答えます。
とにかく読みにくいコードは嫌だ。なんといっても醜いので生理的に不快。


では可読性とは何か?と考えてみたところ、2つのタイプがあるように思いましたので、ここではそれを書き留めておきたいと思います。

2つのタイプとは、意図の明快さを実現する「ミクロな可読性」と、構成の明快さを実現する「マクロな可読性」の2つです。

ミクロな可読性

ミクロな可読性とは、コードを読む人が以下の2要素をどの程度明快に理解できるかの指標です。

  • コードで達成しようとしている目的
  • 具体的にどのように目的を達成しているかの方法

前者は、クラスやメソッド名の命名やそのクラス・メソッドに関するドキュメントの適切さが重要で、後者はメソッドの中で使用される変数名やステップの明快さ、各ステップの抽象度レベルの一致が重要です。

目的を明快にする

まず、例を使って前者の説明をします。以下のコードを見てみましょう。

class P(object):
  t = 0.05

  def f(self):
    pt = self.pr * self.t
    return self.pr + pt

このコード何をしているかはわかるでしょう。self.prとself.tの積にself.prを足して返しています。
一方、この関数がどんな目的を持っているかはこれだけでは意味不明でしょう。
つまり、このコードは方法は(ある程度)明確だが、目的が全く不明、という状態です。


以下のようにリファクタリングしてみましょう。

class Product
  '''
  商品を扱うクラス
  '''
  # 消費税率
  sales_tax = 0.05

  def calculate_price_with_tax(self):
    '''
    商品の価格に消費税を上乗せして返却する。
    '''
    tax = self.price * self.sales_tax
    return product.price + tax

処理の流れには全く手を入れずに名前の変更とコメントの追加をすることで、「ある商品の税込価格を求める」という目的がはっきりしました。この方が、全く事情を知らない人が読んだとしても理解できるでしょう。

このように、コードの目的をはっきりさせるには名前のチョイスが非常に重要だと思います。
(それだけでは無いことには賛成しますが、ひとまず名前の重要性を強調しておきたいです)

※改善前の例が大袈裟だと思われるかもしれませんが、たまにあのようなコードを見ることがあるので世の中怖いものです。

手段を明快にする

次に後者の説明をします。次のコードを見てください。

def register_user(input_params, connection):
  name = input_params.get('name')
  password = input_params.get('password')
  if not (user and password):
    return None

  same_name_user_count = connection.execute_query('select count(*) as count from users where name = %s' % escape(name))['count']
  if sanme_name_user_count > 0:
    return None

  if len(password) < 8:
    return None

  user = User(name, password)
  user.update(connection)

  return user

命名がしっかりしているので、入力値を元にユーザーの登録をしようとしている、という目的はなんとなくわかるでしょう。
ただ、処理の流れが散乱している感じで追いづらいと思います。この程度のプログラムならまだしも、これが100行とか続いたりするとさすがにつらいですよね。

問題は、ユーザーを登録するというこのメソッドが、その登録の過程で行われる詳細に足を突っ込みすぎてるのです。


以下のようにしてみましょう。

def register_user(input_params, connection):
  # バリデーションを行う
  is_valid_request = input_params.validate()
  if not is_valid_request:
    return None

  # ユーザー登録を行う
  new_user = User.register_user(input_params, connection)
  return new_user

バリデーションの方法やユーザーの保存方法の詳細は別のメソッドに任せることで、パラメータのバリデーションを行なって、ユーザーの登録をする、というこの処理の主要な部分が目立っています。
また、コメントがその2つの要素があるということを更に引き立てています。
(input_params.validateにconnetionを渡していないのは、ユーザーの重複チェックをUser.register_userで行うようにしたからです。ユーザー名の唯一性は、入力値のチェックというよりは、Userというドメインが守るべきことだと思いますので。)

つまり、その処理の中身の抽象度を適切なレベルに保つことが、手段の明快さを実現する上で重要だということです。


なお、このミクロな可読性については「リーダブルコード」に詳しく書かれていますので読んでみてください。

リーダブルコード ―より良いコードを書くためのシンプルで実践的なテクニック (Theory in practice)

リーダブルコード ―より良いコードを書くためのシンプルで実践的なテクニック (Theory in practice)

マクロな可読性

ミクロな可読性が「コードの意図を明快にする」という目的を持っていたのに対し、マクロな可読性は「コードの構成を明快にする」という目的を持っています。


構成を明快にするとは例えば、パッケージ全体を見た時にどのパッケージが何をやってるかわかるとか、パッケージ内のソースファイル名をざっと眺めたときにどこで何をやってるかすぐにわかる、という性質です。
つまり、ここでも名前、それに加えて分類の適切さが重要になってきます。名前がはっきりしていても、一つのパッケージの中にビジネスロジックを実現するソースファイルと入力をハンドリングするソースファイルが混じっていたら混乱するでしょう。


今や当たり前となったMVCも、Model・View・Controllerの明快な分類を行なっているので、マクロな可読性の向上に貢献するという意味でも有効な方法だと思います。(Modelはちょっとざっくりしすぎなのでもう少し分類する必要があるとは思いますが)

この分類と命名がしっかりできていると、仕様変更やバグ調査を行うときにどこを見ればよいかわかりやすくなるので、変更のしやすさというソフトウェアにとって非常に重要な価値にもつながります。


以上、散髪中にふと思いつたいので書いてみました。

「オブジェクト指向でコードが書けるようになろう」に参加して来ました

オブジェクト指向でコードが書けるようになろう」に参加して来ました。

会を通して考えることがあったのでメモ。

設問

内容は、事前に出題されていたCodeIQの設問「クラス名を考えてみよう」について数人で議論しました。
設問は以下(いろいろはしょってます)。

あるWebサービスの会員登録の画面で、氏名の入力が必要です。
氏名は20文字以内である必要があります。
この入力チェックを実現するためのクラスを考えてください。

氏名をクラスにするか?

議論になったところは、氏名をUserNameのような形でクラス化するか、組み込みの文字列型として持つか、というところ。

ユーザー名について、クラス化した場合とそうでない場合について考えてみます。
(ここでいうクラスとは、フィールドに氏名を表す文字列型を持ち、20文字制約のロジックを実装しているクラス。クラス化しない場合は、XXXValidatorクラスを作って氏名を利用する側がバリデーションを行う。私は後者を選択しました。)

クラス化のメリット

クラス化するメリットは、

  • バリデーションロジックが複数箇所に出てきた場合でもクラスにバリデーションロジックが隠蔽されているので重複することはない。
  • ミドルネームやラストネームといった新たな仕様が氏名に持ち込まれた場合、対応しやすい。

という主張がありました。これは納得です。

クラス化しないメリット

私ももちろん、設計時にはこれらは想定しましたが、クラス化しないほうを選択した理由として、

  • 要求の単純さからして、わざわざ会員の1属性である氏名のようなものをクラス化するのは冗長だし複雑性を持ち込む。
  • 実際にロジックが複雑になったり重複が発生したら、その時ベストな対応をしたら良い。

があります。

「柔軟性は必要な箇所のみ」という原則はコードをシンプルに保つ上で大事だと思っているので、クラス化しないほうが後々保守する際にも良いだろうと考えました。

再考

が、よくよく考えてみると、議論のあとの増田さんのフィードバックでもあったとおり、
「必要になったら柔軟性を持ち込む」
という選択は1つの有効な選択ではありますが、いざ柔軟性が必要になったタイミングが出てきても、時間の都合やそのときに実装を行うエンジニアの能力次第では対応できなっかたりする、というのが私のこれまでの実践感覚です。

また、設計の一貫性が崩れる(他のフィールドはバリデータクラスでバリデートしてるのに、ユーザー名だけユーザー名クラスでバリデートすることになっちゃう)という問題もあると思いました。

増田さんのスタイルは、どうせ必要になった時にちゃんと対応できないんだったら最初から用意しておけ、だそうです。
ただ、だからといってじゃあフィールド1個1個をクラスにしてしまって複雑になったり可読性が落ちてしまったりしないかという不安もあります。(やったことないのでどうなるかわからない)


まぁこの辺のバランス感覚はコンテキストによって変わってきそうですが、増田さんのフィードバックや自分のこれまでの経験を踏まえると、「そのデータ対して1つでもロジックが入った場合はクラス化する」という方針がいいのかな、と思いつつあるところです。

追記

社内でこの話をしたら、「UserNameでバリデーションする場合、バリデーションメッセージどうするの?」という疑問があがりました。

どうしましょうかね?

例外方式

UserNameがプレゼン層にいるオブジェクトならまだしも、ここでは(多分)ドメイン層にいる前提なので、画面表示用のメッセージ返すわけにはいきません。

思いついたのは、エラーコードとパラメーター(ここでは20という数字)を情報に含んだ例外を飛ばして、プレゼン側でエラーコードから適切なバリデーションメッセージ(「氏名は{max_length}文字以内で入力してください」とか)を取得し、パラメータでプレースホルダーになっているところを埋める、という手です。

プレゼンの変更にモデルが影響を受けてしまう

上記の例外方式を採用したとしましょう。
ここで、例えばパスワードのチェックで以下の様な仕様があるとしましょう。

パスワードは半角英数字と記号の組み合わせ。
違反した場合は画面に「半角英数字と記号の組み合わせを指定してください」と表示する。


この仕様をパスワード文字列とバリデーションロジックを持つPasswordクラスとして実装しました。違反した場合は、英数と記号のどちらかが欠けていることを表すエラーコードを情報として保持した例外を投げます。

ところが実装後、以下の様な仕様変更がありました。

英数字を含んでいなかった場合は「半角英数字が含まれていません。」というメッセージを出し、記号がない場合は「記号が含まれていません。」というメッセージを出す。


この場合、Passwordクラス内のバリデーションロジックで記号が含まれていない場合と英数字が含まれていない場合を別々に判定しなければいけなくなるので修正が入ります。
つまり、画面メッセージの変更がモデルに影響しているわけで、これはちょっと頂けないなと感じます。

プレゼンのバリデーションとモデルのバリデーション

ここまで書いてて、プレゼンでのバリデーションと、モデルでのバリデーションを同じものとして考えないほうがいいんではないか、という気がしてきました。

つまり、モデル層のPasswordとは別に、プレゼン層にPasswordInputというクラスを定義し、そこに入力されたパスワードとバリデーションロジックを実装する、という形式です。


ただ、バリデーションロジックが重複してしまう気がする。うーん。

何をオブジェクト化しないか

設計において何をオブジェクト化するか、について考えることがあったのでメモ。

何をオブジェクト化するか

通常、設計においてはソフトウェアの仕様から「何をオブジェクト化するか」を考えます。「オブジェクト化する」とは、ソフトウェアの主要な関心事に名前をつけ、責務を分配し、他のオブジェクトとの協調関係を定義する行為です。
もしここで、アプリケーションにおいては重要であるにも関わらず無視される関心事が出てくると、その関心事は名前をつけられたり明確な責務を定義されることもなく、ほかのコードに埋もれます。これにより、

  • 変更容易性の低下
  • 可読性の低下
  • テスト容易性の低下

を引き起こします。つまり、保守性という面での品質が低下するわけです。
重要な関心事は変更頻度が高いことが多いので、保守性の低下は禍根を残します。
なので、重要な関心事はきっちりとオブジェクト化することが大事です。

何をオブジェクト化しないか

しかし、過度なオブジェクト化は逆に可読性を下げます。
例えば、名前を表すnameフィールドを持ったUserクラスを考えてみます。このnameをNameクラスのオブジェクトとして持つと、Nameクラスの中身を気にする必要が出てきてしまうことで可読性が多少下がります。標準の文字列型で持ったほうがより直接的でわかりやすいでしょう。

一方でオブジェクト化をしない場合、その概念に関する柔軟性を犠牲にしていることになります。
例えばUserクラスにログインに関するロジックをもたせている場合、ログインロジックの変更にはUserクラスの変更が必要になります。ログインオブジェクトとして切り出せばログインロジックの修正でUserに手を加えることはなくなりますし、そこにStrategyパターンを適用すればユーザーごとにログインロジックを変えるといったことも可能になります。
つまり、可読性と柔軟性の間でトレードオフが生じているわけです。

このバランスは、柔軟性をどこにどれだけ提供したいか、どの概念をコードの中で際立たせたいかによります。

このように、すべてをオブジェクトとして意識しつつ、可読性と柔軟性のバランスを考慮して最適な粒度のオブジェクトにまとめることを考えることが大事だと思いました。