Open17

「コーディングを支える技術」メモ

haseyuyhaseyuy

言語によって違う「オブジェクト指向」の意味

  • C++では、「classはユーザ定義型を作るための仕組み」
  • Stimulaでは、「オブジェクト指向プログラミングとはユーザ定義型と継承を使ったプログラミングのことだ」
  • Smalltalkでは、「状態を持ったオブジェクトメッセージを送り合うことでコミュニケーションする」というモデルでプログラムを実現すること
haseyuyhaseyuy

関連性の強い関数や変数のまとまりを明示するために、モジュールという概念が導入された。
PythonやRubyはそのまま「モジュール」、JavaやPerlでは「パッケージ」という名前で呼ばれる

モジュールは「まとめる手法」→これを使って変数や関数をまとめて「現実世界のモノ」の模型が作れるのではないか、という考えになる

haseyuyhaseyuy

ファーストクラス、第一級

以下の性質を持つプログラミング言語のこと

  • 変数に代入する
  • 関数の引数として渡す
  • 関数の戻り値として返す
  • データ構造に組み込むことができる
haseyuyhaseyuy

共有してよいモノをプロトタイプに移す

var Counter = function() {
    this.count = 0;
}

// Counterのプロトタイプにpushという名前で新しい名前を追加
Counter.prototype.push = function(){
    this.count++;
    console.log(this.count + "匹");
}

var c1 = new Counter();
c1.push(); // => 1匹
c1.push(); // => 2匹

var c2 = new Counter()
// true: push関数が共有できている
console.log(c1.push === c2.push) // => true
haseyuyhaseyuy

Hoareの考えていたクラス

  • 「現実世界のモノ(objects)は、しばしば便宜上、いくつかの相互排除的な種類(classes)に分類される」
  • 「ある種類のものをさらに細かな種類(subclasses)に分類することもできると便利だ」

⇨ つまり「分類」の意味で考えられていた

haseyuyhaseyuy

C++のクラス

  • intやfloatなどの組み込み型と同じように扱える、新しい型をユーザが定義できるようする考え
  • クラス(=型)は仕様の表明、つまり「オブジェクトがどういうメソッドを持っていて、どういうメソッドを持っていないか」を仕様を宣言する役割

クラスはタイプである。これが、C++のきわめて重要な考え方だ。
C++ではclassがユーザ定義のタイプを意味するなら、なぜそれをtypeと呼ばないのか?

haseyuyhaseyuy

クラスが持つ3つの役割

  1. まとまったものを作る生成器(new演算子)
  2. どういう操作が可能かという仕様(インターフェース)
  3. コードを再利用する単位(継承)
haseyuyhaseyuy

継承とは

  • 継承に対する考え方は大きく分けて3通りある

一般化/特殊化

  • 親クラスで一般的な機能を実装し、子クラスで目的に特化した機能を実装する
  • 子クラスは親クラスの特殊化

共通部分の抽出

  • 複数クラスの共通部分をその親クラスとして抽出する

差分実装

  • 継承して変更点だけ実装

継承は諸刃の剣

  • 継承は異なった使い方があり、自由度が高いため、むやみやたらに使うとわかりにくい
  • 継承が深くなると、継承関係をたどってたくさんのソースを確認しなければいけない
  • あるメソッドを書き換えると、その影響は子クラス全てに及ぶ

継承を使ってコードを再利用すると、書く量は減って楽だが、コードの影響範囲が広くなり、理解が難しくなる

haseyuyhaseyuy

多重継承

現実世界で一つのモノが複数の分類に属することがあるのだから、それをモデル化する道具であるプログラミング言語は複数のクラスからの継承をサポートするべきでないか?という考え方

多重継承の問題点

  • 複数の親クラスで同じ名前のメソッドを持っていた場合、名前解決の問題が発生する
  • Javaはクラスの多重継承を禁止している

解決策1: 委譲(delegation)

  • 代わりに、使いたい実装を持っているクラスのオブジェクトを作って、必要に応じてそのオブジェクトに処理を頼む、という方法
  • 単一責任の原則を満たして保守性を上げることができる
  • JavaScriptでは言語として委譲が強力にサポートされており、そのプロトタイプチェーンを用いて暗黙の委譲が行われる

解決策2: Method Resolution Orderの工夫

  • どういう順番で探索するか明確に定義すれば良い、という考え方
  • 単純な深さ優先探索の場合、菱形継承を解決できない

C3線形化(C3 Linearization )で順序を決める

  • 親クラスは子クラスより先に探索されない
  • あるクラスが複数の親クラスを継承している場合は先に書いてあるものが優先される
  • Python2.3(2003年)からは、このアルゴリズムが使われている

解決策3: 処理を混ぜ込む(Mix-in)

  • 祖先クラスへ辿り着く方法が複数あるのが問題なので、再利用したい機能だけを持った小さなクラスを作って、その機能を追加したい大きなクラスに混ぜ込めば良い、という考え方
  • 混ぜ込むための小さなクラスを「ミックスイン(Mix-in)」と呼ぶ
  • 他のクラスで使用するためのメソッドを含むクラスで、継承されることを前提に作られる
// The type Constructor<T> is known as the construct signature. 
// It describes the type as one that can be used to construct objects of the generic type T, and T defaults to {}.
// new (...args: any[]) tells us that the constructor function of this type accepts an arbitrary number of parameters of any type.
// Note that the name of the type could have been anything but Constructor is the most suitable name.
type Constructor<T = {}> = new (...args: any[]) => T;

// クラスを拡張して返す
function Animalled<TBase extends Constructor>(Base: TBase) {
    return class extends Base {
        move(distanceInMeters: number = 0) {
            console.log(`Animal moved ${distanceInMeters}m.`);
        }
    }
}
function Mortalled<TBase extends Constructor>(Base: TBase) {
    return class extends Base {
        confess(distanceInMeters: number = 0) {
            console.log("I seek immortality!");
        }
    }
}
class Dog {
    bark() {
        console.log("Woof!");
    }
}
const AnimalledDog = Animalled(Dog);
const MortalledAndAnimalledDog = Mortalled(AnimalledDog);
let dog = new MortalledAndAnimalledDog();
dog.bark(); // “Woof!” 
dog.move(10); // “Animal moved 10m.”
dog.confess(); // "I seek immortality!"
haseyuyhaseyuy

リスコフの置換原則(Liskov substitution principle)

T型のオブジェクトxに関してある属性q(x)が常に真であるとする。そのSがTの派生型であれ>ば、S型オブジェクトyの属性q(y)が常に真でなければならない

つまり「あるクラスTのオブジェクトについて必ず成り立つ性質があるなら、その条件は子クラスSのオブジェクトについても必ず成り立たなければいけない」

  • SOLIDの原則のL
  • 「[継承]を使うべきタイミング」の指針を提供する原則
haseyuyhaseyuy

オープンクローズドの原則(Open/closed principle)

ソフトウェアの構成要素(クラス、モジュール、関数など)は拡張に対しては開いて(オープン:Open)いて、修正に対して閉じて(クローズド:Closed)いなけばならない

つまり、ソフトウェアの振る舞いは既存の成果物を変更せず、新たにコードを追加するだけで対応できるようにするべき、ということ

違反例

  • 以下のコードは違反例(猫と犬以外に新たに動物を追加したいとき、AnimalSound関数に対して条件分岐を追加しなければいけない)

  • if・switchでの条件分岐はやりがちだが、AnimalSound部分は既存コードの重要なロジック部分で、
    既存コードに修正は加えず というのが守れていない

class Animal {
    animalName: string
    constructor(name: string) {
        this.animalName = name
    }
}

const AnimalSound = (animal: Animal[]) => {
    for (let i = 0; i < animal.length; i++) {
        if (animal[i].animalName === 'cat') console.log('meow')
        if (animal[i].animalName === 'dog') console.log('bowwow')
    }
}

const animals: Animal[] = [new Animal('cat'), new Animal('dog')]

AnimalSound(animals) // meow bowwow

改善例

  • 各動物に対するクラスを作成し、インターフェースを実装します。新しい動物を追加するときは、新しいクラスを作成するだけになり、既存のAnimalSound関数に手を入れなくてよい

  • これによって各種Animalを個別に実装する事が可能になり(Open)、AnimalSound()の修正が影響をうけることがなくなりました。(Closed)

interface IAnimal {
   makeSound(): string
}

class Cat implements IAnimal {
   makeSound = () => 'meow'

}

class Dog implements IAnimal {
    makeSound = () => 'bowwow'
}

const AnimalSound = (animals: IAnimal[]) => {
    for (let i = 0; i < animals.length; i++) {
        console.log(animals[i].makeSound())
    }
}

const animals = [new Cat(), new Dog()]

AnimalSound(animals)
haseyuyhaseyuy

型のいろいろな展開

ユーザ定義型とオブジェクト指向

言語が用意している基本的な型を組み合わせて、新しい型を作る機能が発明された。C言語の構造体等。ユーザ定義型と呼ばれる。

/* 整数型と文字列型を組み合わせて新しいperson型を作る */
struct persion {
   int age;
   char *name;
};

仕様としての型

公開と非公開を分ける

  • 型を全部公開するのでなく、最小限だけを公開する、ということが行われるようになる
  • いわゆるpublicprivateなどのアクセス制御のこと

インターフェースへの発展

  • 「型は仕様である」という考え方をさらに推し進めて、具体的な実装を持たない型も生まれる
  • いわゆるinterfaceのこと
  • 以下のコードは「引数を取らず、値を返さない(void)、runという名前のメソッドを持っている」という仕様をinterfaceで定義している
package java.lang;
public interface Runnable {
  public abstract void run();
}

総称型、ジェネリクス、テンプレート

  • いろいろな型を組み合わせて作った複雑な型が使われるようになって、「その一部だけ変えたいのに全部定義しなおすのはおかしい、再利用したい」というニーズが生まれる
  • 「構成要素の型の一部が変わる型」つまり総称型が生まれました、別の表現をするなら「型を引数にとって型を作る関数」
haseyuyhaseyuy

動的型付けのスクリプト言語の一種であるPythonでは、変数に型の宣言が必要ない
同じ変数に整数を入れたり浮動小数点数を入れることもできる

x = 1234
x = 3.1415

メモリ上で同じ型として扱えるように設計されている
Pythonの世界では値は、整数でも浮動小数点数でも文字列でも、すべてPyObject型として扱えるように、同じになっている

メリット

柔軟に書ける

デメリット

型チェックがないので、実行する前にバグに気づくことができなくなる

haseyuyhaseyuy

型推論

コンパイル時の型チェックは捨てずに、でも面倒な型宣言を減らしたいを達成するために、
コンピュータが型を推論するというアプローチ

haseyuyhaseyuy

短絡評価/ショートサーキット

  • 左辺を評価した時点で論理式の結果が確定した場合は右辺の評価を行わないこと
  • A and Bという論理式があった場合、Afalseならその時点で式全体の結果はfalseで確定する為、Bがどうであるかについてはチェックしない
  • 軽い処理と重い処理がある場合、先に左側に軽い処理を持ってくるとで、重い処理をなるべく行わないようにすることができる
haseyuyhaseyuy

並行処理

協調的マルチタスク(ノンプリエンプティブ)

  • 実行プロセスの切替をプログラム自身に任せる方式で、プログラムが自発的にCPUを開放した時間でほかタスクを実行する

プリエンプション(preemption)

  • 実行行状態のタスクがプリエンプションが起きて実行可能状態に変わる処理
  • 「emption」は「購入」という意味で、preemptionは前もって購入すること,つまり「先取り」の意味

ディスパッチ

  • 実行状態に遷移させCPU能力を割り当てること

競合状態

以下の3つの条件を満たす状態

  • 二つの処理が変数を共有している
  • 少なくとも一つの処理がその変数を書き換える
  • 一つの処理が一段落つく前にもう一方の処理が割り込む可能性がある

競合状態の解決策

1. 共有しないアプローチ

プロセス

  • 実行中のプログラムのこと(タスクマネージャーを開いて表示されるプログラム一覧のようなもの?)
  • 異なるプロセスはメモリを共有しない
  • UNIXではプロセスごとに「使ってもよいメモリ領域」を決めることで、「異なるプロセスではメモリを共有しない」仕組みを実現している

スレッド

  • プロセス内で命令を逐次実行する部分であり、CPU コアを利用する単位
  • プロセスと異なり、異なるスレッドではメモリ領域を共有する
  • マルチスレッドの環境では、適切なメモリ管理を行わなければならず、このことをスレッドセーフという

アクターモデル

  • 「メモリを共有」ではなく「メッセージを送る」仕組み
  • ErlangやScalaなどで採用されている

2. 書き換えない(const, val, immutable)

  • 「メモリを共有しても、値を書き換えなければ問題ない」というアプローチ
  • Haskellではすべての値は変更不可能
  • Javaではimmutableパターンというものがある
    • クラスにprivateなフィールドを作り、それを読みだすためのgetterメソッドは作るが、書き換えるためのsetterメソッドは作らないこと

3. 割り込まない

協調的スレッドを使う(ファイバー, コールチン, グリーンスレッド)

  • スレッドがプリエンティブなことが割り込まれる原因なのだから、協調的なスレッドを作ればよい、という考え
  • 協調的なマルチタスクのため、あるスレッドがCPUを独り占めすると、ほかのスレッドの処理は止まる状態となる

割り込まれると困る処理中は印をつける(ロック, ミューテックス, セマフォ)

haseyuyhaseyuy

並行処理と並列処理の違い

  • 並行処理(Concurrency)は、複数個のスレッドを共通の期間内で実行すること
  • 並列処理(Parallelism)は、複数個のスレッドを同時に実行する能力