ඩිපෙන්ඩන්සි ඉන්ජෙක්ෂන් පැටර්න් එක භාවිතා කරන විට ඩිපෙන්ඩන්සීස් හදන්නේ කෙසේද?

Submitted by Kamal Wickramanayake on සඳු, 11/16/2020 - 23:43

In object oriented design of software, the dependency injection pattern (or inversion of control pattern – abbreviated as IoC pattern) is widely used to avoid key classes from instantiating their dependencies but to accept the dependencies from outside. Accordingly, the concern of object usage and the concern of object instantiation are separated. It is a case where "separation of concerns" is seen.

There are other design patterns that deal with the concern of creation. For example, the Gang of Four collection of design patterns describes five creational design patterns (Singleton, Factory Method, Abstract Factory, Builder and Prototype).  For your better understanding, here is how GoF puts forward the creational design patterns:

"Creational design patterns abstract the instantiation process. They help make the system independent of how its objects are created, composed, and represented."

You may have come across other creational patterns as well. The reason why we have different "creational patterns" is that they exhibit different qualities so we may prefer one creational pattern over the other based on the uniqueness of the problem and the context.

At times, one may find a creational part of a system that should instantiate objects only from a limited number of classes. But in some other cases, the types of objects that a creational part of a system should instantiate may change over time. Here is an example:

// Code from http://stackoverflow.com/questions/24032453/what-is-this-design-pattern

interace PaymentGateway {
  void makePayment();
}

class PaypalPaymentGateway implements PaymentGateway {
  public void makePayment() {
    //some implementation
  }
}

class AuthorizeNetPaymentGateway implements PaymentGateway {
  public void makePayment() {
    //some implementation
  }
}

class PaymentGatewayFactory{
  PaymentGateway createPaymentGateway(int gatewayId) {
    if(gatewayId == 1)
      return new PaypalPaymentGateway();
    else if(gatewayId == 2)
      return new AuthorizeNetPaymentGateway();
  }
}

Here, the PaymentGatewayFactory class abstracts the creation process of PaymentGateways. How good is this abstraction? Does it sufficiently fulfill the needs? Is there room for improvement?

One consideration is to determine whether you need to always return "new" instances of PaymentGateways. If you prefer good abstractions, you may not want to instantiate many PaypalPaymentGateways. Paypal represents a service and you don't want many Paypal services, do you? So one improvement is to alter the factory class to instantiate each type of PaymentGateway only once, hold their references internally and return such cached instances when the createPaymentGateway() method is called repeatedly.

Importantly, supporting new PaymentGateways in future is a requirement. At the moment, only Paypal and AnuthrizeNet are supported. Removal of some PaymentGateways from the existing list is also a requirement. Hence the abstraction that we expect from the PaymentGatewayFactory should ideally possess these two qualities: extensibility and configurability. The above coding of the PaymentGatewayFactory class does not possess them. So to support new PaymentGateways or to remove existing PaymentGateways from the list, we need to modify the PaymentGatewayFactory class. This violates the commonly known open/closed principle - which states that existing code should not require modifications when you make modifications to the ultimate behaviour of software (Your current code should allow the behaviour change by extension - for example by adding new classes and reconfiguring the software).

Generalized, our focus here is system modifiability. We have a requirement that states that our system should be modifiable to support new payment gateways or change them. What's the best way to achieve system modifiability? Should the developer leave the code as indicated above or should (s)he add extensibility and configurability to the PaymentGatewayFactory class?

Violate the open/closed principle

One may decide to modify the PaymentGatewayFactory class when it is time for a change to the supported list of PaymentGateways. This doesn't add extensibility and configurability to the PaymentGatewayFactory class but depends on modifications to achieve those qualities. This strategy violates the open/closed principle. If the cost of doing so is within someone's acceptable limits and is the lowest of all options available, this current design should be considered the best. One should keep in mind that the costs not only include code modification and recompilation of the PaymentGatewayFactory class but may include other aspects as well (testing, deployment costs, costs due to system down times,...). Still, they may be within acceptable limits. You need to analyze your case and decide.

At least, the above coding of the PaymentGatewayFactory class has localized and contained the scope of modification to the createPaymentGateway() method (* This is not very correct though. You see that from somewhere else the gatewayIds should be passed to the method. So some other modifications are also needed. Let's not worry about that for the time being and limit our discussion.).

Improve the PaymentGatewayFactory class

In case adding extensibility and configurability to the PaymentGatewayFactory class (as inherent qualities) is deemed more beneficial in a certain case, one needs to look at best ways to do so. Then again, there are different paths to consider.

Use Factory Method design pattern:

One solution would be to apply the Factory Method design pattern to the PaymentGatewayFactory (i.e. creation of the factory itself is abstracted). So one would write newer factories as modifications to PaymentGateways take place. I wouldn't elaborate much on this since this option is not ideal for this specific example. You need to keep in mind that someone still needs to make modifications to the place where the ultimate factory is instantiated (to replace the existing factory with a newer factory).

Use dependency injection within PaymentGatewayFactory:

Another solution would be to alter the PaymentGatewayFactory class to allow external injection of supported PaymentGateway instances. PaymentGateways should be instantiated somewhere else. The createPaymentGateway(int gatewayId) method needs to match the IDs and return the appropriate PaymentGateway instance (from the externally injected set).

Here, the PaymentGatewayFactory class should have the ability to maintain the externally injected set of PaymentGateways and know the association between gatewayId and the corresponding PaymentGateway instance. So something like a hash map or an associative array can be used.

But then again, in some other class you have to code the instantiation of supported PaymentGateways and that class needs modifications when the supported list of PaymentGateways change. Oops! Our attempt to not violate the open/closed principle fails again. Still, you may prefer this option if your PaymentGatewayFactory has additional responsibilities (like caching and fault prevention) and for that reason you are reluctant to change it often. The other class where the actual instantiation takes place becomes more like a configuration means.

Use reflection:

We can alter the PaymentGatewayFactory to accept the names of PaymentGateway classes and use the reflection capabilities of the programming language to instantiate classes from the names. This is another solution. In this case, instantiation of PaymentGateways happens inside the PaymentGatewayFactory (no dependency injection as objects, but class names are injected). Somewhere else, PaymentGatewayFactory should be initialized by passing PaymentGateway class names.

Bad side is that we need to deal with the reflection APIs of the programming language that makes the programs relatively difficult to debug. IDEs and compilers fail to hint us the problems in our reflection code since reflection APIs avoid compile time type checking.

Use a dependency injection infrastructure:

May be dealing with the reflection API is too much of work for us. Why not we leverage an already popular piece of software for that purpose? We can code the PaymentGatewayFactory to accept injected dependencies and leave the reflection API based instantiation of dependencies and injection of them with infrastructure software. For example in Java, one can depend on Spring Framework where in an XML configuration file we specify the class names of dependencies (with IDs if you wish) and wire the objects (configure a PaymentGatewayFactory instance to accept instances of different PaymentGateways).

If you don't like the Spring Framework for whatever the reason (may be you are not using Java), you can still write some reusable classes to do similar work (where from a configuration file you read the PaymentGateway class names, use reflection APIs to instantiate and initialize them). But you go for this option if you find "reusability" of a set of such classes valuable (meaning that you are interested in using those classes in many software you write).

Now we don't have to violate the open/closed principle. If we need a modification to the supported list of PaymentGateways, we may write new PaymentGateway classes if required. But we don't modify existing classes. We make the change by altering the configuration file.

Summary

In short, you need to keep in mind that whether you use dependency injection for you key classes or not, whether you write extensible and configurable classes or not, you still need to instantiate all of them (key classes as well as dependencies). Frequency of changes to key class instantiation may be low since key classes represent key abstractions of a system that may not change easily. Frequency of changes to dependent class instantiation is relatively high. Still, all such instantiation logic can change.

Dependency injection infrastructures like Spring Framework allow most instantiation related work to be carried out by them - irrespective of whether your focus is on key classes or dependencies. Still, there are costs of using such frameworks. Some costs may not be in your favor for a given application or a particular context.

A good designer evaluates the costs and benefits of changes and determines some changes to happen via code modifications and some changes to happen via configuration changes and some changes to happen via a combination of code and configuration changes. If everything can be changed only by configuration changes, that's ideal only if we have such configurable software in hand. But writing such configurable software requires significant effort. Hence, "everything configurable" software turns to be not practical and not ideal.

What about principles like open/closed principle? Well, they are "accepted as true" or "accepted as good" generalizations. They are good to argue and create basis for reasoning. But "meet expectations with the least cost and effort" is a better accepted "principle" than any other principle.

Knowledge of extrinsic and intrinsic costs and benefits of options and trade-off analysis are key knowledge and skill areas that software professionals should master. Oops! It's applicable everywhere - not just in software development. I should stop writing this now!