【初心者向け】単一責任原則の具体例
単一責任原則とは?
単一責任原則(Single Responsibility Principle)は、ソフトウェア開発のソリッド原則の一つで、「クラスは1つの責任だけを持つべきだ」というものです。
クラスが複数の責任を持つ場合、それらの責任のいずれかが変更されたとき、その影響が他の責任にも影響してしまう可能性があります。
単一責任原則を無視すると、一見効率的なコードもすぐに管理が困難になってしまいます。
単一責任でないコードの具体例
ここでは、ECサイトにおける割引機能の実装を例として見てみましょう。
通常割引の初期実装
Flutterでの割引管理クラスは次のようになっていて、1商品につき500円割引される仕様です。
class DiscountManager {
static const int discountAmount = 500;
int applyDiscount(int originalPrice) {
final discountedPrice = originalPrice - discountAmount;
return discountedPrice > 0 ? discountedPrice : 0;
}
}
限定割引の追加と問題の発生
その後、限定割引の仕様が追加されることになりました。
ただ、この割引は上記で定義した割引とは仕様が異なるが、1商品につき500円割引される仕様は同じです。
限定割引は別の人が実装担当することになったと仮定して、「DiscountManager
に500円割引ロジックが実装されてるから流用しよう、DRYにもなる 」と判断して以下のように実装しました。
class SpecialDiscountManager {
int applySpecialDiscount(int originalPrice) {
return DiscountManager().applyDiscount(originalPrice);;
}
}
既存割引の変更と影響
DiscountManager
の割引の仕様が5%割引に変更されるようになり、実装担当者は、 以下のように修正しました。
(限定割引で流用されていることも知らないままです…)
class DiscountManager {
static const int discountAmount = 0.05;
int applyDiscount(int originalPrice) {
final discountedPrice = originalPrice * (1.00 - discountAmount);
return discountedPrice > 0 ? discountedPrice : 0;
}
}
結果的に、限定割引が予期しない結果を招くことになりました。
この例から、各クラスが単一の責任を持つよう設計することが非常に重要です。
クラスの責任が明確でないと、コードの変更が一部に影響を及ぼす可能性が高くなり、バグの原因となります。
単一責任である改善例のコード
以下のような設計にすることで、各割引価格の計算が独立して行われます。
これにより、割引計算の変更が商品追加のロジックに影響を与えないようになりました。
class RegularPrice {
static const int minAmount = 0;
final int amount;
RegularPrice(this.amount) {
if (amount < minAmount) {
throw ArgumentError('Price cannot be less than $minAmount.');
}
}
}
class RegularDiscountedPrice {
static const double discountRate = 0.05;
final int amount;
RegularDiscountedPrice(RegularPrice price) {
this.amount = (price.amount * (1.00 - discountRate)).toInt();
}
}
class SummerDiscountedPrice {
static const int minAmount = 0;
static const int discountAmount = 500;
final int amount;
SummerDiscountedPrice(RegularPrice price) {
int discountedAmount = price.amount - discountAmount;
this.amount = discountedAmount < minAmount ? minAmount : discountedAmount;
}
}
まとめ
適切な設計原則の適用をすることで、このような問題を未然に防ぐことができるので、しっかりと意識していきましょう。
【番外編】Flutterによくあるコードを見てみる
以下のコード例はFlutterでよく見られるパターンですが、よりクリーンなアーキテクチャを目指す場合、データ取得とUIロジックを分離することで、各クラスの責任を明確にすることが望ましいです。
class UserProfilePage extends StatefulWidget {
@override
_UserProfilePageState createState() => _UserProfilePageState();
}
class _UserProfilePageState extends State<UserProfilePage> {
User user;
@override
void initState() {
super.initState();
_fetchUserData();
}
Future<void> _fetchUserData() async {
final userData = await UserDataRepository.getUserData();
setState(() {
user = userData;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(user.name),
),
body: Center(
child: Text('Welcome, ${user.name}'),
),
);
}
}
責任の分離をするために、データ取得用のロジックを外部のクラスに委譲し、UserProfilePage
ではUI表示に専念させることができます。
例えば、ProviderやRiverpodを使用して、データ取得と状態管理を専門のクラスに任せます。
class UserDataRepository {
Future<User> fetchUserData() async {
await Future.delayed(Duration(seconds: 2));
return User("Tanaka");
}
}
final userProvider = FutureProvider<User>((ref) async {
return await fetchUserData();
});
------------------------------------------------------------
class UserProfilePage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
AsyncValue<User> user = ref.watch(userProvider);
return Scaffold(
appBar: AppBar(title: Text('User Profile')),
body: user.when(
loading: () => Center(child: CircularProgressIndicator()),
error: (err, stack) => Center(child: Text('Error: $err')),
data: (user) => Center(child: Text('Welcome, ${user.name}')),
),
);
}
}
この設計では、UserDataRepository
がデータ取得の責任を担い、UI層 (UserProfilePage
) は表示の責任のみを持ちます。
これにより、各クラスが単一の責任を持ち、変更が他の機能に影響を与えるリスクを最小限に抑えることができるという単一責任原則に基づいた設計の例となります。