密結合と疎結合を分かりやすく解説
密結合とは?
密結合とは、クラスやウィジェット、コンポーネントが互いに強く依存している状態のことです。
密結合なコードの具体例・改善例
以下のコードはUserProfileScreen
がUserSettingsScreen
に直接依存しているため、密結合しています。
UserProfileScreen
を別の場所で使おうとすると、他の関連部分も一緒に修正する必要が出てきます。
class UserProfileScreen extends StatelessWidget {
final User user;
UserProfileScreen(this.user);
@override
Widget build(BuildContext context) {
return Column(
children: [
Text('Name: ${user.name}'),
Text('Email: ${user.email}'),
ElevatedButton(
onPressed: () {
Navigator.of(context).push(MaterialPageRoute(
builder: (context) => UserSettingsScreen(user),
));
},
child: Text('Settings'),
),
],
);
}
}
改善例として、以下のコードのように、コールバック関数を渡すことで、画面遷移の処理をUserProfileScreen
から分離します。
このようにすると、遷移先や動作を後から変更しやすくなり、再利用性も向上します。
class UserProfileScreen extends StatelessWidget {
final User user;
final VoidCallback onPressed;
final String buttonText;
UserProfileScreen(this.user, this.onPressed, this buttonText);
@override
Widget build(BuildContext context) {
return Column(
children: [
Text('Name: ${user.name}'),
Text('Email: ${user.email}'),
ElevatedButton(
onPressed: onPressed,
child: Text(buttonText),
),
],
);
}
}
密結合なコードのデメリット3つ
密結合なコードのデメリット3つを解説します。
1.変更が難しい
ある部分を変更すると、依存している他の部分も一緒に修正する必要があり、時間と工数がかかります。
2.再利用性が低い
他のプロジェクトや他の部分で再利用したくても、密結合しているため簡単に取り出して使うことができません。
3.テストが難しい
一部の機能だけをテストしたい場合でも、密結合だと他の関連部分も含めてテストしないといけません。
疎結合とは?
疎結合とは、各コンポーネントやクラスができるだけ独立しており、互いに依存関係が少ない状態のことです。
疎結合なコードのメリット3つ
疎結合なコードのメリット3つを解説します。
1.変更や拡張がしやすい
あるコンポーネントを変更しても他への影響が少ないため、保守が容易です。
2.テストが容易
個々の部分を独立してテストできるため、単体テストがしやすく、バグの検出も簡単です。
3.再利用性が高い
独立性が高いため、別の場所や別のプロジェクトでそのまま再利用しやすいです。
すべてを疎結合にすることのデメリット3つ
疎結合は理想的に見えますが、すべてを疎結合にすることで生まれるデメリットもあります。
1.複雑化
すべてを疎結合にしようとすると、コンポーネント同士の連携に工夫が必要で、コード全体が複雑になることがあります。
2.設定の難しさ
各部分が独立しているため、状態管理や依存関係の設定が増え、コードが読みにくくなる可能性があります。
3. パフォーマンスの低下
各部分が独立しすぎると、やり取りが増えてパフォーマンスが低下する場合もあります。
疎結合にしすぎた具体例
以下のように疎結合にしすぎるがゆえに、コールバックやデータの受け渡しが増えすぎてコードが複雑化しています。
また、複数のスクリーンで同じデータを管理するため、更新の際にどこを変更すべきかを把握するのが難しくなります。
今回の例においては、完全な疎結合を避けて、共通のデータをProvider
やRiverpod
などの状態管理パッケージを使って、一元化することで疎結合を保ちながらもデータの管理が簡単になります。
// ユーザー情報を表示し、設定ボタンが押されたときにコールバックを実行
class UserProfileScreen extends StatelessWidget {
final User user;
final VoidCallback onSettingsPressed;
UserProfileScreen(this.user, this.onSettingsPressed);
@override
Widget build(BuildContext context) {
return Column(
children: [
Text('Name: ${user.name}'),
Text('Email: ${user.email}'),
ElevatedButton(
onPressed: onSettingsPressed,
child: Text('Settings'),
),
],
);
}
}
// ユーザー設定を行いますが、UserProfileScreen からデータを引き継ぐ方法が複雑になっています
class UserSettingsScreen extends StatelessWidget {
final Function(User) onSaveSettings;
UserSettingsScreen(this.onSaveSettings);
@override
Widget build(BuildContext context) {
// 設定を保存するためのフォームや入力フィールド
return Column(
children: [
TextField(
decoration: InputDecoration(labelText: 'Username'),
onSubmitted: (newName) {
// 保存ボタンでデータをコールバック経由で保存
onSaveSettings(User(newName, 'newEmail@example.com'));
},
),
// 他の設定フィールド...
],
);
}
}
// つなぎ合わせるメインスクリーン
class MainScreen extends StatefulWidget {
@override
_MainScreenState createState() => _MainScreenState();
}
class _MainScreenState extends State<MainScreen> {
User user = User('Alice', 'alice@example.com');
void _goToSettings() {
Navigator.of(context).push(MaterialPageRoute(
builder: (context) => UserSettingsScreen((updatedUser) {
setState(() {
user = updatedUser;
});
}),
));
}
@override
Widget build(BuildContext context) {
return UserProfileScreen(user, _goToSettings);
}
}
class User {
String name;
String email;
User(this.name, this.email);
}
まとめ
疎結合を意識することは大切ですが、必要以上に分離しすぎず、適度に密結合も取り入れるバランスが重要です。