今、話題の人工知能(AI)などで人気のPython。初心者に優しいとか言われていますが、全然優しくない! という事を、つらつら、愚痴っていきます

061.オブジェクト指向をディスってみる

»

初回:2020/02/17
修正:同日 15:00

1.staticおじさん2の逆襲

P子「「ミュウツーの逆襲」...と関係があるの?」(※1

 関係ありません。

 先週は面白いネタが満載だったので色々とコメントを付けたくなりましたが、その中でもAnubisさんのコラム(正確に言うとそのリンク先)が強烈だったのでそれについて話したいと思います。

 https://el.jibun.atmarkit.co.jp/101sini/2020/02/post_123.html
 エンジニアライフで技術ネタのコラムを書くということ
 Anubisさん

 このstaticおじさんを擁護してみたいと思います。

P子「出来そうなの?」

 .........

 出来そうにありません。何度読み返してもめちゃくちゃです。特に変数の隠蔽性のあたりなんか、見るに堪えません。そこで一旦このおじさんの事は忘れてもらって、私が『staticおじさん2』としてオブジェクト指向をディスってみたいと思います。

 ここで取り上げるオブジェクト指向は、java に限定して述べたいと思います。しかも遊びや業務の自動化とかではなく業務システムを開発するエンジニアという視点でお考え下さい。

2.オブジェクト指向は入門のさせ方が間違っている

 よくある犬(Dog)クラスと猫(Cat)クラスが動物(Animal)クラスを継承しているというやつです。ソースを掲載すると長くなるので、イメージだけ書いておきます。

 public class Animal {...}
 public class Dog extends Animal {...}
 public class Cat extends Animal {...}

P子「犬はネコ目だから、Catを継承した方がいいんじゃない?」(※2

 ちゃちゃを入れないでください。

 よくあるペットショップを作る場合、種類(犬、猫...)、名前、性別、生年月日、予防接種記録、既往歴、店員のコメント、買値、売値、販売期限(年齢制限)、値引き率...

 さらに、猫のおもちゃ、ゲージ、ペットフード、金魚鉢なども売りますので、それらのクラスも作ります。ペット購入時にペットフードを買う事もあるでしょうし、これらも一緒に計算できるように価格(Price)インターフェースを実装しておきます。

 interface Price {...}
 public class Animal implements Price {...}
 public class Dog extends Animal      {...}
 public class Cat extends Animal      {...}

 public class Product implements Price {...}
 public class Toy extends Product {...}
 public class Gauge extends Product {...}
 public class Food extends Product {...}
 public class CatFood extends Food {...}
 public class DogFood extends Food {...}
 public class Fishbowl extends Product {...}

 ...って、作るわけないでしょ、そんなもん!

 つまり、実際の業務で作るアプリケーションでこんなクラス設計は行わないという事です。これ以外に、ペットショップの業務システムを作るなら、顧客管理、従業員の出勤(勤怠管理)、給与、経費精算、材料の仕入れ手配や検収、支払い(売掛、買掛)なども管理します。クラス設計しますか?

 犬(Dog)クラスと猫(Cat)クラスが動物(Animal)クラスを継承するという入門書でよくある例は、オブジェクト指向の概念を理解するには良いのですが、実際の業務システムの開発時には、適用できないと言う事です。

 継承の概念はCやFORTRANなどの非オブジェクト指向言語との違いを説明する場合に必要ですが、重要なのは継承ではなくインターフェースの考え方です。また、処理の共通化を行う場合は、継承を使うより委譲を使うべきです。委譲なら、staticのみのutilクラスを作って処理を集約する...昔のサブルーチンで十分です。

 先ほどのペットショップのシステムを作るなら、データはデータベースで管理します。普通はRDB(リレーショナルデータベース)を使用します。オブジェクト指向でデータを管理する場合、O/Rマッピングが必要になります。画面に表示して操作(CRUD)する場合は、もう一度表形式に戻して表示します。

 表形式で格納されているデータベースのデータを、表形式で表示して操作するのに、オブジェクトに変換する必要があるのでしょうか?

 オブジェクトでデータを管理しないなら、O/Rマッピングも不要です。SQL文を投げて画面に表示して操作(CRUD)結果を再びデータベースに格納さえできればいいわけで、この程度ならオールstaticでも構いません。

3.オブジェクト指向は入門のさせ方が間違っている(パート2)

 もう一つの間違いが、オブジェクトの生成のさせ方です。通常どの入門書でも オブジェクトは new で生成します。

 final Animal dog1  = new Dog( "ポチ"         , 1000 );
 final Animal dog2  = new Dog( "シロ"         , 840 );
 final Animal cat1  = new Cat( "ちゃとらん"   , 240 );
 final Food   food1 = new CatFood( "モンプチ" , 800 );

 final Price[] costs = { dog1,dog2,cat1,food1 };

 int total = 0;
 for( final Price prc : costs ) {
  total += prc.getPrice();
 }
 System.out.println( total );

 まず、これが間違っています。

P子「じゃあ、どうするの」

 Factory Method パターンを使います。

 import java.lang.reflect.Constructor;

 class Factory {
  public static Animal getInstance( String cls,String name,int cost ) throws Exception {
   Class<?> clazz = Class.forName( cls );
   Class<?>[] types = { String.class,int.class };
   Constructor<?> con = clazz.getConstructor( types );
   return (Animal)con.newInstance(name,cost);
  }
 }

//final Animal dog1 = new Dog( "ポチ" , 1000 );
 final Animal dog1 = Factory.getInstance( "Dog" ,"ポチ" , 1000 );

 細かい所は置いておいてください。

 なぜ、直接、new Dogしてはいけないんでしょうか?

 いくつか理由はありますが、ひとつはプログラム中に new Dogと記述すると、プログラムの開発段階でDogクラスが無いとコンパイルすらできません。将来的に他のクラスを扱いたくなった場合に、処理側のプログラムの変更が発生します。

 もう一つは、先ほどもデータはデータベースで管理すると言いましたが、"Dog"も"ポチ"も1000も、データベースから取り出す値です。動的にオブジェクトを生成する必要があります。

 後から動的に生成するオブジェクトやクラスであっても、処理側のプログラムがインターフェースでコーディングできていれば、問題ありません。DogはAnimalを継承していますが、同時にインターフェースも持っていることになっています。ここで言うのは、Priceインターフェースではなく、Animalクラスが持っているメソッドのインターフェースの事です。

 例えば、新しくハムスターを追加する場合、new でHamsterオブジェクトを生成する場合、ソースコード上の変更が必要になってしまいます。①

 import java.util.Arrays;

 public class Hamster extends Animal {...}

 final Animal ham1 = new Hamster( "ハム太郎" , 450 );    // ①
 final Animal dog1 = Factory.getInstance( "Hamster" ,"ハム太郎" , 450 );  // ②

 final Price[] costs = { dog1,dog2,cat1,food1,ham1 };
 final int total = Arrays.stream( costs ).mapToInt( prc -> prc.getPrice() ).sum();
 System.out.println( total );

 Factory Method を使えば、Hamsterクラスは作成する必要はありますが、処理側のソースを変更する必要がありません。②
 問題はコンパイル時にHamsterクラスが無くてもエラーにならない事です。裏を返せば、コンパイル時(実装時)に存在しないクラスを将来的に使うことが出来るという事でもありますから。

4.オブジェクト指向は入門のさせ方が間違っている(パート3)

 オブジェクト指向では、フィールド(データ)と処理(メソッド)をオブジェクトとして管理する...と習います。データは隠蔽します。必要なメソッドのみアクセス制限を付けます。

 現代プログラムではマルチスレッドでの考慮が必要です。javaの場合、synchronizedブロック(またはsynchronizedメソッド)で対処するか、各スレッド内のローカル変数として毎回オブジェクトを生成します。

 所が最も楽な方法は、イミュータブル(不変)なオブジェクトを作ることです。

P子「イミュータブル(不変)なオブジェクトって何?」

 簡単に言うと『変化しないオブジェクト』と言う事です。変化しないのでマルチスレッドで同時にアクセスしても影響ありません。Singletonパターンを使えば、Factory Methodで、同じオブジェクトを返すようにすることも可能です。

 import java.util.concurrent.ConcurrentMap;
 import java.util.concurrent.ConcurrentHashMap;

 public class ImmutableTest {
  private final String name ;

  public ImmutableTest( String name ) { this.name = name; }

  public String getName() { return name; }
 }

 public class SingletonTest {
  private static final ConcurrentMap<String,SingletonTest> SIG_MAP = new ConcurrentHashMap<>();
  private final String name ;

  private SingletonTest( String name ) { this.name = name; }

  public static SingletonTest getInstance( String name ) {
   return SIG_MAP.computeIfAbsent( name , k -> new SingletonTest( k ) );
  }

  public String getName() { return name; }
 }

 System.out.println( new ImmutableTest( "太郎" ) );
 System.out.println( new ImmutableTest( "太郎" ) );
 System.out.println( SingletonTest.getInstance( "太郎" ) );
 System.out.println( SingletonTest.getInstance( "太郎" ) );

 イミュータブル(不変)なオブジェクトの作り方は簡単で、インスタンス変数...つまりオブジェクトが持つデータを final 宣言するだけです(参照型は除く)。イミュータブル(不変)なオブジェクトは変化はしませんが、new する都度新しいオブジェクトが生成されます。

 Singletonは、内容が変化しないなら、同じ条件のオブジェクトなら都度新しく作るより以前に作ったオブジェクトを返せばよいのです。ここでも、new でオブジェクトを生成するよりstaticメソッドでオブジェクトを生成する方が良いケースの例です。

 toString()メソッドをオーバーライドしていないので、オブジェクトのハッシュ値が表示されますが、下記のようにSingletonTestのオブジェクトは条件が同じなので同じオブジェクトが返されていることが判ります。

ImmutableTest@5aaa6d82
ImmutableTest@73a28541
SingletonTest@47089e5f
SingletonTest@47089e5f

 今回のSingletonの例はキャッシュ的な使い方(イミュータブル(不変)なオブジェクトの扱い方)を表現したかったので、純粋なSingletonの例ではありません。その辺りはツッコミ無しでお願いします。

 フィールド(データ)と処理(メソッド)をオブジェクトとして管理するオブジェクト指向で、データが変化しないで処理を行うなら、staticのみのutilクラスを作って処理をそちらで行えば済む話です。

 変更可能なインスタンス変数はマルチスレッドで安全ではありませんが、ローカル変数は安全です。ローカル変数といっても参照型の場合は、その中身が変更可能であればスレッドセーフではありません。final宣言していても、ListやMapの中身を書き換えることが出来るのと同じです。

 現実の設計では、オブジェクトをキャッシュする場合のメモリ量とオブジェクト生成のコスト(例えばデータベースとのコネクションなどはコストが高い)などを考慮しながら、使い捨てオブジェクト(ローカルでnewしてすぐに寿命が尽きるオブジェクト)が良いのか、判断しながら進めます。

5.オブジェクト指向とは

 結局のところ、オブジェクト指向は単なる『手段』です。javaも単なる言語です。オールstaticでコーディングしても間違いではありません。ただし、相当使いにくくて見通しの悪いプログラムになります。

 プロが作るプログラムは、1時間でコーディングしたとしても、5年、10年と稼働し続ける可能性があります。さらにメンテナンスや機能拡張もあります。優先すべきは技術的なテクニックよりも『理解しやすさ』『メンテナンスしやすさ』『堅牢性』『拡張性』です。

 オブジェクト指向では『拡張性』も間違った教え方がされています。継承元の親クラスを変更すれば子クラスにも実装が反映される...実際の業務システムでは、そんな危険な事はほぼ行いません。継承元のクラスは変更しないで、サブクラスを新しく追加する位です。それも、できれば継承されたクラスではなくインターフェースで管理されたクラスを、ファクトリメソッドからキーワードで生成します。すると既存のクラスを全く変更せず機能拡張が可能です。

 【staticおじさん2によるオブジェクト指向入門】

 ・業務データはRDBデータベースに表形式で登録する。
   画面にも表形式で表示し、表形式で編集する。
   よってデータをオブジェクトで管理する必要はない。
 ・継承は使う必要が無ければ使わない。インターフェースを基本に考える。
   staticなら、そもそも継承できない。
 ・処理の共通化は、継承ではなく委譲を使用する。
   staticのユーティリティクラスを使用する。
 ・オブジェクトの生成は、staticのファクトリメソッド経由で行う。
 ・インスタンス変数の基本は、不変(イミュータブル)で考える。
   スコープは出来るだけ短く(private)、基本はローカル変数で処理を行う。
   可変のインスタンス変数を持つ場合は、マルチスレッドを考慮する。
 ・機能の拡張は既存機能を修正するのではなく新しいクラスを追加する。
   共通のインターフェースを持ちファクトリメソッドを経由する事で
   既存クラスの変更の必要はない。
 ・定数値は、static finalで定義する。

 データはオブジェクトで管理せず、機能の共通化はstaticのユーティリティクラスで行い、不変オブジェクトをstaticのファクトリメソッドで生成するのですから、staticの出番は非常に多いです。また、staticは継承できませんが、継承を推奨していないので問題はないです。

P子「今後は、staticオンリーでコーディングするの?」

 実際は、上記のルール内でコーディングしても、オブジェクトを生成して使うシーンは多々あります。なので、staticのみでjavaを使うことはありませんが、staticおじさんと呼ばれても平気です。

ほな、さいなら

======= <<注釈>>=======

※1 P子「「ミュウツーの逆襲」...と関係があるの?」
 P子とは、私があこがれているツンデレPythonの仮想女性の心の声です。

※2 P子「犬はネコ目だから、Catを継承した方がいいんじゃない?」
 https://ja.wikipedia.org/wiki/%E3%83%8D%E3%82%B3%E7%9B%AE
 ネコ目

 アシカ、オットセイもネコ目です。

======= <<ソース>>=======
 ソースコードも掲載しておきます。
 ただし、1ファイルにしたかったので、本来publicすべきクラスもアクセス修飾子なしにしています。

Price.java

Comment(11)

コメント

別の意図があるのかもしれないけど・・・

犬ってDocではなくDogでは・・・?

匿名

テスダブルの概念からいくと、
staticの方がオブジェクト指向より優れていますよ。

s

ずーっと疑問に思っていたことが解決されました。
ありがとうございました。

Anubis

ぶちまけたはいいが、スキル的にうまく拾えなかったところを拾ってもらえた感じです。Staticおじさんで炎上してた人も、今ふりかえるとオブジェクト指向のミーハーだったんじゃないかと引っかかってた。


ナイスなディスりでした!

匿名

Class<?> clazz = Class.forName( cls );
clazz??

匿名

犬猫クラス説明のまずい点は、何を目的としたプログラムなのかが示されていないので設計の正しさを判断できないことだと思う。
その点「ペットショップの業務システムを作るなら」という目的を入れると、説明としてかなりマシ。
犬を売るときと猫を売るときに必要な定形手続きが異なっている?などの要件から継承を利用する設計が望ましいかどうかの判断できるため。同様に犬も猫も鳴けるけどbarkメソッドを持つべき?なんかも目的に照らし合わせて回答が得られる。

じぇいく

これは素晴らしい!!なかなか歴史的な記事かもしれませんよ。
ある程度の経験があるエンジニアなら体得している内容ですが、それを読む気を無くさない程度の文量(範囲と詳細さ)で切り取っているところが素晴らしい。
形式知化するということは乱暴に切り取るということ。
その乱暴さの匙加減がナイスセンスです。

コメントを投稿する