Dependency Injection (DI) සිංහලෙන්: Spring Boot & Java SC Guide

Dependency Injection (DI) සිංහලෙන්: Spring Boot & Java SC Guide

Dependency Injection (DI) සිංහලෙන්: Spring Boot සහ Java එක්ක වැඩේ ගොඩ දාගමු!

මචන්ලා, කොහොමද ඉතින්? අද අපි කතා කරන්න යන්නේ ඔයාලා හැමෝටම වැදගත් වෙන, Software Engineering ලෝකේ ජනප්‍රිය Concept එකක් ගැන. ඒ තමයි Dependency Injection (DI) කියන එක. ගොඩක් අය මේක අහලා තිබ්බට, හරියටම මොකක්ද මේකෙන් වෙන්නේ, ඇයි මේක පාවිච්චි කරන්නේ කියලා හරියටම දන්නේ නැති වෙන්න පුළුවන්. විශේෂයෙන්ම Spring Framework එකත් එක්ක වැඩ කරනවා නම්, DI කියන්නේ නැතුවම බෑ!

අපි මේක බොක්කෙන්ම කියනවා නම්, ඔයා ලියන Code එක Clean, Maintainable, Testable කරගන්න තියෙන සුපිරිම විදියක් තමයි DI. එහෙනම් අපි යමු බලන්න මේක කොහොමද වැඩ කරන්නේ කියලා. අද මේ Article එකෙන් අපි DI කියන්නේ මොකක්ද, ඒකේ තියෙන වර්ග මොනවද, Spring Boot එකත් එක්ක ඒක කොහොමද පාවිච්චි කරන්නේ වගේම, පොඩි Java project එකක DI implement කරන හැටිත් බලමු.

Dependency Injection (DI) කියන්නේ මොකක්ද?

සරලවම කිව්වොත්, Dependency Injection කියන්නේ Software Design Pattern එකක්. මේකෙන් කරන්නේ, Object එකක් තමන්ට අවශ්‍ය වෙන තව Object එකක් (ඒ කියන්නේ Dependency එකක්) තනියම හදාගන්නේ නැතුව, කවුරුහරි එළියෙන් ඒක Object එකට දීලා යවන එක.

ටිකක් පැහැදිලි කරන්නම්. හිතන්න ඔයාට Class එකක් තියෙනවා Car කියලා. මේ Car එකට දුවන්න Engine එකක් ඕනේ. සාමාන්‍ය විදියට නම්, ඔයා Car Class එක ඇතුලේම Engine Object එකක් හදාගන්නවා නේද? ඒ කියන්නේ:

public class Car {
    private Engine engine;

    public Car() {
        this.engine = new Engine(); // Car එකම Engine එක හදාගන්නවා
    }

    public void start() {
        engine.start();
        System.out.println("Car started!");
    }
}

මෙහිදී, Car Class එක Engine Class එකට Dependent වෙනවා. මේකෙදි තියෙන ප්‍රශ්නේ තමයි, Car එකට වෙනම Engine වර්ගයක් (උදා: Electric Engine) යොදන්න ඕන වුණොත්, අපිට Car Class එකේ Code එක වෙනස් කරන්න වෙනවා. ඒ වගේම, Car Class එක Test කරනකොට, අපිට Engine එකත් එක්කම Test කරන්න වෙනවා. ඒ කියන්නේ, Car එක Engine එකට තදින් බැඳිලා (tightly coupled) තියෙනවා.

DI වලදී වෙන්නේ මේ වැඩේ වෙනස් විදියකට කරන එක. Car Class එකට Engine එකක් ඕන වුණාට, ඒක හදාගන්න එක Car එකට භාර දෙන්නේ නැහැ. වෙන කවුරුහරි (එහෙමත් නැත්නම් DI Container එකක්) Engine Object එක හදලා Car එකට දීලා යවනවා. ඒක හරියට Factory එකකින් කාර් එකක් හදනකොට, Car body එකට Engine එක සවි කරලා දෙනවා වගේ වැඩක්.

public class Car {
    private Engine engine;

    // Constructor Injection
    public Car(Engine engine) {
        this.engine = engine; // Car එකට Engine එක එළියෙන් දෙනවා
    }

    public void start() {
        engine.start();
        System.out.println("Car started!");
    }
}

// මේක තමා injector
public class Application {
    public static void main(String[] args) {
        Engine petrolEngine = new PetrolEngine(); // හෝ new ElectricEngine();
        Car myCar = new Car(petrolEngine);
        myCar.start();
    }
}

දැන් Car Class එක Engine Class එක හදන හැටි දන්නෙ නෑ. ඒක දන්නේ තමන්ට Engine එකක් අවශ්‍යයි කියලා විතරයි. මේක තමයි Loose Coupling කියන්නේ.

ඇයි අපි DI පාවිච්චි කරන්නේ? (Benefits of DI)

DI පාවිච්චි කරන එකෙන් අපිට ගොඩක් වාසි තියෙනවා:

  1. Loose Coupling: කලින් කිව්වා වගේ, Class එකක් තවත් Class එකක් එක්ක තදින් බැඳිලා නැහැ. ඒ නිසා, එක Class එකක වෙනසක් කරනකොට, අනිත් Class වලට බලපාන්නේ නැහැ.
  2. Easier Testing: මේක පට්ටම වාසියක්. අපි Car Class එක Test කරනකොට, අපිට ඇත්ත Engine එකක් ඕනේ නැහැ. අපිට පුළුවන් Mock (ව්‍යාජ) Engine එකක් දීලා Car Class එකේ Code එක හරියට වැඩ කරනවද කියලා Test කරන්න.
  3. Improved Maintainability: Code එක තේරුම් ගන්නත්, වෙනස් කරන්නත් ලේසියි. මොකද හැම dependency එකක්ම පැහැදිලිව define කරලා තියෙන නිසා.
  4. Reusability: Components හෙවත් Class එක Class එක වෙනම හදාගන්න පුළුවන් නිසා, ඒවය Reuse කරන්න ලේසියි.
  5. Readability: Code එක කියවන කෙනෙකුට Class එකකට මොන Dependencies ද ඕනේ කියලා එකපාරටම තේරෙනවා.

Dependency Injection වර්ග (Types of DI)

ප්‍රධාන වශයෙන් DI වර්ග තුනක් තියෙනවා:

1. Constructor Injection

මේක තමයි වැඩිපුරම recommended කරන සහ හොඳම DI විදිය. මෙහිදී, Class එකකට අවශ්‍ය වන Dependencies, එහි Constructor එක හරහා ලබා දෙනවා.

public class CustomerService {
    private CustomerRepository customerRepository;

    // Dependencies Constructor එක හරහා ලබා දෙනවා
    public CustomerService(CustomerRepository customerRepository) {
        this.customerRepository = customerRepository;
    }

    public Customer getCustomerById(String id) {
        return customerRepository.findById(id);
    }
}

// භාවිතය
public class App {
    public static void main(String[] args) {
        CustomerRepository repo = new JpaCustomerRepository(); // හෝ new JdbcCustomerRepository();
        CustomerService service = new CustomerService(repo);
        Customer customer = service.getCustomerById("123");
        System.out.println(customer);
    }
}

වාසි:

  • Class එකක් නිර්මාණය කරන විටම අවශ්‍ය වන Dependencies අනිවාර්යයෙන්ම ලබා දිය යුතුයි. ඒ නිසා Class එකක් හැම වෙලාවෙම Valid State එකක පවතිනවා.
  • Dependencies Immutable (වෙනස් කළ නොහැකි) කරන්න පුළුවන් (final keyword එක පාවිච්චි කරලා).
  • Testing වලට හොඳයි.

අවාසි:

  • Dependencies ගොඩක් තිබ්බොත් Constructor එක දිග වෙනවා (Constructor Hell).

2. Setter Injection

මෙහිදී, Dependencies Class එකක Setter Methods හරහා ලබා දෙනවා.

public class OrderProcessor {
    private InventoryService inventoryService;

    public void setInventoryService(InventoryService inventoryService) {
        this.inventoryService = inventoryService;
    }

    public void processOrder(Order order) {
        if (inventoryService != null) {
            inventoryService.updateStock(order.getProductId(), order.getQuantity());
            System.out.println("Order processed: " + order.getId());
        } else {
            System.out.println("InventoryService not set. Cannot process order.");
        }
    }
}

// භාවිතය
public class App {
    public static void main(String[] args) {
        InventoryService service = new LiveInventoryService();
        OrderProcessor processor = new OrderProcessor();
        processor.setInventoryService(service);
        processor.processOrder(new Order("ORD001", "P001", 2));
    }
}

වාසි:

  • සියලුම Dependencies එකවර ලබා දීම අනිවාර්ය නොවන විට (optional dependencies) සුදුසුයි.
  • Runtime එකේදී Dependencies වෙනස් කිරීමට අවශ්‍ය නම් (කලාතුරකින්).

අවාසි:

  • Dependency එකක් set නොකළහොත් Class එක Invalid State එකක පැවතිය හැක. ඒ නිසා NullPointerException වගේ Errors එන්න පුළුවන්.

3. Field (or Property) Injection

මේක Spring වගේ Frameworks වල ගොඩක් ජනප්‍රියයි. මෙහිදී, Dependencies Class එකේ Fields (variables) වලට කෙලින්ම Inject කරනවා. Java වලදී මේක කරන්නේ Reflection කියන feature එක පාවිච්චි කරලා. Spring වලදී @Autowired annotation එකෙන් මේ වැඩේ ලේසියෙන් කරගන්න පුළුවන්.

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class ProductService {

    @Autowired // Field එකට කෙලින්ම Inject කරනවා
    private ProductRepository productRepository;

    public Product getProductDetails(String id) {
        return productRepository.findById(id);
    }
}

// Spring Application එකකදී මේක auto configure වෙනවා.

වාසි:

  • Code එක ගොඩක් කෙටියි (Concise). Boilerplate code අඩුයි.
  • පහසුයි, ඉක්මනින් implement කරන්න පුළුවන්.

අවාසි:

  • Class එක DI Framework එකකට තදින් බැඳෙනවා (tightly coupled). Framework එකක් නැතුව Test කරන එක අමාරුයි.
  • Dependencies මොනවද කියලා Constructor එකක වගේ පැහැදිලිව පේන්නේ නැහැ. Code Readability අඩු වෙන්න පුළුවන්.
  • Immutable Dependencies හදන්න බැහැ.

ඉතින්, මේ තුනෙන් Constructor Injection එක තමයි Best Practice විදියට recommend කරන්නේ. Field Injection එක පහසු වුණත්, ඒකෙන් Code එකේ Quality එක අඩු වෙන්න පුළුවන්.

Spring Framework එකේ DI

Spring Framework එක DI concept එකේ රජා කියලා කිව්වොත් වැරැද්දක් නැහැ. Spring එකේ තියෙන IoC Container (Inversion of Control Container) තමයි මේ DI වැඩේ බලාගන්නේ. IoC Container එකෙන් Object හදන එක, ඒවාට Dependencies Inject කරන එක වගේ හැම දෙයක්ම AutoManage කරනවා. මේ Object වලට Spring වලදී Beans කියලා කියනවා.

Spring එකේ ප්‍රධාන වශයෙන් DI කරන්න පාවිච්චි කරන Annotations ටිකක් තියෙනවා:

  • @Autowired: මේක තමයි ප්‍රධානම Annotation එක. ඕනෑම dependency එකක් Inject කරන්න මේක පාවිච්චි කරනවා. Constructor, Setter, Field යන තුනටම මේක යොදන්න පුළුවන්.
  • @Qualifier: එකම Interface එකට Implementations කිහිපයක් තියෙනකොට, අපිට අවශ්‍ය Implementation එක Specify කරන්න මේක පාවිච්චි කරනවා.
  • @Value: Configuration Properties Inject කරන්න මේක පාවිච්චි කරනවා.

Spring එක්ක DI: උදාහරණ

1. Constructor Injection with Spring

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.stereotype.Repository;

// Interface
interface PaymentGateway {
    void processPayment(double amount);
}

// Implementations
@Repository
class PayPalGateway implements PaymentGateway {
    @Override
    public void processPayment(double amount) {
        System.out.println("Processing PayPal payment: " + amount);
    }
}

@Repository
class StripeGateway implements PaymentGateway {
    @Override
    public void processPayment(double amount) {
        System.out.println("Processing Stripe payment: " + amount);
    }
}

@Service
public class PaymentService {
    private final PaymentGateway paymentGateway;

    @Autowired // Spring auto-inject කරයි
    public PaymentService(@Qualifier("payPalGateway") PaymentGateway paymentGateway) {
        this.paymentGateway = paymentGateway;
    }

    public void makePayment(double amount) {
        paymentGateway.processPayment(amount);
    }
}

මෙහිදී, PaymentService එකට PaymentGateway එකක් අවශ්‍යයි. අපි @Qualifier("payPalGateway") යොදාගෙන තියෙන්නේ, Spring එකට PayPalGateway implementation එක Inject කරන්න කියලා කියන්නයි.

2. Setter Injection with Spring

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class ReportGenerator {
    private DataService dataService;

    @Autowired
    public void setDataService(DataService dataService) {
        this.dataService = dataService;
    }

    public void generateReport() {
        if (dataService != null) {
            System.out.println("Generating report with data: " + dataService.getData());
        } else {
            System.out.println("DataService not set.");
        }
    }
}

// DataService interface සහ implementation
interface DataService {
    String getData();
}

@Component
class DatabaseDataService implements DataService {
    @Override
    public String getData() {
        return "Data from Database";
    }
}

3. Field Injection with Spring

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;

@Controller
public class UserController {

    @Autowired
    private UserService userService;

    public String getUser(String userId) {
        return userService.getUserDetails(userId);
    }
}

// UserService interface සහ implementation
interface UserService {
    String getUserDetails(String userId);
}

@Service
class UserServiceImpl implements UserService {
    @Override
    public String getUserDetails(String userId) {
        return "Details for user: " + userId;
    }
}

නියමයි නේද? Spring Framework එක මේ DI වැඩේ පට්ටම ලේසි කරනවා.

ප්‍රායෝගික අභ්‍යාසය: කුඩා Java Project එකක DI Implement කරමු

දැන් අපි මේ ඉගෙන ගත්තු දේවල් වලින් පොඩි Project එකක් කරලා බලමු.

පියවර 1: Project Setup

අලුතින් Maven හෝ Gradle project එකක් හදාගන්න. Maven නම් pom.xml එකට Spring Boot Starter Dependencies එකතු කරගන්න:

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

පියවර 2: Interfaces සහ Implementations නිර්මාණය කිරීම

අපි සරල MessageService එකක් හදමු, ඒක EmailService සහ SMSService වගේ Dependencies පාවිච්චි කරනවා කියලා හිතමු.

// interfaces
public interface MessageSender {
    void sendMessage(String message, String recipient);
}

// Implementations
@Service("emailSender") // Spring Bean එකක් විදියට register කරනවා
public class EmailService implements MessageSender {
    @Override
    public void sendMessage(String message, String recipient) {
        System.out.println("Sending Email to " + recipient + ": " + message);
    }
}

@Service("smsSender") // Spring Bean එකක් විදියට register කරනවා
public class SMSService implements MessageSender {
    @Override
    public void sendMessage(String message, String recipient) {
        System.out.println("Sending SMS to " + recipient + ": " + message);
    }
}

// Service Class එක
@Service
public class NotificationService {

    private final MessageSender messageSender;

    // Constructor Injection
    @Autowired
    public NotificationService(@Qualifier("emailSender") MessageSender messageSender) {
        this.messageSender = messageSender;
    }

    public void sendNotification(String message, String recipient) {
        messageSender.sendMessage(message, recipient);
    }
}

පියවර 3: Spring Application Class එක

දැන් Spring Application එක Start කරලා අපේ NotificationService එක පාවිච්චි කරමු.

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;

@SpringBootApplication
public class DiDemoApplication {

    public static void main(String[] args) {
        ConfigurableApplicationContext context = SpringApplication.run(DiDemoApplication.class, args);

        // Spring context එකෙන් NotificationService bean එක ගන්නවා
        NotificationService notificationService = context.getBean(NotificationService.class);

        // Notification යවනවා
        notificationService.sendNotification("Hello from DI Demo!", "[email protected]");

        // Application context එක වහනවා
        context.close();
    }
}

මේ Code එක Run කරලා බලන්න. Output එක Sending Email to [email protected]: Hello from DI Demo! කියලා එන්න ඕනේ.

දැන් ඔයාට ඕනේ නම් NotificationService එක SMSService එක පාවිච්චි කරන විදියට වෙනස් කරන්න පුළුවන්. කරන්න තියෙන්නේ @Qualifier එකේ නම "smsSender" විදියට වෙනස් කරන එක විතරයි. එතකොට Code එකේ වෙන කිසිම තැනක් වෙනස් කරන්න ඕනේ නැහැ. නියමයි නේද?

අවසන් වදන්

ඉතින් මචන්ලා, මේ Article එකෙන් Dependency Injection (DI) කියන්නේ මොකක්ද, ඒකේ තියෙන වර්ග මොනවද, Spring Boot එකත් එක්ක ඒක කොහොමද පාවිච්චි කරන්නේ කියලා පැහැදිලිව තේරුණා කියලා හිතනවා. DI කියන්නේ Clean Code ලියන්න සහ Maintainable Software Systems හදන්න නැතුවම බැරි Concept එකක්. Spring Framework එකේ නම් මේක හැමතැනම තියෙනවා.

මේක තේරුම් ගන්න එක ඔයාගේ Software Engineering Skill එකට ලොකු Boost එකක් දෙනවා. අනිවාර්යයෙන්ම මේ Article එකේ තියෙන Exercise එක කරලා බලන්න. ඔයාට ප්‍රශ්න තියෙනවා නම්, Code එකේ මොනවා හරි අවුලක් නම්, පහළින් Comment කරලා කියන්නකෝ. පුළුවන් විදියට උදව් කරන්නම්. එහෙනම් ආයෙත් දවසක තවත් වටිනා Topic එකක් අරගෙන හමුවෙමු! Happy Coding!