💬

密結合と疎結合について

に公開

密結合

クラスやコンポーネントが他のクラスに直接依存して実装されている状態を指します
テストや保守が困難になる可能性があります

コード

コードとしてはRepositoryを作成せずServiceとControllerのみ作成し、ServiceをControllerから直接三所する形で実装してみてどのようなコードが密結合か確認していきます

+------------------+      +------------------+ 
|  Controller      | ---> |  Service         | 
+------------------+      +------------------+

Service作成

メッセージを送信する体のコードを書いていきます

zsh
mkdir -p src/app/Services/HighCoupling
touch src/app/Services/HighCoupling/NotificationService.php
src/app/Services/HighCoupling/NotificationService.php
<?php

namespace App\Services\HighCoupling;

class NotificationService
{
    public function sendEmail(string $message)
    {
        echo "Sending Email: $message";
    }
}

Controller作成

コマンドラインからControllerを作成していきます

zsh
php artisan make:controller HighCoupling/HighCouplingNotificationController

作成したControllerにて先ほど作成したServiceを呼び出してあげます

app/Http/Controllers/HighCoupling/HighCouplingNotificationController.php
<?php

namespace App\Http\Controllers\HighCoupling;

use App\Http\Controllers\Controller;
use App\Services\HighCoupling\NotificationService;
use Illuminate\Http\Request;

class HighCouplingNotificationController extends Controller
{
    public function __construct()
    {
        $this->notificationService = new NotificationService();
    }

    public function index()
    {
        return view('highCoupling.send');
    }

    public function send(Request $request)
    {
        $message = $request->input('message');
        return $this->notificationService->sendEmail($message);
    }
}

view作成

確認しやすいようにviewを作成していきます

zsh
php artisan make:view highCoupling/send
src/resources/views/highCoupling/send.blade.php
<html>
    <head>
        <title>Send</title>
    </head>
    <body>
        <form action="{{ route('high-coupling.send') }}" method="post">
            @csrf
            <input type="text" name="message" placeholder="Message">
            <button type="submit">Send</button>
        </form>
    </body>
</html>

Route追加

ここまで書けたらRouteを追加していきます

src/routes/web.php
<?php

use Illuminate\Support\Facades\Route;
use App\Http\Controllers\HighCoupling\HighCouplingNotificationController;
use App\Http\Controllers\LowCoupling\LowCouplingNotificationController;

/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
|
| Here is where you can register web routes for your application. These
| routes are loaded by the RouteServiceProvider and all of them will
| be assigned to the "web" middleware group. Make something great!
|
*/

Route::get('/', function () {
    return view('welcome');
});

+ Route::prefix('high-coupling')
+    ->name('high-coupling.')
+    ->controller(HighCouplingNotificationController::class)
+    ->group(function () {
+        Route::get('/', 'index')->name('index');
+        Route::post('/send', 'send')->name('send');
+    });

テスト

作成したService、Controllerのテストを書いていきます

Service

zsh
php artisan make:test Services/HighCoupling/NotificationServiceTest --unit
php artisan make:test Controllers/HighCoupling/HighCouplingNotificationControllerTest --unit
src/app/Services/HighCoupling/NotificationServiceTest.php
<?php

namespace Tests\Unit\Services\HighCoupling;

use PHPUnit\Framework\TestCase;
use App\Services\HighCoupling\NotificationService;

class NotificationServiceTest extends TestCase
{
    public function test_sendEmail()
    {
        $notificationService = new NotificationService();

        $this->expectOutputString("Sending Email: Test message");

        $notificationService->sendEmail('Test message');
    }
}

Controller

src/app/Controllers/HighCoupling/HighCouplingNotificationControllerTest.php
<?php

namespace Tests\Unit\Http\Controllers\HighCoupling;

use PHPUnit\Framework\TestCase;
use App\Http\Controllers\HighCoupling\HighCouplingNotificationController;
use Illuminate\Http\Request;

class HighCouplingNotificationControllerTest extends TestCase
{
    public function test_send()
    {
        $controller = new HighCouplingNotificationController();

        $request = new Request(['message' => 'Test message']);

        $response = $controller->send($request);

        $this->assertNull($response);
    }
}

何が問題なのか

今回のコードは短いので密結合の問題が出にくいかもしれませんが
問題として

テストのしずらさ

Controller内で直接呼び出しているためモックを作成出来ないためテストがしづらくなっています

改修がしづらい

実装時はEmailでメッセージを送信しようとしています
それに合わせてテストも実装しています
ですが、途中でEmailではなくSMSで実装する方針になった場合、Service層の実装を改修する必要があります
改修時にServiceのメソッド名がemail用に実装しているためsms用にするとControllerも改修する必要が出るためService以外でも改修する必要が出てきます
Serviceのコードを改修するとなるテストも改修する必要も出てきます
Controllerの改修が必要になるとも記載しているためControllerのテストも改修になると影響範囲が大きくなってしまします

疎結合

密結合とは逆でクラスやコンポーネントが他のクラスに依存関係が少ない状態で実装されている状態を指します
テストや保守がようになる可能性があります

コード

コードとしてはControllerとServiceだけでなくRepositoryも作成していきます
また依存性注入(DI)を行うためのInterfaceも作成していきます

+------------------+      +---------------------+
|  Controller      | ---> |  Service Interface  | ---> (DI)
+------------------+      +---------------------+
                              |
                              v
                    +-------------------+
                    |  Service Impl     |
                    +-------------------+
                              |
                              v
                    +-----------------------+
                    |  Repository Interface |
                    +-----------------------+
                              |
                              v
                    +-------------------+
                    |  Repository Impl  |
                    +-------------------+

Repository作成

Repositoryを作成していきます
依存性注入を行うためにInterfaceも一緒に作成していきます
また改修のしやすさを見るためにEmailとSMSでメッセージを送信する体でRepositoryを作成していきます

zsh
mkdir -p src/app/Repositories/Interfaces
touch src/app/Repositories/Interfaces/NotifierRepositoryInterface.php
touch src/app/Repositories/EmailNotifierRepository.php
touch src/app/Repositories/SMSNotifierRepository.php
src/app/Repositories/Interfaces/NotifierRepositoryInterface.php
<?php

namespace App\Repositories\Interfaces;

interface NotifierRepositoryInterface
{
    public function sendNotification(string $message): void;
}
src/app/Repositories/EmailNotifierRepository.php
<?php

namespace App\Repositories;

use App\Repositories\Interfaces\NotifierRepositoryInterface;

class EmailNotifierRepository implements NotifierRepositoryInterface
{
    public function sendNotification(string $message): void
    {
        echo "Sending Email: $message";
    }
}
src/app/Repositories/SMSNotifierRepository.php
<?php

namespace App\Repositories;

use App\Repositories\Interfaces\NotifierRepositoryInterface;

class SMSNotifierRepository implements NotifierRepositoryInterface
{
    public function sendNotification(string $message): void
    {
        echo "Sending SMS: $message";
    }
}

Service作成

Serviceを作成していきます
こちらでも依存性注入のためにInterfaceを一緒に作成していきます

zsh
mkdir -p src/app/Services/Interfaces
mkdir -p src/app/Services/LowCoupling
touch src/app/Services/Interfaces/NotificationServiceInterface.php
touch src/app/Services/LowCoupling/NotificationService.php
src/app/Services/Interfaces/NotificationServiceInterface.php
<?php

namespace App\Services\Interfaces;

interface NotificationServiceInterface
{
    public function notifyUser(string $message): void;
}

ここでRepositoryを呼ぶ時、Repository自身を呼ぶのではなく依存性注入に使用したInterfaceを呼ぶようにしてあげます

src/app/Services/LowCoupling/NotificationService.php
<?php

namespace App\Services\LowCoupling;

use App\Services\Interfaces\NotificationServiceInterface;
use App\Repositories\Interfaces\NotifierRepositoryInterface;

class NotificationService implements NotificationServiceInterface
{
    private NotifierRepositoryInterface $notifierRepository;

    public function __construct(NotifierRepositoryInterface $notifierRepository)
    {
        $this->notifierRepository = $notifierRepository;
    }

    public function notifyUser(string $message): void
    {
        $this->notifierRepository->sendNotification($message);
    }
}

サービスプロバイダに登録

作成したRepositoryをサービスプロバイダに登録していきます
ここで登録することでServiceでは自動的に依存関係を解決してくれます

src/app/Providers/AppServiceProvider.php
<?php

namespace App\Providers;

use App\Services\LowCoupling\NotificationService;
use Illuminate\Support\ServiceProvider;
+ use App\Repositories\Interfaces\NotifierRepositoryInterface;
+ use App\Repositories\EmailNotifierRepository;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Register any application services.
     */
    public function register(): void
    {
+        $this->app->singleton(
+            NotifierRepositoryInterface::class,
+            EmailNotifierRepository::class
+        );
    }

    /**
     * Bootstrap any application services.
     */
    public function boot(): void
    {
        //
    }
}

viewの作成

確認しやすくするためにviewを作成していきます

zsh
php artisan make:view lowCoupling/send
src/resources/views/lowCoupling/send.blade.php
<html>
    <head>
        <title>Send</title>
    </head>
    <body>
        <form action="{{ route('low-coupling.send') }}" method="post">
            @csrf
            <input type="text" name="message" placeholder="Message">
            <button type="submit">Send</button>
        </form>
    </body>
</html>

Routeの登録

ここまで書けたらRouteを追加していきます

src/routes/web.php
<?php

use Illuminate\Support\Facades\Route;
use App\Http\Controllers\HighCoupling\HighCouplingNotificationController;
use App\Http\Controllers\LowCoupling\LowCouplingNotificationController;

/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
|
| Here is where you can register web routes for your application. These
| routes are loaded by the RouteServiceProvider and all of them will
| be assigned to the "web" middleware group. Make something great!
|
*/

Route::get('/', function () {
    return view('welcome');
});

Route::prefix('high-coupling')
    ->name('high-coupling.')
    ->controller(HighCouplingNotificationController::class)
    ->group(function () {
        Route::get('/', 'index')->name('index');
        Route::post('/send', 'send')->name('send');
    });

+ Route::prefix('low-coupling')
+    ->name('low-coupling.')
+    ->controller(LowCouplingNotificationController::class)
+    ->group(function () {
+        Route::get('/', 'index')->name('index');
+        Route::post('/send', 'send')->name('send');
+    });

テスト

作成したRepository、Service、Controllerのテストを書いていきます

Repository

zsh
php artisan make:test Repositories/EmailNotifierRepositoryTest --unit
php artisan make:test Repositories/SMSNotifierRepositoryTest --unit
src/tests/Unit/Repositories/EmailNotifierRepositoryTest.php
<?php

namespace Tests\Unit\Repositories;

use App\Repositories\EmailNotifierRepository;
use PHPUnit\Framework\TestCase;

class EmailNotifierRepositoryTest extends TestCase
{
    /**
     * A basic unit test example.
     */
    public function test_email(): void
    {
        $emailNotifier = new EmailNotifierRepository();
        $this->expectOutputString("Sending Email: Hello World");
        $emailNotifier->sendNotification('Hello World');
    }
}
src/tests/Unit/Repositories/SMSNotifierRepositoryTest.php
<?php

namespace Tests\Unit\Repositories;

use App\Repositories\SMSNotifierRepository;
use PHPUnit\Framework\TestCase;

class SMSNotifierRepositoryTest extends TestCase
{
    /**
     * A basic unit test example.
     */
    public function test_sms(): void
    {
        $smsNotifier = new SMSNotifierRepository();
        $this->expectOutputString("Sending SMS: Hello World");
        $smsNotifier->sendNotification('Hello World');
    }
}

Service

zsh
php artisan make:test Services/LowCoupling/NotificationServiceTest --unit
src/tests/Services/LowCoupling/NotificationServiceTest.php
<?php

namespace Tests\Unit\Services\LowCoupling;

use App\Services\LowCoupling\NotificationService;
use App\Repositories\Interfaces\NotifierRepositoryInterface;
use PHPUnit\Framework\TestCase;

class NotificationServiceTest extends TestCase
{
    public function testNotifyUser()
    {
        $notifierMock = $this->createMock(NotifierRepositoryInterface::class);

        $notifierMock->expects($this->once())
            ->method('sendNotification')
            ->with($this->equalTo('Test message'));

        $notificationService = new NotificationService($notifierMock);

        $notificationService->notifyUser('Test message');
    }
}

Controller

zsh
php artisan make:test Controllers/LowCoupling/LowCouplingNotificationControllerTest --unit
src/tests/Controllers/LowCoupling/LowCouplingNotificationControllerTest
<?php

namespace Tests\Unit\Controllers\LowCoupling;

use App\Http\Controllers\LowCoupling\LowCouplingNotificationController;
use App\Services\LowCoupling\NotificationService;
use PHPUnit\Framework\TestCase;
use Illuminate\Http\Request;

class LowCouplingNotificationControllerTest extends TestCase
{
    public function test_send(): void
    {
        $notificationServiceMock = $this->createMock(NotificationService::class);
        $notificationServiceMock->expects($this->once())
            ->method('notifyUser')
            ->with($this->equalTo('Test message'));

        $controller = new LowCouplingNotificationController($notificationServiceMock);

        $request = new Request(['message' => 'Test message']);

        $response = $controller->send($request);

        $this->assertNull($response);
    }
}

何が良いのか

今回のコードは短いのですが良さが分かりやすいかと思います

テストがしやすい

ServiceでのテストでRepositoryを直接呼んでいるわけではないためモックを作成して渡すことでテストが容易になります

改修のしやすさ

EmailではなくSMSでメッセージを送信する実装にする方針になった場合に改修が容易になっています
SMSのRepositoryは作成済みなのでサービスプロバイダのみ改修すれば変更が可能です

src/app/Providers/AppServiceProvider.php
<?php

namespace App\Providers;

use App\Services\LowCoupling\NotificationService;
use Illuminate\Support\ServiceProvider;
use App\Repositories\Interfaces\NotifierRepositoryInterface;
- use App\Repositories\EmailNotifierRepository;
+ use App\Repositories\SMSNotifierRepository;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Register any application services.
     */
    public function register(): void
    {
        $this->app->singleton(
            NotifierRepositoryInterface::class,
-            EmailNotifierRepository::class
+            SMSNotifierRepository::class
        );
    }

    /**
     * Bootstrap any application services.
     */
    public function boot(): void
    {
        //
    }
}

また、環境ごとに振る舞いを変更する場合にも便利です

class AppServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        if (App::environment('production')) {
            // 本番環境では Email 通知を使用
            $this->app->singleton(
                NotifierRepositoryInterface::class,
                EmailNotifierRepository::class
            );
        } else {
            // テスト環境・ローカル環境では Fake 通知を使用 (実際の送信を行わない)
            $this->app->singleton(
                NotifierRepositoryInterface::class,
                FakeNotifierRepository::class
            );
        }
    }
}

Discussion