概要
疎結合、密結合についてそれぞれ内容を整理する。
この記事を見て理解できること
- 密結合とは何か
- 疎結合とは何か
- 密結合が良くない理由
密結合とは
密結合は、異なるコンポーネント(クラスやモジュール)が強く依存し合っている状態を指します。
密結合になっている例
例えば、Recycler Viewを使用してそれぞれのitemでクリックイベントを用意したいケースがあるとします。
また、その場合の密結合になっているコード例は以下です。
class MyAdapter(
private val items: List<String>,
private val fragment: MyFragment // Fragmentに依存
) : RecyclerView.Adapter<MyAdapter.ViewHolder>() {
class ViewHolder(val binding: ItemViewBinding) : RecyclerView.ViewHolder(binding.root)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val binding = ItemViewBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return ViewHolder(binding)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.binding.textView.text = items[position]
// Fragmentのメソッドを直接呼び出してクリックイベントを処理
holder.binding.root.setOnClickListener {
(fragment as MyFragment).onItemClicked(items[position])
}
}
override fun getItemCount(): Int = items.size
}
class MyFragment : Fragment() {
private lateinit var adapter: MyAdapter
private var _binding: FragmentMyBinding? = null
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = FragmentMyBinding.inflate(inflater, container, false)
val items = listOf("Item 1", "Item 2", "Item 3")
// Fragment自体をAdapterに渡してインスタンス化(密結合)
adapter = MyAdapter(items, this)
binding.recyclerView.adapter = adapter
return binding.root
}
// Adapterから直接呼ばれるメソッド
fun onItemClicked(item: String) {
println("Tight Coupling")
}
}
上記からわかる通り、AdapterがFragmentに直接依存しており、Adapterの内部でFragmentのメソッドを直接呼び出してます。
これによる弊害については例えば以下が挙げられます。
- Fragmentの
onItemClicked()
を変更した時、Adapter側も修正が必要になる - 違うFragmentで
MyAdapter
を使用する場合、都度、setOnClickListner
を修正しなければならない
このことから、密結合は再利用性の低下につながります。
疎結合とは
疎結合とは「コンポーネント同士の結合が緩く、独立している」ことを指します。実装をイメージすると「実態を知らなくても利用できる」状態のことを指します。
→ここでのコンポーネント同士の結合がゆるいとは実装に依存せず、インターフェースに依存する結合関係のことを指します。
疎結合できている例
上記の例を疎結合する場合、以下のコードになります。
interface OnItemClickListener {
fun onItemClicked(item: String)
}
class MyAdapter(
private val items: List<String>,
private val listener: OnItemClickListener // インターフェースを通じてイベントを通知
) : RecyclerView.Adapter<MyAdapter.ViewHolder>() {
class ViewHolder(val binding: ItemViewBinding) : RecyclerView.ViewHolder(binding.root)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val binding = ItemViewBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return ViewHolder(binding)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.binding.textView.text = items[position]
// インターフェースを使用してクリックイベントを外部に通知
holder.binding.root.setOnClickListener {
listener.onItemClicked(items[position])
}
}
override fun getItemCount(): Int = items.size
}
class MyFragment : Fragment(), OnItemClickListener {
private lateinit var adapter: MyAdapter
private var _binding: FragmentMyBinding? = null
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = FragmentMyBinding.inflate(inflater, container, false)
val items = listOf("Item A", "Item B", "Item C")
// Fragmentがインターフェースを実装し、Adapterに渡す(疎結合)
adapter = MyAdapter(items, this)
binding.recyclerView.adapter = adapter
return binding.root
}
// インターフェースのメソッドを実装
override fun onItemClicked(item: String) {
println("Tight Coupling")
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
上記では、AdapterとFragmentがインターフェースを介してやり取りしているため、疎結合になっており、双方を独立して変更可能になってます。
密結合が良くない理由
上記の説明でもありましたが、密結合によるデメリットをまとめると以下が挙げられます。
密結合のデメリット
-
メンテナンスが困難になる
- モジュール同士が強く依存しているため、1つのコンポーネントに変更を加えると他の部分にも影響が及びやすい
- 再利用性の低下
-
柔軟性と拡張性の欠如
- システムやアプリケーションの一部を変更、拡張したり、新機能を追加する場合、密結合された部分は他の部分に大きく依存しているため、簡単に拡張することが難しい(例に記載した通り)
-
単体テストが難しくなる
- コンポーネントが互いに依存し合うと、単独でのテストが難しくなり、テスト範囲が広がるため、テストの複雑性が増す
まとめ
今回DIの説明等はなかったですが、密結合を避けるために、設計時には「疎結合」(loose coupling)を目指し、以下のアプローチを採用することが重要です。
- 依存性注入(Dependency Injection)
- インターフェースや抽象クラスの使用(今回の内容)
- モジュール化やコンポーネントベースの設計