OOPは、状態の読み取りと書き込みを暗黙的に行います。たとえば、Pythonでは:
class Foo:
def bar(self):
# This method may read and/or write any number of self.attributes.
# There is no way to know or limit what self state this method
# accesses and/or modifies.
に比べ:
def bar(qux, baz, flux):
# This function's only inputs are qux, baz, and flux. Its only output:
return trax
後者の方が、読みやすく、保守しやすく、テストし、理由を説明するのがはるかに簡単に思えます。
この問題の解決策はありますか? PythonやC++などの主流の言語でこの問題を解決する方法に特に興味がありますが、それを解決するツールや言語を指摘することも役立ちます。
それは正しくありません。
つまり、メソッドの入力と出力を明示的に宣言することは非常に優れているということです。実際に型を宣言する言語は、あなたのPythonの例よりも優れています。
しかし、OOが暗黙的であるというのは正しくありません。 OOの世界では、読み取りと書き込みはメソッドがオンになっているインスタンスに制限されます。そして、データであるためプライベートでは、状態の変更はそのクラスに限定されることがわかります。また、適切なOOはクラスを単一の責任に集中するように制約するため、メソッドからは見えない状態になる傾向がありますが、そのメソッドの依存関係ではありません。
これが実際にどのように機能するかについていくつかの質問がありますが、同じ質問が関数型プログラミングモデルや命令型プログラミングモデルにも当てはまります。
tl; dr-OO文字どおりに状態の範囲を制限することにより、状態変更の範囲を制限します。
OOPはカプセル化でこの特定の問題を軽減します。
メソッドを(外部から)呼び出す場合、どの内部属性が読み取られ、変更される可能性があるかがわかりません。しかし、OOあなたは知っておくべきではありません。
より一般的には、テストおよびテストの対象となる「ユニット」は、関数ではなくobjectです。したがって、内部属性は関数内のローカル変数のようなものです。関数を呼び出すときにそれらを気にする必要はなく、観察可能な動作と入出力のみを気にします。
オブジェクトをテストする場合、内部状態を検査してテストすることはありません。それは確かに面倒で壊れやすいでしょう。代わりに、パブリックインターフェイスを介してオブジェクトの動作をテストします。
foo
/bar
の例は本当に嫌いなので、より現実的な例を見てみましょう。順序付けされた辞書があるとしましょう:
let dict = OrderedDictionary()
dict.Add("car", "voiture");
dict.Add("horse", "cheval");
print dict["horse"] --> cheval
print dict[0] --> voiture
この辞書は、複数の方法で実装できます。キーと値のペアのリンクリスト、配列と組み合わせたハッシュテーブルなど。アイテムの数に基づいて戦略を変更することもできます。ポイントは、それが機能する限り、あなたは気にしないということです。
次に、すべてのパラメーターを明示的にする必要があるかどうかを検討します。
dict_h = HashTable()
dict_l = Array()
dict_Add(dict_h, dict_l, "car", "voiture")
dict_Add(dict_h, dict_l, "horse", "cheval")
print dict_by_key(dict_h, "horse") --> cheval
print dict_by_index(dict_l, 0) --> voiture
ここではそれが明示的です。 dict_by_key
はハッシュテーブルのみを使用し、配列は使用しません。しかし、この明示性の代償は非常に高くなります。複雑さと実装の詳細をクライアントにプッシュすると、プログラム全体に広がり、実装を変更することがはるかに困難でリスクを伴います。 (Hashtable自体が複数の属性で構成されることを気にしないでください)
関数型言語は通常、複数のフィールドを含む可能性のあるレコードタイプを使用してこれを解決します。しかし、その後、正方形の1つに戻ります。これらのフィールドのどれが、関数呼び出しによって読み取られたか、変更されたかが正確にわかりません。
現実の世界ではメソッドにbar
やその他の無意味な名前を付けることはできません(すべきではありません)ため、これは通常問題ではありませんが、オブジェクトが提供するサービスのセマンティクスを表現します。それがこのサービスを実装する方法は、クラスのユーザーとしてのあなたのビジネスではありません、それはキャッシュされた結果を保持する純粋な関数または複雑なアルゴリズムであるかもしれません、またはそれは外部サービスなどに委任するかもしれません。メソッドname()
は使用するたびに異なる値を返さないと想定することができますが、balance()
はdeposit()
およびwithdraw()
実行されました。
開発者の責任は、適切なセマンティクスと適切な名前を選択し(そしてできればそれらを簡単に理解できる形式で文書化すること)、ユーザーにとって厄介な驚きを避けることです。これは実際には非OOPプログラミングと大差ありません...
ステートフルなOOPの純粋な関数型プログラミングと純粋な関数型プログラミングが少し矛盾しているというのはあなたの言うとおりです。しかし、これを緩和する方法はいくつかあります:
1つのアプローチは、Immutableオブジェクトを使用し、状態を変更するすべてのメソッド呼び出しで新しいオブジェクトを返すことです。これはある程度まで機能し、浅いコピーを使用する場合でも、それほど効率的ではありませんが、実装がかなり面倒になります。
このアプローチの拡張は、「監視可能な状態」が変化した場合にのみ新しいオブジェクトを返すことです。それ以外の場合は、状態が変更された同じオブジェクトを返します。純粋な関数型言語では、キャッシングとローカル計算にかなり制限されますが、ステートフルOO言語でプラグマティズムを求める場合は、さらに多くの小刻みの余地を与えることができます。
たとえば、オブジェクトを作成して一連のメソッドで更新する「ビルダー」パターンを考えてみます。そのビルダーを最終結果以外の場所で使用していない場合でも、すべてのメソッド呼び出しでそのオブジェクトを変更できます。メソッド呼び出しの同じシーケンスを使用してビルダーを再作成することにより、クリーンな方法でテストします。明らかに、これはすべてのメソッド呼び出しが確定的であり、その下で変更される可能性のある外部データ(ランダム性やデータベースフェッチなど)に依存しない場合にのみ適用されます
ここでは決定論が有効な言葉です。全体として、ステートフルなOO言語から参照の透明性を絞り出すことは決してありませんが、メソッドが内部状態を更新する方法が決定論的である場合は、呼び出しのシーケンスを確実にテストし、それらの呼び出しをいくつでもユーティリティメソッドにまとめます。