テックカリキュラム

オブジェクト指向設計と設計原則

オブジェクト指向設計と設計原則

本章では、VB.NETにおけるオブジェクト指向設計と、保守しやすいコードを書くための設計原則について学習します。

VB.NETは、単に画面や処理を順番に書いていくだけの言語ではありません。 Class、Interface、継承、ポリモーフィズム、Delegate、Propertyなどを活用することで、 大規模な業務システムにも耐えられる設計が可能です。

特に業務システムでは、最初にコードを書いた時点では問題がなくても、 数か月後、数年後に仕様変更、機能追加、障害対応、担当者変更が発生します。 そのときに重要になるのが、「動くコード」ではなく「変更に強いコード」です。

本章の目的は、VB.NETでオブジェクト指向の構文を使えるようになることではありません。 クラスをどのように分けるべきか、どこに処理を書くべきか、どのように依存関係を整理すべきかを理解し、 保守性の高いVB.NETアプリケーションを設計できるようになることです。


1. クラス設計の基本と責務分離

オブジェクト指向設計において、最も重要な考え方のひとつが「責務分離」です。 責務とは、そのクラスやメソッドが担当する役割のことです。

悪い設計では、ひとつのクラスが画面制御、入力チェック、業務ロジック、データベースアクセス、 ログ出力、ファイル出力など、複数の責務を抱え込んでしまいます。 このようなクラスは、一見便利に見えますが、変更に非常に弱くなります。

責務が集中した悪い例

Public Class UserForm

    Public Sub RegisterUser()
        ' 入力チェック
        If txtUserName.Text = "" Then
            MessageBox.Show("ユーザー名を入力してください。")
            Return
        End If

        ' 業務ロジック
        Dim userName As String = txtUserName.Text
        Dim createdAt As DateTime = DateTime.Now

        ' DB登録
        Dim connectionString As String = "Data Source=..."
        Using connection As New SqlClient.SqlConnection(connectionString)
            connection.Open()

            Dim sql As String = "INSERT INTO Users (UserName, CreatedAt) VALUES (@UserName, @CreatedAt)"
            Using command As New SqlClient.SqlCommand(sql, connection)
                command.Parameters.AddWithValue("@UserName", userName)
                command.Parameters.AddWithValue("@CreatedAt", createdAt)
                command.ExecuteNonQuery()
            End Using
        End Using

        ' 完了メッセージ
        MessageBox.Show("登録が完了しました。")
    End Sub

End Class

このコードは動作するかもしれません。 しかし、画面、入力チェック、業務処理、DBアクセスがすべて同じ場所に書かれています。

このような設計では、以下のような問題が発生します。

  • 入力チェックだけを変更したい場合でも画面コードを修正する必要がある
  • DB登録処理を他の画面で再利用しにくい
  • 単体テストがしづらい
  • 仕様変更時の影響範囲が分かりにくい
  • 担当者が変わったときにコードを理解しにくい

責務を分離した設計例

Public Class User
    Public Property UserName As String
    Public Property CreatedAt As DateTime
End Class
Public Class UserValidator

    Public Function Validate(user As User) As List(Of String)
        Dim errors As New List(Of String)()

        If String.IsNullOrWhiteSpace(user.UserName) Then
            errors.Add("ユーザー名を入力してください。")
        End If

        Return errors
    End Function

End Class
Public Class UserRepository

    Public Sub Insert(user As User)
        ' DB登録処理
    End Sub

End Class
Public Class UserService

    Private ReadOnly _validator As UserValidator
    Private ReadOnly _repository As UserRepository

    Public Sub New()
        _validator = New UserValidator()
        _repository = New UserRepository()
    End Sub

    Public Function Register(user As User) As List(Of String)
        Dim errors = _validator.Validate(user)

        If errors.Count > 0 Then
            Return errors
        End If

        user.CreatedAt = DateTime.Now
        _repository.Insert(user)

        Return errors
    End Function

End Class

このように分離すると、それぞれのクラスの役割が明確になります。 Userクラスはデータを表現し、UserValidatorは入力チェックを担当し、 UserRepositoryはDBアクセスを担当し、UserServiceは業務処理の流れを制御します。

責務分離の基本方針

  • 画面クラスに業務ロジックを書きすぎない
  • DBアクセス処理をServiceに直接書かない
  • 入力チェックを画面イベント内に散らばらせない
  • データを表すクラスと処理を行うクラスを分ける
  • 1つのクラスに多くの役割を持たせない

2. 継承・抽象クラス・インターフェース

VB.NETでは、継承、抽象クラス、インターフェースを利用することで、 共通処理の再利用や、処理の差し替えが可能になります。

ただし、これらは便利な反面、使い方を誤ると複雑で変更しにくい設計になります。 そのため、それぞれの役割を正しく理解することが重要です。

継承とは

継承とは、既存のクラスをもとに新しいクラスを作る仕組みです。 共通するプロパティやメソッドを親クラスに定義し、子クラスで再利用できます。

Public Class Employee
    Public Property Name As String
    Public Property Department As String

    Public Overridable Function GetRoleName() As String
        Return "社員"
    End Function
End Class

Public Class Manager
    Inherits Employee

    Public Overrides Function GetRoleName() As String
        Return "管理者"
    End Function
End Class

ManagerクラスはEmployeeクラスを継承しているため、 NameやDepartmentなどのプロパティをそのまま利用できます。 また、GetRoleNameメソッドをOverridesすることで、子クラス独自の動作を定義できます。

抽象クラスとは

抽象クラスは、直接インスタンス化することを目的とせず、 共通の構造や処理を子クラスに提供するためのクラスです。 VB.NETでは、MustInheritを使用します。

Public MustInherit Class ReportBase

    Public Property Title As String

    Public Sub PrintHeader()
        Console.WriteLine("レポート名:" & Title)
    End Sub

    Public MustOverride Sub PrintBody()

End Class
Public Class SalesReport
    Inherits ReportBase

    Public Overrides Sub PrintBody()
        Console.WriteLine("売上レポートの本文を出力します。")
    End Sub

End Class

抽象クラスでは、共通処理を実装しつつ、 一部の処理を子クラスに強制的に実装させることができます。

インターフェースとは

インターフェースは、クラスが実装すべき機能の契約を定義するものです。 VB.NETでは、Interfaceを使用します。

Public Interface IUserRepository
    Sub Insert(user As User)
    Function FindById(id As Integer) As User
End Interface
Public Class UserRepository
    Implements IUserRepository

    Public Sub Insert(user As User) Implements IUserRepository.Insert
        ' DB登録処理
    End Sub

    Public Function FindById(id As Integer) As User Implements IUserRepository.FindById
        ' DB検索処理
        Return New User()
    End Function

End Class

インターフェースを使うことで、呼び出し側は具体的なクラスではなく、 「この機能を持っているもの」として処理を扱えます。

継承・抽象クラス・インターフェースの使い分け

仕組み主な目的使う場面
継承共通機能の再利用明確な親子関係がある場合
抽象クラス共通処理と実装強制共通処理を持ちつつ一部だけ個別実装させたい場合
インターフェース機能の契約定義実装を差し替えたい場合、疎結合にしたい場合

実務では、継承よりもインターフェースを使った設計の方が柔軟になることが多いです。 継承は親クラスへの依存が強くなるため、むやみに使うと変更しにくい構造になります。


3. ポリモーフィズムの実務利用

ポリモーフィズムとは、同じ呼び出し方で異なる処理を実行できる仕組みです。 日本語では「多態性」と呼ばれます。

実務では、処理の種類が増える可能性がある場面で特に有効です。 たとえば、支払方法、帳票出力形式、通知方法、データ取込方式などは、 種類が増えやすい典型的な例です。

悪い例:If文で処理を分岐する

Public Sub SendNotification(notificationType As String, message As String)

    If notificationType = "Mail" Then
        Console.WriteLine("メール送信:" & message)
    ElseIf notificationType = "Slack" Then
        Console.WriteLine("Slack送信:" & message)
    ElseIf notificationType = "Sms" Then
        Console.WriteLine("SMS送信:" & message)
    End If

End Sub

この設計では、通知方法が増えるたびにSendNotificationメソッドを修正する必要があります。 分岐が増えるほど可読性が下がり、修正漏れやバグの原因になります。

良い例:インターフェースで処理を抽象化する

Public Interface INotificationSender
    Sub Send(message As String)
End Interface
Public Class MailSender
    Implements INotificationSender

    Public Sub Send(message As String) Implements INotificationSender.Send
        Console.WriteLine("メール送信:" & message)
    End Sub

End Class
Public Class SlackSender
    Implements INotificationSender

    Public Sub Send(message As String) Implements INotificationSender.Send
        Console.WriteLine("Slack送信:" & message)
    End Sub

End Class
Public Class NotificationService

    Private ReadOnly _sender As INotificationSender

    Public Sub New(sender As INotificationSender)
        _sender = sender
    End Sub

    Public Sub Notify(message As String)
        _sender.Send(message)
    End Sub

End Class

この設計では、NotificationServiceは具体的な送信方法を知る必要がありません。 MailSenderでもSlackSenderでも、INotificationSenderを実装していれば同じように扱えます。

ポリモーフィズムのメリット

  • If文やSelect Caseの肥大化を防げる
  • 新しい処理を追加しやすい
  • 既存コードへの影響を減らせる
  • テスト用の実装に差し替えやすい
  • 処理の責務をクラス単位で分離できる

ポリモーフィズムは、単なるオブジェクト指向の概念ではなく、 仕様変更に強い業務システムを作るための実践的な設計手法です。


4. SOLID原則

SOLID原則は、オブジェクト指向設計における代表的な設計原則です。 変更に強く、保守しやすいコードを書くための基本指針として、多くの開発現場で利用されています。

SOLIDは、以下の5つの原則の頭文字を取ったものです。

  • S:Single Responsibility Principle 単一責任の原則
  • O:Open/Closed Principle 開放閉鎖の原則
  • L:Liskov Substitution Principle リスコフの置換原則
  • I:Interface Segregation Principle インターフェース分離の原則
  • D:Dependency Inversion Principle 依存関係逆転の原則

単一責任の原則

単一責任の原則は、ひとつのクラスはひとつの責務だけを持つべきという考え方です。

たとえば、ユーザー登録処理において、入力チェック、DB登録、メール送信、ログ出力を ひとつのクラスにまとめると、変更理由が複数存在するクラスになります。

変更理由が複数あるクラスは、仕様変更のたびに壊れやすくなります。 そのため、責務ごとにクラスを分割することが重要です。

開放閉鎖の原則

開放閉鎖の原則は、拡張には開かれており、修正には閉じているべきという考え方です。

たとえば、新しい通知方法を追加するたびに既存のNotificationServiceを修正する設計は、 開放閉鎖の原則に反しています。 一方、INotificationSenderを実装した新しいクラスを追加するだけで対応できる設計であれば、 既存コードを大きく修正せずに拡張できます。

リスコフの置換原則

リスコフの置換原則は、親クラスやインターフェースを使っている場所に、 その派生クラスや実装クラスを置き換えても正しく動作すべきという考え方です。

たとえば、IReportExporterというインターフェースを使っている場合、 CsvExporter、PdfExporter、ExcelExporterのどれに差し替えても、 呼び出し側が破綻しない設計である必要があります。

インターフェース分離の原則

インターフェース分離の原則は、利用しないメソッドへの依存を強制してはいけないという考え方です。

たとえば、ひとつの巨大なIUserServiceに登録、更新、削除、検索、権限変更、通知、CSV出力などを すべて定義すると、実装クラスは不要なメソッドまで実装しなければならなくなります。

このような場合は、IUserRegisterService、IUserSearchService、IUserExportServiceのように、 目的ごとにインターフェースを分ける方が保守しやすくなります。

依存関係逆転の原則

依存関係逆転の原則は、上位モジュールが下位モジュールの具体実装に直接依存するのではなく、 抽象に依存すべきという考え方です。

ServiceクラスがUserRepositoryという具体クラスに直接依存していると、 DB実装を変更したり、テスト用Repositoryに差し替えたりすることが難しくなります。

一方、IUserRepositoryというインターフェースに依存していれば、 実際のDB実装、テスト用実装、外部API実装などに柔軟に差し替えることができます。


5. DTO / Entity / Service / Repositoryの分離

業務システムでは、データを扱うクラスと処理を行うクラスを適切に分けることが重要です。 特に、DTO、Entity、Service、Repositoryの役割を混同すると、 コードの責務が曖昧になり、保守性が低下します。

DTOとは

DTOは、Data Transfer Objectの略で、データを受け渡すためのオブジェクトです。 主に画面、API、外部システムとの間でデータをやり取りするために使用します。

Public Class UserRegisterDto
    Public Property UserName As String
    Public Property Email As String
    Public Property Password As String
End Class

DTOは、データの運搬を目的とするため、複雑な業務ロジックを持たせないのが基本です。

Entityとは

Entityは、業務上の意味を持つデータを表現するクラスです。 データベースのテーブルと対応することもありますが、単なるテーブル構造ではなく、 業務ルールを表現する役割を持つ場合もあります。

Public Class User
    Public Property Id As Integer
    Public Property UserName As String
    Public Property Email As String
    Public Property CreatedAt As DateTime

    Public Function IsValidEmail() As Boolean
        Return Email IsNot Nothing AndAlso Email.Contains("@")
    End Function
End Class

Entityには、そのデータ自身に関わる自然な振る舞いを持たせることがあります。 ただし、DB接続や画面制御のような外部処理を持たせるべきではありません。

Serviceとは

Serviceは、業務処理の流れを制御するクラスです。 入力チェック、Entityの生成、Repositoryの呼び出し、トランザクション制御、 外部連携などを組み合わせて、ひとつの業務機能を実現します。

Public Class UserService

    Private ReadOnly _repository As IUserRepository

    Public Sub New(repository As IUserRepository)
        _repository = repository
    End Sub

    Public Sub Register(dto As UserRegisterDto)
        Dim user As New User With {
            .UserName = dto.UserName,
            .Email = dto.Email,
            .CreatedAt = DateTime.Now
        }

        If Not user.IsValidEmail() Then
            Throw New ArgumentException("メールアドレスが不正です。")
        End If

        _repository.Insert(user)
    End Sub

End Class

Repositoryとは

Repositoryは、データアクセスを担当するクラスです。 DBへの登録、更新、削除、検索などを行います。 ServiceはRepositoryを通じてデータにアクセスし、SQLや接続処理の詳細を直接知る必要がないようにします。

Public Interface IUserRepository
    Sub Insert(user As User)
    Function FindById(id As Integer) As User
End Interface
Public Class UserRepository
    Implements IUserRepository

    Public Sub Insert(user As User) Implements IUserRepository.Insert
        ' INSERT文を実行する
    End Sub

    Public Function FindById(id As Integer) As User Implements IUserRepository.FindById
        ' SELECT文を実行する
        Return New User()
    End Function

End Class

各層の役割整理

分類役割書くべき処理書くべきでない処理
DTOデータの受け渡し画面/APIから受け取る値DB処理、複雑な業務ロジック
Entity業務データの表現データ自身に関わるルール画面制御、DB接続
Service業務処理の制御登録、更新、検索などの業務フローSQLの詳細、UI制御
RepositoryデータアクセスDB登録、検索、更新、削除画面制御、業務判断の集中

6. 密結合と疎結合

密結合とは、クラス同士が具体的な実装に強く依存している状態です。 密結合なコードは、ひとつの変更が他の多くの箇所に影響しやすく、テストや修正が難しくなります。

密結合な例

Public Class OrderService

    Private ReadOnly _repository As New OrderRepository()

    Public Sub RegisterOrder(order As Order)
        _repository.Insert(order)
    End Sub

End Class

この例では、OrderServiceがOrderRepositoryを直接生成しています。 そのため、OrderRepository以外の実装に差し替えることが困難です。

たとえば、単体テストでDBに接続しないFakeRepositoryを使いたい場合でも、 OrderService内部でNewしているため、差し替えができません。

疎結合な例

Public Interface IOrderRepository
    Sub Insert(order As Order)
End Interface
Public Class OrderService

    Private ReadOnly _repository As IOrderRepository

    Public Sub New(repository As IOrderRepository)
        _repository = repository
    End Sub

    Public Sub RegisterOrder(order As Order)
        _repository.Insert(order)
    End Sub

End Class

この設計では、OrderServiceはIOrderRepositoryに依存しており、 具体的なDB実装には依存していません。

そのため、本番環境ではOrderRepositoryを渡し、 テスト環境ではFakeOrderRepositoryを渡すことができます。

疎結合のメリット

  • 実装を差し替えやすい
  • 単体テストがしやすい
  • 仕様変更時の影響範囲を小さくできる
  • クラスごとの責務が明確になる
  • 再利用性が高まる

疎結合な設計は、初期実装では少し手間に感じることがあります。 しかし、長期運用される業務システムでは、その効果が非常に大きくなります。


7. 依存性注入の考え方

依存性注入とは、クラスが必要とする依存オブジェクトを、自分自身で生成するのではなく、 外部から受け取る設計手法です。 Dependency Injection、略してDIとも呼ばれます。

DIを利用することで、クラス同士の結合度を下げ、テストしやすく、変更に強い設計にできます。

依存性注入を使わない例

Public Class PaymentService

    Private ReadOnly _repository As New PaymentRepository()

    Public Sub Pay(payment As Payment)
        _repository.Insert(payment)
    End Sub

End Class

この設計では、PaymentServiceがPaymentRepositoryを直接生成しています。 そのため、PaymentServiceはPaymentRepositoryに強く依存しています。

コンストラクタインジェクション

代表的なDIの方法が、コンストラクタインジェクションです。 必要な依存オブジェクトをコンストラクタ経由で受け取ります。

Public Class PaymentService

    Private ReadOnly _repository As IPaymentRepository

    Public Sub New(repository As IPaymentRepository)
        _repository = repository
    End Sub

    Public Sub Pay(payment As Payment)
        _repository.Insert(payment)
    End Sub

End Class

このようにすることで、PaymentServiceは具体的なPaymentRepositoryではなく、 IPaymentRepositoryという抽象に依存します。

本番用実装

Public Class PaymentRepository
    Implements IPaymentRepository

    Public Sub Insert(payment As Payment) Implements IPaymentRepository.Insert
        ' DB登録処理
    End Sub

End Class

テスト用実装

Public Class FakePaymentRepository
    Implements IPaymentRepository

    Public Sub Insert(payment As Payment) Implements IPaymentRepository.Insert
        ' テスト用の処理
    End Sub

End Class

このように、同じIPaymentRepositoryを実装していれば、 本番用とテスト用の実装を簡単に差し替えられます。

DIのメリット

  • クラスが具体実装に依存しなくなる
  • テスト用のモックやFakeを使いやすくなる
  • 実装の差し替えが容易になる
  • 責務分離が進む
  • 保守性が高まる

VB.NETでも、ASP.NETや.NET Coreの環境ではDIコンテナを利用できます。 一方、Windows Formsなどの古い業務アプリではDIコンテナを使わない場合もあります。 その場合でも、コンストラクタで依存オブジェクトを渡す設計を意識するだけで、 十分に保守性を高めることができます。


8. 実務でありがちな悪い設計パターン

ここでは、VB.NETの業務システムでよく見られる悪い設計パターンを整理します。

画面クラスが巨大化している

Windows Formsのコードビハインドに、ボタンクリック処理、入力チェック、DBアクセス、 帳票出力、ログ出力がすべて書かれているケースです。

この状態になると、画面の修正が業務ロジックの修正と密接に絡み合い、 少しの変更でも影響範囲が広がります。

すべての処理が共通クラスに集約されている

CommonUtil、SystemCommon、GeneralFunctionのような名前の巨大クラスに、 文字列処理、日付処理、DB処理、ファイル処理、業務判定が大量に入っているケースです。

共通化そのものは悪くありません。 しかし、責務が曖昧な共通クラスは、結果的にどこからでも呼ばれる巨大な依存先になりやすいです。

Entityが単なる入れ物になっている

Entityがプロパティだけを持ち、業務ルールがすべてService側に散らばっているケースです。

すべての業務ロジックをEntityに入れる必要はありませんが、 データ自身に強く関係するルールは、Entity側に持たせた方が自然な場合もあります。

Repositoryに業務判断が入りすぎている

Repositoryは本来、データアクセスを担当する層です。 しかし、登録可否の判断、ステータス変更の業務ルール、通知処理などまでRepositoryに入ると、 データアクセス層が肥大化します。

Interfaceを作りすぎている

疎結合を意識するあまり、すべてのクラスに対して機械的にInterfaceを作るケースもあります。 これは設計を複雑にし、かえって可読性を下げる場合があります。

Interfaceは、実装の差し替えが必要な箇所、テストでモック化したい箇所、 外部依存を抽象化したい箇所を中心に利用するのが現実的です。


9. 演習課題

演習1:画面処理を責務ごとに分離する

ユーザー登録画面の処理を想定し、以下の責務にクラスを分割してください。

  • 入力値を受け取るDTO
  • ユーザー情報を表すEntity
  • 入力チェックを行うValidator
  • 業務処理を行うService
  • DB登録を行うRepository

演習2:通知処理をポリモーフィズムで実装する

メール通知、Slack通知、SMS通知を想定し、 INotificationSenderインターフェースを使って通知処理を切り替えられるようにしてください。

演習3:RepositoryをDIで差し替える

本番用Repositoryとテスト用Repositoryを作成し、 Serviceクラスのコンストラクタでどちらでも受け取れるように設計してください。

演習4:SOLID原則に違反したコードを改善する

入力チェック、DB登録、メール送信、ログ出力をすべて1つのクラスに書いたコードを用意し、 単一責任の原則に基づいてクラス分割してください。

演習5:DTOとEntityの違いを整理する

ユーザー登録、ユーザー検索、ユーザー詳細表示の3つの機能を想定し、 それぞれに必要なDTOとEntityを設計してください。


10. まとめ

本章では、VB.NETにおけるオブジェクト指向設計と設計原則について学習しました。

オブジェクト指向設計の目的は、単にClassやInterfaceを使うことではありません。 重要なのは、コードの責務を整理し、変更に強く、読みやすく、テストしやすい構造を作ることです。

クラス設計では、ひとつのクラスに多くの責務を持たせないことが重要です。 画面、業務ロジック、データアクセス、入力チェック、外部連携などを適切に分離することで、 仕様変更時の影響範囲を小さくできます。

継承、抽象クラス、インターフェースは、それぞれ役割が異なります。 継承は共通機能の再利用に有効ですが、親子関係が強くなるため慎重に使う必要があります。 抽象クラスは共通処理を持ちながら一部の実装を強制したい場合に有効です。 インターフェースは、実装の差し替えや疎結合な設計に役立ちます。

ポリモーフィズムを活用すると、If文やSelect Caseによる分岐を減らし、 処理の追加や変更に強い構造を作れます。 また、SOLID原則を意識することで、保守性の高いオブジェクト指向設計を実現しやすくなります。

DTO、Entity、Service、Repositoryを適切に分離することは、VB.NETの業務システム開発において非常に重要です。 それぞれの役割を明確にすることで、コードの見通しが良くなり、再利用性やテスト容易性も向上します。

さらに、密結合を避け、依存性注入を活用することで、 具体的な実装に依存しすぎない柔軟な設計が可能になります。 これは、将来的な機能追加、DB変更、外部API連携、単体テストなどに大きな効果を発揮します。

本章のゴールは、VB.NETで「動くコード」を書くだけでなく、 「変更に強いコード」を設計できるようになることです。 この考え方は、今後学習するDB連携、例外処理、非同期処理、テスト設計、 レガシーシステムの改善にも直結する重要な基礎となります。