Circular Dependencies විසඳමු! | Software Engineering Tips Sri Lanka

කට්ටියට කොහොමද යාලුවනේ? ඈතට ගියාම අඳුරන කෙනෙක් පාරේ දැක්කම, "අඩෝ කොහොමද බං වැඩේ?" කියලා කතා කරනවා වගේ තමයි අදත් අපි කතා කරන්න යන්නේ අපේ code එකේ හැංගිලා ඉන්න පොඩි "ප්රශ්නකාරයෙක්" ගැන. මේ පොර තමයි Circular Dependencies. අහලා තියෙනවා නේද? සමහර විට ඔබ දන්නෙම නැතුව ඔබේ code එක ඇතුළෙත් මේ "dependencies චක්රය" කරකැවෙනවා ඇති. හැබැයි මේක අපි හිතනවට වඩා භයානකයි, silent killer කෙනෙක් වගේ අපේ code එක අවුල් කරනවා.
හිතන්නකෝ, ඔබ ලස්සනට software එකක් හදනවා, module එකක් තව module එකකට connect කරනවා, වැඩේ නියමෙට යනවා. හැබැයි ටික කාලයක් යනකොට, පොඩි වෙනසක් කරන්න ගියාම whole system එකම කඩාගෙන වැටෙනවා, tests fail වෙනවා, deploy කරන්න බෑ වගේ ප්රශ්න එනවා. ඔය වගේ වෙලාවට තමයි මේ "Circular Dependencies" කියන මාරයා ඔළුව උස්සන්නේ. අද අපි මේ post එකෙන් බලමු මොනවද මේ Circular Dependencies, ඇයි මේවා අපේ code එකට විෂ වෙන්නේ, කොහොමද මේවා හොයාගන්නේ සහ ඊටත් වඩා වැදගත් දේ, කොහොමද මේ මාරයාගෙන් මිදෙන්නේ කියලා. එහෙනම් වැඩේට බහිමුද?
Circular Dependencies කියන්නේ මොනවද?
සරලව කිව්වොත්, Circular Dependency කියන්නේ module A එක module B මත රඳා පවතිනවා (depends) වගේම, module B එකත් module A මත රඳා පවතින අවස්ථාවක්. හරියට යාලුවෝ දෙන්නෙක් වගේ. අරූ නැතුව මේකට වැඩේ පටන් ගන්න බෑ, මේක නැතුව අරුට වැඩේ පටන් ගන්න බෑ. ඊට පස්සේ දෙන්නම අතරමං වෙනවා වගේ වැඩක් තමයි වෙන්නේ. මේක module දෙකක් අතර වෙන්න පුළුවන්, නැත්නම් තුනක් හතරක් අතර චක්රයක් විදිහට වෙන්නත් පුළුවන් (A -> B -> C -> A).
දැන් හිතන්න, UserService
කියලා class එකක් තියෙනවා, ඒක NotificationService
එකට යූසර් කෙනෙක්ගේ ඊමේල් එකක් යවන්න ඕන වුණාම NotificationService
එකෙන් help ගන්නවා. ඒ කියන්නේ UserService
එක NotificationService
එක මත dependent වෙනවා. ඒ වගේම, NotificationService
එකට අලුත් notification එකක් යවන්න කලින්, යූසර්ගේ profile එක check කරන්න UserService
එකෙන් help ගන්න ඕන වුණා කියලා හිතමු. දැන් NotificationService
එකත් UserService
එක මත dependent වෙනවා. දැන් මොකද වෙන්නේ? දෙන්නම එකිනෙකාට රඳා පවතිනවා. මේක තමයි Circular Dependency එකක්.
ඇයි මේවා අපේ code එකට හොඳ නැත්තේ?
- Build/Compilation Issues: සමහර Programming languages වලදී, Circular Dependencies නිසා code compile වෙද්දී error එන්න පුළුවන්.
- Reduced Modularity: Modules එකිනෙකට තදින් බැඳෙනවා (tightly coupled). Module එකක් තනියම වෙනස් කරන්න හරි, අලුත් තැනක use කරන්න හරි බෑ.
- Hard to Test: Unit testing කරන්න ගියාම මේක මහා වදයක්. Module A test කරන්න module B ඕන, Module B test කරන්න module A ඕන. Mock කරන්නත් අමාරුයි.
- Maintenance Nightmares: පොඩි වෙනසක් කළොත් whole system එකම කඩාගෙන වැටෙන්න පුළුවන්. Bugs හොයාගන්නත් අමාරුයි.
- Reduced Reusability: Modules තනියම use කරන්න බැරි නිසා, code reusability නැති වෙනවා.
අපි කොහොමද මේවා හොයාගන්නේ?
මේ Circular Dependencies හොයාගන්න එක ටිකක් අමාරු වෙන්න පුළුවන්. විශේෂයෙන්ම ලොකු code base එකක. හැබැයි නොයෙක් tools සහ techniques තියෙනවා මේවා identify කරගන්න. අපි බලමු මොනවද ඒවා කියලා:
1. Static Analysis Tools
දැන් නම් මේකට නොයෙක් tools තියෙනවා. මේවා අපේ code එක run කරන්නේ නැතුව analyze කරලා dependencies detect කරනවා. Tools වගේම languages අනුවත් මේවා වෙනස් වෙනවා:
- JavaScript/TypeScript: ESLint (
eslint-plugin-import
වගේ plugins එක්ක), madge, dependency-cruiser. - Python: Mypy, Pylint, dep-tree.
- Java: SonarQube, JArchitect, Maven Enforcer Plugin.
- .NET: NDepend, ReSharper.
මේවා ඔබේ CI/CD pipeline එකට add කරගන්න පුළුවන්. එතකොට code commit කරද්දී automatic මේවා check කරලා errors තියෙනවා නම් දැනුම් දෙනවා.
2. Manual Code Review
ලොකු code base එකකට වඩා පොඩි project වලට මේක ගොඩක් හොඳයි. Code review කරද්දී, import
statements, class initializations, සහ object creations ගැන විශේෂ අවධානයක් දෙන්න. විශේෂයෙන්ම දෙපැත්තටම import
වෙලා තියෙන තැන් ගැන බලන්න. "අඩෝ! මේක මෙහෙටත් import වෙලා, අරක එහෙටත් import වෙලා, එතකොට කොහොමද වැඩේ වෙන්නේ?" කියලා හිතන්න පුරුදු වෙන්න.
3. Test Failures සහ Runtime Errors
සමහර වෙලාවට Circular Dependencies නිසා direct build errors එන්නේ නෑ. හැබැයි run කරද්දී stack overflow errors, undefined behavior, හෝ initialization errors වගේ ඒවා එන්න පුළුවන්. ඒ වගේම unit tests ලියද්දී dependencies mock කරන්න බැරි නම්, ඒකත් Circular Dependency එකක ලක්ෂණයක් වෙන්න පුළුවන්.
4. Dependency Graph Visualization
සමහර tools (madge වගේ) ඔබේ code base එකේ dependency graph එක visualize කරන්න පුළුවන්. එතකොට චක්ර (cycles) පැහැදිලිව පේනවා. මේක හරිම effective ක්රමයක්, මොකද visual එකකින් බලනකොට තත්වය පැහැදිලිව තේරෙනවා.
Circular Dependencies විසඳන ක්රම
දැන් අපි බලමු කොහොමද මේ Circular Dependencies වලින් ගැලවෙන්නේ කියලා. මේවා විසඳන්න නොයෙක් design patterns සහ refactoring techniques තියෙනවා. අපි වඩාත් පොදු සහ effective ක්රම ටිකක් බලමු.
1. Extract a New Module / Interface (නව Module/Interface එකක් නිර්මාණය කිරීම)
මේක තමයි ගොඩක් වෙලාවට use කරන සරලම සහ effectiveම ක්රමය. Modules දෙකක් A සහ B එකිනෙකා මත dependent නම්, ඒ දෙකටම පොදු වෙන functionalities ටිකක් අලුත් module එකකට (C) දාන්න පුළුවන්. ඊට පස්සේ A සහ B දෙකම C මත depend වෙනවා. දැන් A B මත dependent නෑ, Bත් A මත dependent නෑ. දෙන්නම C මත dependent වෙනවා.
ඒ වගේම, Interface එකක් නිර්මාණය කරන්නත් පුළුවන්. UserService
එක NotificationService
එක මතත්, NotificationService
එක UserService
එක මතත් depend නම්, UserNotifier
කියලා Interface එකක් හදන්න. NotificationService
එක UserNotifier
Interface එක implement කරනවා. UserService
එක UserNotifier
Interface එක මත depend වෙනවා. මේක Dependency Inversion Principle (DIP) එකට හොඳ උදාහරණයක්.
2. Dependency Inversion Principle (DIP)
SOLID principles අතරින් DIP කියන්නේ හරිම වැදගත් එකක්. මේකෙන් කියන්නේ "High-level modules should not depend on low-level modules. Both should depend on abstractions. Abstractions should not depend on details. Details should depend on abstractions." කියලා. සරලව කිව්වොත්, අපේ code එක concrete implementations මත depend නොවී, interfaces හෝ abstract classes මත depend වෙන්න ඕන. මේක හරියට "ගණන් නොදැන කරන වැඩක්" වගේ නෙවෙයි, හරියටම දැනගෙන කරන වැඩක්. Circular Dependencies විසඳන්න මේක ගොඩක් උදව් වෙනවා.
උදාහරණයක් විදිහට,
# Problematic code (Conceptual)
# user_service.py
import notification_service
class UserService:
def __init__(self):
self.notifier = notification_service.NotificationService()
def create_user(self, name, email):
print(f"Creating user: {name}")
self.notifier.notify_user_created(email) # UserService depends on NotificationService
# notification_service.py
import user_service # Circular import
class NotificationService:
def __init__(self):
# This will create a circular dependency during initialization if not careful
# self.user_repo = user_service.UserService()
pass
def notify_user_created(self, email):
# Imagine NotificationService also needs to query user details from UserService
# user_details = self.user_repo.get_user_by_email(email)
print(f"Notifying user at {email}")
# main.py
# If you try to run this, you might get issues
# from user_service import UserService
# from notification_service import NotificationService
දැන් අපි මේක DIP use කරලා solve කරමු. අපි Interface එකක් හදමු.
# Resolved code using DIP
# interfaces.py
from abc import ABC, abstractmethod
class UserNotifier(ABC):
@abstractmethod
def notify_user_created(self, email: str):
pass
# notification_service.py
from interfaces import UserNotifier
class EmailNotificationService(UserNotifier):
def notify_user_created(self, email: str):
print(f"Emailing user at {email}: Welcome!")
# user_service.py
from interfaces import UserNotifier
class UserService:
def __init__(self, notifier: UserNotifier): # Inject the dependency
self.notifier = notifier
def create_user(self, name: str, email: str):
print(f"Creating user: {name}")
self.notifier.notify_user_created(email)
# main.py
from user_service import UserService
from notification_service import EmailNotificationService
# Now we can create instances without circular issues
email_notifier = EmailNotificationService()
user_service = UserService(email_notifier) # Inject EmailNotificationService
user_service.create_user("Kasun", "[email protected]")
දැන් බලන්න, UserService
එක concrete EmailNotificationService
එක මත depend නොවී, abstract UserNotifier
Interface එක මත depend වෙනවා. EmailNotificationService
එකත් මේ UserNotifier
Interface එක implement කරනවා. මේකෙන් circularity එක කැඩෙනවා.
3. Event Emitter / Observer Pattern
මේක තවත් effective ක්රමයක්. Modules එකිනෙකාට direct call කරනවා වෙනුවට, events use කරන්න පුළුවන්. Module එකක් යම්කිසි දෙයක් වුණාම event එකක් emit කරනවා, ඊට පස්සේ වෙන modules ඒ event එකට listen කරලා තමන්ගේ වැඩේ කරනවා. මේකෙන් modules අතර coupling එක ගොඩක් අඩු වෙනවා.
උදාහරණයක් විදිහට, OrderService
එකක් Order එකක් create කළාම, InventoryService
එකට ඒක දැනගන්න ඕන නම්, OrderService
එක OrderCreated
කියලා event එකක් emit කරනවා. InventoryService
එක ඒ event එකට listen කරලා තමන්ගේ inventory update කරනවා. මේකෙන් OrderService
එක InventoryService
එක ගැන දැනගන්න ඕන නෑ, InventoryService
එකත් OrderService
එක ගැන දැනගන්න ඕන නෑ. දෙන්නම event mechanism එක ගැන විතරයි දන්නේ.
4. Parameter Passing / Constructor Injection
සමහර වෙලාවට dependencies functions වලට parameters විදිහට pass කිරීමෙන් හෝ class constructor එක හරහා inject කිරීමෙන් Circular Dependencies වලක්වාගන්න පුළුවන්. මේකෙන් dependency එක explicit වෙනවා, සහ control එක inversion වෙනවා.
# Problematic
# module_a.py
import module_b
def process_data_a():
data = "Data from A"
module_b.process_data_b(data)
# module_b.py
import module_a # Potential circular if module_a needs module_b for init/other
def process_data_b(data):
print(f"Processing {data} in B")
# If module_b needs to call something back in module_a...
# module_a.another_func_in_a()
මෙන්න මේක constructor injection හෝ parameter passing වලින් solve කරන විදිහ:
# Resolved using Parameter Passing
# module_a.py
def process_data_a(processor_b): # processor_b is injected
data = "Data from A"
processor_b(data)
# module_b.py
def process_data_b(data):
print(f"Processing {data} in B")
# main.py
import module_a
import module_b
module_a.process_data_a(module_b.process_data_b)
මේකෙන් module A එකට module B එක direct import කරන්න ඕන වෙන්නේ නෑ. B හි function එක parameter එකක් විදිහට pass වෙනවා. මේ වගේ සරල ක්රමවලින් පවා ලොකු ප්රශ්න විසඳගන්න පුළුවන්.
5. Reorganize Code Structure (Code Structure නැවත සකස් කිරීම)
සමහර වෙලාවට Circular Dependency එකක් ඇතිවෙන්නේ code එකේ organization එකේ අවුලක් නිසා. සමහර files වලට අයිති නැති functionalities වෙනත් files වල තියෙන්න පුළුවන්. Dependencies analyze කරලා, functionalities ඒ අදාළ modules වලට move කිරීමෙන්, හෝ common utilities වෙනම module එකකට දැමීමෙන් මේවා විසඳගන්න පුළුවන්. මේක හොඳ architecture design එකකට යන පලමු පියවරක්.
වැළැක්වීම තමයි හොඳම විසඳුම!
Circular Dependencies විසඳනවා වගේම, මේවා ඇතිවීම වළක්වා ගැනීමත් ගොඩක් වැදගත්. අපි මුල ඉඳන්ම හොඳ design practices follow කළොත්, මේ වගේ කරදර වලින් ගැලවෙන්න පුළුවන්.
- Design for Modularity: මුල ඉඳන්ම modules ස්වාධීනව වැඩ කරන්න පුළුවන් විදිහට design කරන්න. හැම module එකකටම තියෙන්න ඕනේ එකම responsibility එකක් (Single Responsibility Principle - SRP).
- Follow SOLID Principles: SOLID principles කියන්නේ software design එකට තියෙන රත්තරන් නීති ටිකක් වගේ. විශේෂයෙන්ම Dependency Inversion Principle (DIP) එකට අවධානය යොමු කරන්න.
- Code Reviews: Regular code reviews කරන්න. Team එකේ අනිත් අයගේ eyes මේවා detect කරන්න උදව් වෙනවා.
- Automated Tools in CI/CD: Static analysis tools ඔබේ build pipeline එකට integrate කරන්න. එතකොට code commit කරද්දී automatic check කරලා warn කරනවා.
- Keep Modules Small and Focused: Module එකක් පොඩි වෙන්න පොඩි වෙන්න, ඒකේ dependencies අඩු වෙනවා. ඒ නිසා circularity එන්න තියෙන ඉඩකඩත් අඩු වෙනවා.
අවසාන වශයෙන්…
Circular Dependencies කියන්නේ අපේ software project එකක performance එකට වගේම maintainability එකටත් ලොකු හානියක් කරන්න පුළුවන් ප්රශ්නයක්. මේවා "silent killers" වගේ ඉඳලා, පස්සේ ලොකු ගැටළු ඇති කරනවා. හැබැයි අපි මේවා ගැන දැනුවත් වෙලා, නිවැරදි methods use කරලා, හොඳ design practices follow කළොත්, මේවාගෙන් මිදෙන්න පුළුවන් වගේම, නියම quality එකක් තියෙන clean, maintainable code එකක් ලියන්නත් පුළුවන්.
මේ article එකෙන් ඔබට Circular Dependencies ගැනත්, ඒවා විසඳන ක්රම ගැනත් හොඳ අවබෝධයක් ලැබෙන්න ඇති කියලා මම හිතනවා. ඔබත් මේ වගේ ප්රශ්න වලට මූණ දීලා තියෙනවා නම්, නැත්නම් මේවා විසඳන්න වෙනත් effective ක්රම තියෙනවා නම්, අනිවාර්යයෙන්ම පහළ comment section එකේ ඔබේ අදහස් සහ අත්දැකීම් අපිත් එක්ක බෙදාගන්න. ඔබේ අදහස් අනිත් අයටත් ගොඩක් වැදගත් වේවි! එහෙනම් තවත් අලුත් දෙයකින් හමුවෙමු, හැමෝටම ජය!