Software Design Patterns and Principles
An introduction to good object-oriented principles and design patterns every software programmer should be using on a daily basis.
Join the DZone community and get the full member experience.
Join For FreeDesign patterns are a set of templates formulated over a period of time by software architects. They are best practices that software programmers can apply to their own software/application development. They provide incredible advantages by saving time and effort by reusing pre-existing solutions while improving the quality and consistency of the code. It can also help enhance communication and collaboration with other developers, facilitate the adaptation and evolution of your code, and prevent common pitfalls and errors. All in all, design patterns are a great way to streamline your programming process.
Broadly speaking there are three kinds of patterns: structural, creational, and behavioral.
- Creational Patterns are concerned with the creation of an object and separating the object creation from the client that will use the object. Examples include Singleton, Factory, and Factory Method.
- Behavioral Patterns define how multiple objects interact with each other. Command Patterns, Iterator Patterns, and Observer Patterns are some of the common behavioral patterns. The patterns show how different components of the system behave and respond to each other. It can improve the code by enhancing flexibility, modularity, and reusability.
- Structural patterns, on the other hand, deal with the composition and organization of classes and objects. It can hide the complexity of the system by composing classes and objects into larger structures. Examples include Adapter, Composite, Decorator, Facade, and Proxy.
While design patterns are a tried and tested solution, they are formed on the backs of good object-oriented programming principles. Some of the key principles are listed below:
- Program to an interface: Keeps the design flexible.
- Identify the sections of your software that can vary and separate them from the sections that vary: Keep the design modular
- Favor composition over inheritance: Compose the large structure with individual objects.
- Loose coupling: Minimize interdependency on objects.
- The open/closed principle: Class software once code is completed, tested, and validated should be closed for modification but the class is open to extension by means of inheritance.
- Dependency inversion: Depend on abstract or interfaces rather than concrete instantiation of classes.
- Single responsibility principle: A class is responsible for only one requirement.
Example: Proxy Pattern
We chose the Structural Proxy Pattern for our discussion. It is a popular pattern especially used in distributed systems as a Remote Proxy. Proxy Pattern, as the name suggests, is a proxy for someone else. A proxy-designed class provides public-facing APIs that clients use to send requests. The proxy class has the means to access the real object service and can serve the request to it after say authentication of the request from the client. Furthermore, a proxy pattern can allow for lazy initialization. If the real object creation is deemed expensive, we can delay its instantiation only when it is needed by a client. It also allows for logging and caching which can be implemented in the proxy class methods. In the case of distributed systems where the client and the proxy object are on one node and the service real object which services the request is on another node, the proxy pattern allows for a uniform view and the client is unaware of which node is servicing the object.
Figure 1 shows an architecture diagram of the proxy pattern. It has a client class that contains a handle to the ProxyObject. The ProxyClass and RealClass both derive from the iProxy interface class and implement the APIs. To be able to communicate between the two nodes, both additionally have a CommObject which is responsible for the serialization and de-serialization of commands and data when shared over the network. The code snippet provides a minimal set of class and method declarations to visualize how the distributed system would operate together.
// CommClass provides serialization/deserialization to
// to share data and command in a distributed system
class CommClass {
// RealClass registers a callback with the CommClass to receive command
// and data from the proxy
typedef std::function<void(unsigned, vector<unsigned>)> fCallBack;
public:
CommClass() {}
~CommClass() {}
// Proxy Class Uses the CommClass Object to send command and data to the remote service
void sendToRemoteService(unsigned cmd, std::vector<unsigned> data);
// Remote Service Class is notified via event. It uses the registered callback
// to call into the remote service RealClass
void receiveFromProxy(unsigned cmd, std::vector<unsigned> data)
{
_cb(cmd, data);
}
void registerCallBack(fCallBack cb) { _cb = cb; }
private:
fCallBack _cb;
};
// Proxy Interface Class declares the public facing methods
class iProxy {
public:
iProxy() {}
virtual ~iProxy() {}
virtual void methodOne() = 0;
virtual void methodTwo() = 0;
};
// Concrete class which implements the interface class methods and
// lives on the same node as the client
class ProxyClass : public iProxy {
public:
ProxyClass() {};
~ProxyClass() {};
// Implement Interfaces
void methodOne() {};
void methodTwo() {};
private:
CommClass CommObject;
};
// Real Class which implements the Proxy Interface methods and is the service provider
class RealClass : public iProxy {
public:
RealClass() {};
~RealClass() {};
// implement the interfaces
void methodOne() {};
void methodTwo() {};
private:
CommClass CommObject;
};
Conclusion
The example Proxy Pattern is an important arrow in any software engineer's quiver. By using the principle of composition, we can control access to internal objects, add additional logic such as caching and logging around APIs as well add security. It allows for lazy initialization which is useful in a resource-constrained environment. Certain disadvantages such as added overhead and complexity do exist and therefore, this is not a one-size-fits-all solution.
While Proxy Pattern offers a robust solution to many common design challenges, any design pattern should be used with caution to not overcomplicate your design. Always start with the simpler object-oriented principles and then switch to a design pattern when your architecture starts resembling a specific pattern.
Opinions expressed by DZone contributors are their own.
Comments