LoginSignup
0
0

More than 3 years have passed since last update.

クラスとインタフェース TypeScript & Angular 勉強会 #5

Last updated at Posted at 2020-08-27
1 / 13

演習問題回答


Typescriptのクラスの特徴

  • C# から受け継いだ、Java/Delphiの流れを受け継いだクラスベースのオブジェクト指向
    • 継承、インタフェース、アノテーション
  • Javascript から展開された、プロトタイプベースのオブジェクト指向
    • ミックスイン

の双方の影響を受けている


クラス記法

class Position { //クラスの宣言
  constructor (
    private x: number, //コンストラクタでの引数宣言と、クラスのメンバ宣言を同時に行うこともできる
    private y: number
  )

  private member: number //メンバ変数の宣言

  protected member_protected: string //protected
  public member_public: Object //public, デフォルトのアクセス指定子
  private readonly member_ro: boolean //readonly

  public method(arg: number): string { //メンバー関数
     return arg.toString()
  }

}

let pos = new Position(100, 200) //x:100, y:200 のPositionをnewする

継承と抽象クラス

extends キーワードにより継承によるクラスの拡張を行うことができます。

//麻雀牌
class Tiles {
}

//数牌
class Suites extends Tiles {
  protected num: number = 1
}

//萬子
class Characters extends Suites {
  public printTile(): void {
     console.log("")
  }
}

let character = new Characters()

本来は、萬子以外の牌がインスタンス化されることはないので、 abstract キーワードで抽象クラスにします。

//麻雀牌
abstract class Tiles {
}

//数牌
abstract class Suites extends Tiles {
  protected num: number = 1
}

//萬子
class Characters extends Suites {
}

let suit = new Suites() // Cannot create an instance of an abstract class.(2511)

super() による親クラスコンストラクタの呼び出し

現在の実装だと、数字が1の数牌しか定義できないので、コンストラクタを追加しましょう。

//麻雀牌
abstract class Tiles {
}

//数牌
abstract class Suites extends Tiles {
  constructor(
    protected num: number
    ) {
    super() //継承クラスでコンストラクタを定義した場合、親のコンストラクタをsuperで呼び出す必要がある
  }
}

//萬子
class Characters extends Suites {
}

let ch = new Characters(1) //明示的にコンストラクタを定義しない場合、暗黙的に親クラスのコンストラクタを利用できる

ついでに赤牌も定義しておきましょう。

class RedCharacters extends Characters {
  constructor(){
    super(5) //赤牌は5限定なので親クラスコンストラクタをパラメータ付きで呼び出す
  }
}

super による親メソッド呼び出し

牌の種別を文字列で出力する関数を考えましょう。
五萬赤牌は、継承元のprintメソッドを使いたいため、superで親メソッドにアクセスしています。

//萬子
class Characters extends Suites {
  print(): string{
    return `${this.num}萬`
  }
}

//五萬赤
class RedCharacters extends Characters {
  constructor(){
    super(5) //赤牌は5限定なので親クラスコンストラクタをパラメータ付きで呼び出す
  }

  print(): string{
    return `${super.print()} 赤`
  }
}

戻り値の型としての this

自分自身と同じものを作成する関数 clone を考えます。

共通の関数なので、大本の Tiles に作成することにしましょう。

abstract class Tiles {
  public clone():Tiles {
    return {...this}
  }
}

//数牌
abstract class Suites extends Tiles {
  constructor(
    protected num: number
    ) {
    super()
  }
}

//萬子
class Characters extends Suites {
  print(): string{
    return `${this.num}萬`
  }
}

//五萬赤
class RedCharacters extends Characters {
  constructor(){
    super(5) //赤牌は5限定なので親クラスコンストラクタをパラメータ付きで呼び出す
  }

  print(): string{
    return `${super.print()} 赤`
  }
}

let ch = new Characters(1)
let ch2:Characters = ch.clone() //Type 'Tiles' is missing the following properties from type 'Characters': print, num(2739)

しかしその場合、継承先クラスでも親クラスを返してしまうため、コンパイルエラーとなります。

ここで、thisを用いた型づけをすると、継承先のクラスの型をリターンするようになるため、エラーを発生させずに実装できます。

abstract class Tiles {
  public clone():this{
    return {...this}
  }
}

インタフェース

インタフェースは、実装を持たずに型の宣言のみを持ったクラスです。

interface LogItem {
  message: string,
  type: number,
  print: () => string
}

実際のところ、以下の型エイリアスとかなり似ており、いくつかの違いを除いてほぼ同一に扱うことができます。

type LogItem = {
  message: string,
  type: number,
  print: () => string
}

インタフェースも継承させることができます。

interface UILogItem extends LogItem {
  element: Element
}

型エイリアスとインタフェース

型エイリアスとインタフェースは大きく3つの違いがあります。

型エイリアスでしか表現できない型がある

インタフェースは、あくまでにObject型のようにアトリビュートを持った型の構造を規定するものです。
つまり、以下のような構造を持たない型については、interfaceは宣言する術を持ちません。

type numberAlias = number
type Direction = "" | "" | "西" | ""

インタフェースは拡張時に拡張元の型が割り当て可能かを厳密に判断する

以下のようなインタフェース宣言は不可能です。

interface LogItem {
  message: string,
  type: number,
  print: () => string
}

interface UILogItem extends LogItem { //Interface 'UILogItem' incorrectly extends interface 'LogItem'.
  print: () => Element
}

なぜなら、拡張元のインタフェース LogItem が、拡張先のインタフェース UILogItem に代入できなくなっているからです。

似たようなことを 型エイリアスでやってみます。

type UILogItem2 = LogItem & {print: () => Element}

この場合、コンパイルエラーは発生せず、うまくマージされた型を作ってくれます。

インタフェース宣言は、マージされる

インタフェースは以下のように分割して定義しても、コンパイル時にマージされます。

interface Animal {
  name: string
}

interface Animal {
  canFly: boolean
}

let animal: Animal = {
  name: "Lion",
  canFly: false
}

インタフェース宣言の実装

インタフェース宣言は、型エイリアス同様に型定義として利用できますが、クラスに対する制約としても定義できます。
インタフェースは extends ではなく、 implements です。

interface Animal {
  name: string
}

interface Animal {
  canFly: boolean
}

class Cat implements Animal {
  constructor(
    public name: string,
    public canFly: boolean) {
  }
}

また、インタフェースは複数を実装することも普通です。

interface Animal {
  name: string
  canFly: boolean
}

interface Pet {
  ownerName: string
  nickname: string
}


class Cat implements Animal, Pet {
  constructor(
    public name: string,
    public nickname: string,
    public ownerName: string,
    public canFly: boolean) {
  }
}

この「複数のインタフェースを持てる」という部分が、今後説明する設計パターンを活用する上での重要なポイントになります。


演習問題

麻雀牌のクラスを役牌含めて完成させてください。
継承、インタフェース、抽象クラスなど、今回説明したものは何でも用いてOKです。
(途中実装が資料にありますが、それを無視しても構いません)


0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0