密結合と疎結合について
密結合
クラスやコンポーネントが他のクラスに直接依存して実装されている状態を指します
テストや保守が困難になる可能性があります
コード
コードとしてはRepositoryを作成せずServiceとControllerのみ作成し、ServiceをControllerから直接三所する形で実装してみてどのようなコードが密結合か確認していきます
+------------------+ +------------------+
| Controller | ---> | Service |
+------------------+ +------------------+
Service作成
メッセージを送信する体のコードを書いていきます
mkdir -p src/app/Services/HighCoupling
touch src/app/Services/HighCoupling/NotificationService.php
<?php
namespace App\Services\HighCoupling;
class NotificationService
{
public function sendEmail(string $message)
{
echo "Sending Email: $message";
}
}
Controller作成
コマンドラインからControllerを作成していきます
php artisan make:controller HighCoupling/HighCouplingNotificationController
作成したControllerにて先ほど作成したServiceを呼び出してあげます
<?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を作成していきます
php artisan make:view highCoupling/send
<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を追加していきます
<?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
php artisan make:test Services/HighCoupling/NotificationServiceTest --unit
php artisan make:test Controllers/HighCoupling/HighCouplingNotificationControllerTest --unit
<?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
<?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を作成していきます
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
<?php
namespace App\Repositories\Interfaces;
interface NotifierRepositoryInterface
{
public function sendNotification(string $message): void;
}
<?php
namespace App\Repositories;
use App\Repositories\Interfaces\NotifierRepositoryInterface;
class EmailNotifierRepository implements NotifierRepositoryInterface
{
public function sendNotification(string $message): void
{
echo "Sending Email: $message";
}
}
<?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を一緒に作成していきます
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
<?php
namespace App\Services\Interfaces;
interface NotificationServiceInterface
{
public function notifyUser(string $message): void;
}
ここでRepositoryを呼ぶ時、Repository自身を呼ぶのではなく依存性注入に使用したInterfaceを呼ぶようにしてあげます
<?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では自動的に依存関係を解決してくれます
<?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を作成していきます
php artisan make:view lowCoupling/send
<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を追加していきます
<?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
php artisan make:test Repositories/EmailNotifierRepositoryTest --unit
php artisan make:test Repositories/SMSNotifierRepositoryTest --unit
<?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');
}
}
<?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
php artisan make:test Services/LowCoupling/NotificationServiceTest --unit
<?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
php artisan make:test Controllers/LowCoupling/LowCouplingNotificationControllerTest --unit
<?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は作成済みなのでサービスプロバイダのみ改修すれば変更が可能です
<?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