nprogram’s blog

気ままに、プログラミングのトピックについて書いていきます

Observerパターン [C#] [push型]

Observerパターンとは

あるオブジェクトの状態に依存するオブジェクトがあるとき、状態の変化をそれらのオブジェクトに依存せずに自動的に通知するパターン

push型とpull型

Observerパターンでは,push型とpull型があります。違いは以下のとおり。

  • push型 : updateの引数に変更された状態を渡すことで、Observerに通知する
  • pull型 : updateを呼び出されたタイミングでObserverがObservableに状態を問い合わせる

本記事では、push型のObserverパターンについて説明します。

Observerパターンのpush型のクラス図

f:id:nprogram:20190731224438p:plain

各クラスの役割

  • Subject
    • Observer の登録と削除、状態変化の通知の機能を定義する
  • Observer
    • Subject から通知を受け取る機能を定義する
    • ConcreteSubject‐ConcreteObserver 間の結合を抽象的に(直接的な依存関係を排除)するためのインターフェース
  • ConcreteSubject
    • Observer に状態の変化を通知する。
    • Observer とは一対多の関係。
    • 状態の変化を登録されているすべてのObserverに通知する責務を持つ
    • 複数の異なる ConcreteObserverに対応できるように、抽象的に Observer のリストを保持する
    • 被観察者
    • 別名として観察可能な(Observable)クラスとも呼ばれる
    • ConcreteSubjectはConcreteObserverの存在を知らない。Observerの存在は知っている
  • ConcreteObserver
    • Subject の状態が変化されたことが通知される
    • 状態変化による無矛盾性を保つ
    • 観察者

コード

using System;
using System.Collections.Generic;

namespace Observer_Pattern
{
    class Program
    {
        static void Main(string[] args)
        {
            ConcreteObserver observer1 = new ConcreteObserver();
            ConcreteObserver observer2 = new ConcreteObserver();

            ConcreteSubject subject = new ConcreteSubject();

            subject.Add(observer1);
            subject.Add(observer2);

            Random rnd = new System.Random();

            for (int i = 0; i < 3; i++)
            {
                // Set Random Value from 0 to 9
                subject.Status = rnd.Next(10);
            }
        }
    }

    interface IObserver
    {
        void Update(int num);
    }

    interface ISubject
    {
        void Add(IObserver item);
        void Delete(IObserver item);
        void Notify();
    }

    class ConcreteObserver : IObserver
    {
        public void Update(int num)
        {
            Console.WriteLine("Update Value: {0}", num);
        }
    }

    class ConcreteSubject : ISubject
    {
        private List<IObserver> _observers = new List<IObserver>();

        public void Add(IObserver item)
        {
            _observers.Add(item);
        }

        public void Delete(IObserver item)
        {
            _observers.Remove(item);
        }

        public void Notify()
        {
            _observers.ForEach(observer => observer.Update(_Status));

            //foreach (var item in _observers)
            //{
            //    item.Update(_Status);
            //}
        }

        private int _Status;
        public int Status
        {
            get { return _Status; }
            set
            {
                _Status = value;
                Notify();
            }
        }
    }
}
Update Value: 4
Update Value: 4
Update Value: 2
Update Value: 2
Update Value: 5
Update Value: 5

参考リンク

デバッグ技術

はじめに

ソフトウェア評価時では、デバッグは必要不可欠です。

以下のような手順で行うと思います。

手順

  1. バグを記録する (バグレポートを書く)
  2. バグを再現する
  3. バグを分離する (バグ発生原因を特定するために必要)
  4. バグを修正する

バグを記録する

バグ内容・バグ発生手順を記録します。

バグ発生時の環境(PC環境・試験対象ソフトウェアバージョン・試験対象ソフトウェア状態・他の接続機器)も正確に記載してください。

記録がないと、後で再現させることができません。

バグを再現する

バグを再現させます。簡単に再現できない場合もあります。

ソフトウェアの特定の動作タイミングや特定の通信のみで発生するバグなど簡単に再現できない場合は工夫が必要です。

バグを分離する

バグ発生原因を特定するために必要な作業です。

バグを修正する

その他の機能に影響を与えないように慎重にコードを修正します。

Windows標準のイベントビューアーを使用する

Windows のイベントビューアーを使って、何がいつ起きたか調べる

eventvwr.exe

コマンド実行フィールドにeventvwrと記載しても起動できます。

Windows のイベントビューアーを使って、何がいつ起きたか調べる | ラボラジアン

バグを再現させて、IDEのブレークポイントを使用する

デバッグの技術 | POSTD

Nullチェックを削除する方法 (NullObjectパターンとNull合体演算子)

はじめに

Nullチェックを削除する方法はいくつかあります。最適な方法を使用してください。

  • NullObjectパターン
    • オブジェクト指向プログラミングが使えれば、どの言語でも実現可能
  • Null合体演算子
    • 言語仕様にNull合体演算子があれば可能
  • その他の方法
    • Javaの場合はOptionalを使用

NullObjectパターン

NullObjectパターンを使用することで、Nullチェックのif文を削除できます。

クラス図

f:id:nprogram:20190619230456p:plain

コード例

次にように作成します。

  1. 何もない(本例では職種がない)ことを表すサブクラスであるNullEmployeeクラスを作成します。

  2. サブクラスのメソッド(本例ではgetTypeNameメソッド)は呼ばれても何も処理しないようにします

  3. CreatorクラスのfactoryMethodにおいて、不正なパラメータを受け取った場合は、NullEmployeeクラスを返します。

  4. 呼び出し元関数のおいて、Nullチェックのif文を削除します

  5. NullEmployeeクラスのインスタンスが存在するため、NullPointerExceptionの例外が発生しません。

本記事に記載されていない他のクラス(Salesman, Engineer, Manager, Employee)のコードは、以下の記事を参照ください。

switch文を爆散 - nprogram’s blog

[NullEmployee .java]

// 具象クラス
public class NullEmployee extends Employee
{
    public NullEmployee(int typeId)
    {
        _typeId = typeId;
    }
    
    @Override
    public int getTypeId()
    {
        return _typeId;
    }
    
    @Override
    public String getTypeName()
    {
        // Nullオブジェクトであるため、処理が空っぽの状態とする
        return "";
    }
}

[CreatorEmployee.java]

// 本クラスは継承を禁止するため、final修飾子を付与します
public final class CreatorEmployee
{
    // 本例では、定数の定義はCreatorEmployeeクラスが持ちます
    // もし、列挙型(Enum)を使いたいなら、以下のサイトを参考にしてください
    // https://qiita.com/KeithYokoma/items/9681b130ea132cfad64d
    
    public static final int ENGINEER = 0;
    public static final int SALESMAN = 1;
    public static final int MANAGER = 2;
    public static final int ILLEGAL_EMPLOYEE = -1;
    
    // コンストラクタをprivateにすることで、インスタンス化を禁止させましょう
    private CreatorEmployee(){ }
    
    // factoryMethodメソッドの役割は与えられたパラメータによって生成するインスタンスを切り替えること
    public static Employee factoryMethod(int type)
    {
        switch(type)
        {
            case ENGINEER:
                return new Engineer(ENGINEER);
                
            case SALESMAN:
                return new Salesman(SALESMAN);
                
            case MANAGER:
                return new Manager(MANAGER);
                
            default:
                System.out.println("Error!! Illegal Parameter.");
                return new NullEmployee(ILLEGAL_EMPLOYEE);
        }
    }
}

[Main.java]

import java.util.*;

public class Main
{
    public static void main(String[] args) throws Exception
    {
        // (1) インスタンスを生成
        // (2) 関数に作成したインスタンスを渡す
        
        Employee engineer = createEmployee(CreatorEmployee.ENGINEER);
        showTypeName(engineer);
        showId(engineer);
        
        Employee salesman = createEmployee(CreatorEmployee.SALESMAN);
        showTypeName(salesman);
        showId(salesman);
        
        Employee manager = createEmployee(CreatorEmployee.MANAGER);
        showTypeName(manager);
        showId(manager);
        
        Employee nullEmployee = createEmployee(-9999); // 不正値を入力する
        showTypeName(nullEmployee);
        showId(nullEmployee);
    }
    
    public static Employee createEmployee(int type)
    {
        return CreatorEmployee.factoryMethod(type);
    }
    
    
    public static void showId(Employee employ)
    {
        // Nullオブジェクト適用により、null比較のif文がなくなりました。
        System.out.println(employ.getTypeName());
    }
    
    public static void showTypeName(Employee employ)
    {
        // Nullオブジェクト適用により、null比較のif文がなくなりました。
        System.out.println(employ.getTypeId());  
    }
}
0
Engineer
1
Salesman
2
Manager
Error!! Illegal Parameter.
-1

null合体演算子

言語使用に、NullObjectパターンを使用するのではなく、言語使用として、NullObjectパターンを使用しなくともNullPointerException例外を回避できる仕組みがあります。

C#では「null合体演算子」(null coalescing operator、coalesceは「合体する」という意味の動詞)を使うと、nullチェックのif文を削除できます。

以下に、コード例を記載します。

[適用前]

public class Test{
    public static void Main(){

        string documentTitle = null;
        string initialTitle = "報告書";
    
        if (documentTitle == null) {
            documentTitle = initialTitle;
        }
        System.Console.WriteLine(documentTitle);
    }
}
報告書

Null合体演算子(C#では??)を使うと以下のように記載できます。これにより、Nullチェックのif文を削除できました

[適用後]

public class Test{
    public static void Main(){

        string documentTitle = null;
        string initialTitle = "報告書";
        
        string fileName = documentTitle ?? initialTitle;
        
        System.Console.WriteLine(fileName);
    }
}
報告書

その他の方法

Java言語の場合はOptionalを使うことで、NullPointerException例外を回避できそうです。

あとがき

NullObjectパターンを使用しなくともNullPointerException例外を回避できる言語仕様があれば、積極的に使うべきだと思います。

NullObjectパターンは、あまり使われなくなりそうです。

switch文を爆散

はじめに

コードのあちこちに同じようなswitch文処理が書かれている場合があります。

機能追加・修正するたびに変更が必要なswitch文処理を探してコード修正しなくてはなりません。

このようなswitch文処理を見つけたら、ポリモーフィズムが使うことで、リファクタリングできないか考えてみましょう。

また、他のクラスのインスタンスを取得してswitch文で条件分岐処理している場合は、リファクタリングの優先度が高いと考えています。

書籍

  • 新装版 リファクタリング 既存のコードを安全に改善する
    • Martin Fowler [著]
    • P83

static Factory Method Patternを用いて、switch文を削除

switch文を削除する方法はポリモーフィズムを使用しますが、ポリモーフィズムの使用方法はいくつかあります。

そのときの状況で最適な方法が変化すると思います。

本記事では、(1)の方法を使用して、コード例のswitch文を削除します。

  1. static Factory Method Patternで削除
  2. Factory Method Patternで削除
  3. Stateパターンで削除
  4. Starategyパターンで削除

コード例

コード例は、従業員の職業ID値と職業名を表示する処理です。

Mainクラスで、Employeeクラスのインスタンスを生成します。

そして、MainクラスのshowId関数とshowTypeName関数で、Employeeクラスのインスタンスの情報を元に、switch文で条件分岐処理をします。

本コードを(1)の方法で、リファクタリングしていきます。

言語はJavaを使用してますが、オブジェクト指向プログラミングが実現できる言語であれば、どんな言語でも実現可能です。

修正前のコード

本コードで問題なのは、従業員の職業IDの種類が増えるたびに、MainクラスのshowId関数とshowTypeName関数の両方をコード修正しなくてはなりません。

Mainクラスが仮に巨大で複雑なクラスであった場合は、コード修正洩れが発生しやすいです・・・。

クラス図

f:id:nprogram:20190618071624p:plain

[Main.java]

import java.util.*;

public class Main {
    
    public static final int ENGINEER = 0;
    public static final int SLAESMAN = 1;
    public static final int MANAGER = 2;
    
    public static void main(String[] args) throws Exception
    {
        // (1) インスタンスを生成
        // (2) 関数に作成したインスタンスを渡す
        
        Employee engineer = new Employee(ENGINEER);
        showTypeName(engineer);
        showId(engineer);
        
        Employee salesman = new Employee(SLAESMAN);
        showTypeName(salesman);
        showId(salesman);
        
        Employee manager = new Employee(MANAGER);
        showTypeName(manager);
        showId(manager);
        
        Employee nullEmployee = new Employee(-9999); // 不正値を入力する
        showTypeName(nullEmployee);
        showId(nullEmployee);
    }
    
    public static void showId(Employee employ)
    {
        switch(employ._typeId)
        {
            case ENGINEER:
                System.out.println(employ._typeId);
                break;
            case SLAESMAN:
                System.out.println(employ._typeId);
                break;
            case MANAGER:
                System.out.println(employ._typeId);
                break;
            default:
                System.out.println(employ._typeId);
                break;
        }
    }
    
    public static void showTypeName(Employee employ)
    {
        switch(employ._typeId)
        {
            case ENGINEER:
                System.out.println("ENGINEER");
                break;
            case SLAESMAN:
                System.out.println("SLAESMAN");
                break;
            case MANAGER:
                System.out.println("MANAGER");
                break;
            default:
                System.out.println("Error!! Illegal Parameter.");
                break;
        }
    }
}   

[Employee.java]

public class Employee
{
    public int _typeId;

    
    public Employee(int type)
    {
        _typeId = type;
    }
}
ENGINEER
0
SLAESMAN
1
MANAGER
2
Error!! Illegal Parameter.
-9999

修正後のコード

static Factory Methodパターンを用いて、MainクラスのshowId関数とshowTypeName関数のswitch文を削除します。

手順

おおまかな手順は以下のとおりです。

  1. サブクラス(本例では、Enginner, Salesman, Managerクラス)を作成します。
  2. 共通部分はスーパークラス(本例では、Empolyeeクラス)で定義します

  3. Creatorクラス(本例では、CreatorEmployeeクラス)を作成します。

    • Creatorクラスは、インスタンス化しません(インスタンス化を防ぎます)
    • factoryMethodメソッドで、サブクラスのインスタンスを生成します
  4. Mainクラスのswitch文を削除します

クラス図

f:id:nprogram:20190619230651p:plain

[Main.java]

import java.util.*;

public class Main
{
    public static void main(String[] args) throws Exception
    {
        // (1) インスタンスを生成
        // (2) 関数に作成したインスタンスを渡す
        
        Employee engineer = createEmployee(CreatorEmployee.ENGINEER);
        showTypeName(engineer);
        showId(engineer);
        
        Employee salesman = createEmployee(CreatorEmployee.SALESMAN);
        showTypeName(salesman);
        showId(salesman);
        
        Employee manager = createEmployee(CreatorEmployee.MANAGER);
        showTypeName(manager);
        showId(manager);
        
        Employee nullEmployee = createEmployee(-9999); // 不正値を入力する
        showTypeName(nullEmployee);
        showId(nullEmployee);
    }
    
    public static Employee createEmployee(int type)
    {
        return CreatorEmployee.factoryMethod(type);
    }
    
    
    public static void showId(Employee employ)
    {
        // switch文を撲滅完了。ただし、NullPointerExceptionの例外を防ぐため、Nullチェックのif文は必要。
        if (employ != null)
        {
            System.out.println(employ.getTypeName());  
        }
    }
    
    public static void showTypeName(Employee employ)
    {
        // switch文を撲滅完了。ただし、NullPointerExceptionの例外を防ぐため、Nullチェックのif文は必要。
        if (employ != null)
        {
            System.out.println(employ.getTypeId());  
        }
    }
}

[CreatorEmployee.java]

// 本クラスは継承を禁止するため、final修飾子を付与します
public final class CreatorEmployee
{
    // 本例では、定数の定義はCreatorEmployeeクラスが持ちます
    public static final int ENGINEER = 0;
    public static final int SALESMAN = 1;
    public static final int MANAGER = 2;
    public static final int ILLEGAL_EMPLOYEE = -1;
    
    // コンストラクタをprivateにすることで、インスタンス化を禁止させましょう
    private CreatorEmployee(){ }
    
    // factoryMethodメソッドの役割は与えられたパラメータによって生成するインスタンスを切り替えること
    public static Employee factoryMethod(int type)
    {
        switch(type)
        {
            case ENGINEER:
                return new Engineer(ENGINEER);
                
            case SALESMAN:
                return new Salesman(SALESMAN);
                
            case MANAGER:
                return new Manager(MANAGER);
                
            default:
                System.out.println("Error!! Illegal Parameter.");
                return null;
        }
    }
}

[Employee.java]

// 抽象クラス
public abstract class Employee
{
    protected int _typeId;
    protected String _typeName;
    
    public abstract int getTypeId();
    
    public abstract String getTypeName();
}

[Engineer.java]

// 具象クラス
public class Engineer extends Employee
{
    public Engineer(int typeId)
    {
        _typeId = typeId;
    }
    
    @Override
    public int getTypeId()
    {
        return _typeId;
    }
    
    @Override
    public String getTypeName()
    {
        return "Engineer";
    }
}

[Salesman.java]

// 具象クラス
public class Salesman extends Employee
{
    public Salesman(int typeId)
    {
        _typeId = typeId;
    }
    
    @Override
    public int getTypeId()
    {
        return _typeId;
    }
    
    @Override
    public String getTypeName()
    {
        return "Salesman";
    }
}

[Manager.java]

public class Manager extends Employee
{
    public Manager(int typeId)
    {
        _typeId = typeId;
    }
    
    @Override
    public int getTypeId()
    {
        return _typeId;
    }
    
    @Override
    public String getTypeName()
    {
        return "Manager";
    }
}
0
Engineer
1
Salesman
2
Manager
Error!! Illegal Parameter.

まだ、switch文残っているじゃん・・・。

はい。まだswitch文残っています。CreatorEmployeeクラスのfactoryMethodメソッドは、引数に異常値を渡した場合、戻り値としてNullのインスタンスを返します。

そのため、factoryMethodメソッド関数の呼び出し元では、Nullチェックが必要なのです。(Nullチェックしないと、JavaではNull Pointer Exceptionのエラーが発生)

    public static void showId(Employee employ)
    {
        // switch文を撲滅完了。ただし、NullPointerExceptionの例外を防ぐため、Nullチェックのif文は必要。
        if (employ != null)
        {
            System.out.println(employ.getTypeName());  
        }
    }

でも、ご安心ください。そのための秘策として、NullObjectパターンがあります。

以下の記事に、NullObjectパターンを用いて、Nullチェックを削除する方法について記載しました

リンク

Factory MethodパターンとAbstract Factory Methodパターンの違いについては、以下のサイトが詳しいです。

巨大クラスをクラス抽出で爆散

はじめに

巨大クラスを爆散したいときがあります。

その場合は、クラス抽出が便利です。

書籍

  • 新装版 リファクタリング 既存のコードを安全に改善する
    • Martin Fowler [著]

手順

(1) クラスの責務を切り出す方法を決める

(2) 切り出された責務を記述する新たなクラスを作る

(3) 元の暮らすから新たなクラスにリンクを貼る

(4) 移動しようとする各フィールドに「フィールドの移動(p146)」を適用する

(5) 移動が終わるごとにコンパイルしてテストする

(6) メソッドの移動(p142)を適用して元の暮らすから新たなクラスにメソッドを移動する

(7) 移動が終わるごとにコンパイルしてテストする

(8) 各クラスのインタフェースを見直して、減らす

(9) 新たなクラスを公開するか、公開するならそれを参照オブジェクトとして公開するのか、不変な値オブジェクトとして公開するのか決める

Personクラスの中に、電話番号のインスタンスがあります。

クラス分割を行い、Personクラスが持つ電話番号関連の責務を別クラスに持たせましょう。

分割前

import java.util.*;

public class Main {
    public static void main(String[] args) throws Exception {
        Person person = new Person("Tom", "123-", "123-4567");
        
        System.out.println(person.getName());
        System.out.println(person.getTelepthonNumber());
        System.out.println(person.getOfficeNumber());
    }
}
public class Person
{
    private String _name;
    private String _officeAreaCode;
    private String _officeNumber;
    
    public Person(String name, String AreaCode, String number)
    {
        _name = name;
        _officeAreaCode = AreaCode;
        _officeNumber = number;
    }
    
    public String getName()
    {
        return _name;
    }
    
    public String getTelepthonNumber() 
    {
        return _officeAreaCode + _officeNumber;
    }
    
    void setOfficeAreaCode(String arg)
    {
        _officeAreaCode = arg;
    }
    
    String getOfficeNumber()
    {
        return _officeNumber;
    }
    
    void setOfficeNumber(String arg)
    {
        _officeNumber = arg;
    }
}

分割後

import java.util.*;

public class Main {
    public static void main(String[] args) throws Exception
    {
        Person person = new Person("Tom", "123-", "123-4567");
        
        System.out.println(person.getName());
        System.out.println(person.getTelepthonNumber());
        System.out.println(person.getOfficeNumber());
    }
}
public class Person
{
    private String _name;

    private final Telephone telephone;
    
    public Person(String name, String areaCode, String number)
    {
        _name = name;
        telephone = new Telephone(areaCode, number);
    }
    
    public String getName()
    {
        return _name;
    }
    
    public String getTelepthonNumber() 
    {
        return telephone.getTelepthonNumber();
    }
    
    public String getOfficeNumber() 
    {
        return telephone.getOfficeNumber();
    }
    
    public String getAreaCode() 
    {
        return telephone.getAreaCode();
    }
}
public class Person
{
    private String _name;

    private final Telephone telephone;
    
    public Person(String name, String areaCode, String number)
    {
        _name = name;
        telephone = new Telephone(areaCode, number);
    }
    
    public String getName()
    {
        return _name;
    }
    
    public String getTelepthonNumber() 
    {
        return telephone.getTelepthonNumber();
    }
    
    public String getOfficeNumber() 
    {
        return telephone.getOfficeNumber();
    }
    
    public String getAreaCode() 
    {
        return telephone.getAreaCode();
    }
}

リンク

以下のサイトは、巨大クラスを爆散するのにとても役立ちます。

リファクタリング入門

はじめに

以下の書籍を読んでリファクタリングを学習します。

書籍

  • 新装版 リファクタリング 既存のコードを安全に改善する
    • Martin Fowler [著]

リファクタリングの定義

外部から見たときのふるまいを保ちつつ、理解や修正が簡単になるようにソフトウェアの内部構造を変化させること

変更しにくいプログラムとは

  • 読みにくいプログラムは変更しにくい
  • ロジックが重複しているプログラムは変更しにくい
  • 機能追加に伴い、既存のコード修正が必要となるプログラムは変更しにくい
  • 複雑な条件分岐の多いプログラムは変更しにいくい

リファクタリングのコツ

  • 小さな単位で行うこと

    • 失敗する(バグを埋め込む)可能性を軽減できる
  • 新規追加の作業とリファクタリングの作業を同じ作業単位(コミット)で行ってはならない。必ず分けること

    • バグ発生時、バグ発生個所の特定が難しい (新規追加部分が原因か、リファクタリング部分が原因か・・・)
    • コード変更差分がとても見づらくなる (コード差分が新規追加の差分なのか、リファクタリングの差分なのかすぐに判別不可能)

リファクタリングトピック集

巨大なクラス

1つのクラスがあまりにも多くの仕事をしている場合は、たいていインスタンス変数を持ちすぎています。
インスタンス変数が多すぎると、重複コードが存在する可能性が高くなります

巨大クラスをクラス抽出で爆散 - nprogram’s blog

長すぎるパラメータリスト

  • メソッドによるパラメータの置き換え(P.292)
    • 既知のオブジェクトに問い合わせることでパラメータのデータの1つを取得できる
  • オブジェクトそのものの受け渡し (P.288)
    • データをばらばらに渡さずにオブジェクトそのものを渡すようにできます
  • 論理的なオブジェクトとしてデータの集まりを表現できない場合
    • パラメータオブジェクトの導入 (P.295)

switch文に気をつけろ

他のオブジェクトの属性を調べるswitch文処理を書くことは常に間違いである。

switch文処理は、他のオブジェクトについてではなく、自分自身のオブジェクトの情報について行うべきである。

switch文を爆散 - nprogram’s blog

データの群れ

数個のデータがグループとなって、クラスのフィールドやメソッドのシグニチャなど、さまざまな箇所に現れることがあります。

こうした群れをなしたデータはオブジェクトとして、1つにまとめるべき。

属性や操作のパラメータの数を絞り込むことはコードの不吉なにおいをなくすのに役立ちます。

特性の横恋慕

処理の際に、他のオブジェクトのgetメソッドを何度も何度も呼び出している間違ったメソッドがよく見受けられる

処理、および処理に必要なデータを1つにまとめる

あとがき

本の格言は素晴らしいと思います。

  • コンパイラが理解できるコードは誰にでも書ける。優れたプログラマは、人間にとってわかりやすいコードを書く
  • 新規追加の帽子とリファクタリングの帽子を同時にかぶるな