オブジェクトの広場はオージス総研グループのエンジニアによる技術発表サイトです

UML/モデリング

ちょっとうれしい UML 2.0 のこんな技

再定義を設計モデル(クラス図)で使うの巻
オージス総研
山内亨和
2007年1月11日

オージス総研では、社内の有志で集まっていくつかの勉強会を実施しています。その中の一つに、筆者が参加している UML 2.0 に関する勉強会もあります。この勉強会では、現場で活用している UML 2.0 のテクニックを共有したり、便利そうな UML 2.0 の表記法を発掘したりしています。この勉強会の成果を、(少しずつではありますが)皆さんに公開していきたいと思います。

はじめに

 UML 2.0 では再定義 ( redefine ) という新しい仕組みが登場しています。
 再定義とは、操作のオーバーライドをより広範囲に適用した仕組みで、スーパークラスで定義した様々な要素をサブクラスで定義し直すことを意味します。

 UML 2.0 では以下の要素などを再定義可能な要素 ( RedefinableElement ) として定義しています。

  • 属性
  • 関連端
  • 操作
  • 内部クラス
  • 制約
  • ステートマシン

 再定義を使うことで、UML 1.x で表現できなかったことが表現できるようになりました。今回は設計モデル(クラス図)で再定義を使うとどのように有効か解説します。

※ 再定義は設計モデル(クラス図)以外でも有効です。いずれ他の例も紹介したいと思います。

とある設計モデル・ソースコードの例

 以下の Javaのソースコードは、4 つのクラス(抽象全体クラス、抽象部分クラス、具象全体クラス、具象部分クラス)とそれらの相互作用を実装しています。 ( J2SE 1.4 の文法で実装しています)

ソースコードサンプルクラスの説明
public abstract class AbstractWhole{
    private AbstractPart part;
    public AbstractWhole(){
        part = createPart();
    }
    protected abstract AbstractPart createPart();
    public void do(){
        part.do();
    }
}
抽象全体クラス(AbstractWhole)
特徴1:部分クラスの生成手続きを実装している。
特徴2:具体的な処理を部分クラスに委譲している。
public abstract class AbstractPart{
    protected abstract void do();
}
抽象部分クラス(AbstractPart)
特徴1:処理のインターフェース(do 操作)を定義している。
public class ConcreteWhole extends AbstractWhole{
    protected AbstractPart createPart(){
        return new ConcretePart(this);
    }
}
具象全体クラス(ConcreteWhole)
特徴1:特定の具象部分クラスを生成する。
public class ConcretePart exnteds AbstractPart{
    public ConcretePart(ConcreteWhole whole){
        // コンストラクタの実装
    }
    protected void do(){
        // 操作の実装
    }
}
具象部分クラス(ConcretePart)
特徴1:具象部分クラスを生成する。
特徴2:処理(do 操作)を実装している。

 まずは UML 2.0 を意識せず、従来の UML 1.5 のクラス図でこのソースコードを表現してみましょう。


 
図 1 UML 1.5 の設計モデルの例

 UML 1.5 のクラス図では、ConcreteWhole クラスに関する次のような設計情報を表現できません。

  1. スーパークラスの構造とサブクラスの構造の関連性が見えない
    ConcreteWhole クラスと ConcretePart クラスの関係は、AbstractWhole クラスと AbstractPart クラスの関係を特化したものですが、これを表現できていません。
  2. 操作をオーバーライドして何が変わったか分からない
    ConcreteWhole クラスの createPart 操作では ConcretePart クラスのインスタンスを返却値として返すのですが、これを表現できていません。

 UML 1.5 ではサブクラスでスーパークラスをどのように特化したのか定義できないため、このような表現上の不便が発生するのです。

UML 2.0 の再定義を使ったクラス図

 UML 2.0 の再定義は、サブクラスの表現力を向上させます。

関連端を再定義する

 UML 2.0 では、関連端を再定義できます。関連端を再定義することで、サブクラスで関連先のクラスを変えることができます。

 今回の例では、ConcreteWhole クラスの part 関連端を ConcretePart クラスにしています(スーパークラスでは AbstractPart クラスでした)。

 サブクラスの関連端に「{redefines part}」とスーパークラスの関連端名を示すことで、関連端を再定義していることを表現します。


 
図 2 part 関連端を再定義する

操作を再定義する

 UML 2.0 では、操作を再定義できます。操作を再定義することで、操作の返却値の型を変えることができます。

 今回の例では、ConcreteWhole クラスの createPart 操作の返却値を ConcretePart クラスにしています(スーパークラスでは AbstractPart クラスでした)。

 サブクラスの操作に「{redefines createPart()}」とスーパークラスの操作名を示すことで、操作を再定義していることを表現します。


 
図 3 createPart 操作を再定義する

再定義を使う際の注意点(実装とのマッピングルールを用意する)

 このように再定義は便利な概念ですが、使う際に注意が必要です。

 残念ながら、Java などのプログラミング言語は再定義の概念を実装していません。そのため、設計モデルとソースコードのマッピングルールを明確にしておかないと、実装者は誤った実装をしてしまうでしょう。また、モデリングツールを使ってソースコードの自動生成をしたいのであれば、ツールにソースコードへのマッピングルールを実装しなければいけません。

 参考までに、再定義を使った場合の Java へのマッピングルールを示します。

関連端を再定義した場合の Java のマッピングルール

基本ルール

サブクラスで属性(UML の関連端に対応)を宣言してはいけません。
 Java 言語では、サブクラスでスーパークラスと同名の属性を宣言しても、スーパークラスの属性を再定義したことにはなりません(別の属性として宣言されます)。

良い例悪い例
public class ConcreteWhole extends AbstractWhole{
    protected AbstractPart createPart(){
        return new ConcretePart(this);
    }
}
 
public class ConcreteWhole extends AbstractWhole{
    private ConcretePart part; // <= だめ !!!
    protected AbstractPart createPart(){
        return new ConcretePart(this);
    }
}

private 関連端を再定義している場合のルール

 private 関連端を再定義する目的は、スーパークラスの構造とサブクラスの構造の関連性を理解しやすくするためであって、実装への指示ではありません。
 そのため、スーパークラスの抽象メソッドをサブクラスで実装する以外に特別な実装は必要ありません。

protected 関連端を再定義している場合のルール

 サブクラスの属性に再定義した関連端の型を設定するように、サブクラスのコンストラクタやメソッドを実装してください。


 
図 4 protected 関連端を再定義した例

操作の返却値を再定義した場合のマッピングルール

J2SE 1.4 以前のルール

 サブクラスでスーパークラスのメソッドをオーバーライドし、返却値のインスタンスが再定義した型になるように実装してください。
 サブクラスに再定義した返却値の型が異なる操作を宣言してはいけません。

良い例悪い例
public class ConcreteWhole extends AbstractWhole{
    protected AbstractPart createPart(){
        return new ConcretePart(this);
    }
}
 
public class ConcreteWhole extends AbstractWhole{
    protected ConcretePart createPart(){ // <= だめ !!!
        return new ConcretePart(this);
    }
}
 

Java 5 以降のルール

 Java 5 には共変戻り値型という仕組みがあるため、操作の返却値の再定義をより素直に実装できます。
 サブクラスでスーパークラスのメソッドをオーバーライドし、返却値は再定義した型で宣言してください。

良い例悪い例
public class ConcreteWhole extends AbstractWhole{
    protected ConcretePart createPart(){
        return new ConcretePart(this);
    }
}
 
public class ConcreteWhole extends AbstractWhole{
    protected AbstractPart createPart(){ // <= だめ !!!
        return new ConcretePart(this);
    }
}
 

まだまだ使える再定義

 今回紹介した以外にも、クラスにおいて再定義には様々な使い道があります。

  • 属性・関連端のケース
    • 多重度を再定義
      例:属性・関連端の多重度を 0..* から 1..* に変える。(または 1 や 0 に変える)
    • ordered に再定義
      例:属性・関連端の順序を ordered(ソートしてない)から ordered(ソートしている)に変える。

  • 操作のケース
    • 返却値の多重度、orderedを再定義
      例は属性・関連端と同様のため省略します。
    • 事前条件を再定義
      例:事前条件を緩くする。
    • 事後条件を再定義
      例:事後条件を厳しくする。

参考文献

[1] 『その場でつかえるしっかり学べる UML 2.0』オブジェクトの広場編集部/著, 秀和システム