Bridge pattern

Last updated

The bridge pattern is a design pattern used in software engineering that is meant to "decouple an abstraction from its implementation so that the two can vary independently", introduced by the Gang of Four. [1] The bridge uses encapsulation, aggregation, and can use inheritance to separate responsibilities into different classes.

Contents

When a class varies often, the features of object-oriented programming become very useful because changes to a program's code can be made easily with minimal prior knowledge about the program. The bridge pattern is useful when both the class and what it does vary often. The class itself can be thought of as the abstraction and what the class can do as the implementation. The bridge pattern can also be thought of as two layers of abstraction.

When there is only one fixed implementation, this pattern is known as the Pimpl idiom in the C++ world.

The bridge pattern is often confused with the adapter pattern, and is often implemented using the object adapter pattern; e.g., in the Java code below.

Variant: The implementation can be decoupled even more by deferring the presence of the implementation to the point where the abstraction is utilized.

Overview

The Bridge design pattern is one of the twenty-three well-known GoF design patterns that describe how to solve recurring design problems to design flexible and reusable object-oriented software, that is, objects that are easier to implement, change, test, and reuse. [1]

What problems can the Bridge design pattern solve? [2]

When using subclassing, different subclasses implement an abstract class in different ways. But an implementation is bound to the abstraction at compile-time and cannot be changed at run-time.

What solution does the Bridge design pattern describe?

This enables to configure an Abstraction with an Implementor object at run-time.
See also the Unified Modeling Language class and sequence diagram below.

Structure

UML class and sequence diagram

A sample UML class and sequence diagram for the Bridge design pattern. W3sDesign Bridge Design Pattern UML.jpg
A sample UML class and sequence diagram for the Bridge design pattern.

In the above Unified Modeling Language class diagram, an abstraction (Abstraction) is not implemented as usual in a single inheritance hierarchy. Instead, there is one hierarchy for an abstraction (Abstraction) and a separate hierarchy for its implementation (Implementor), which makes the two independent from each other. The Abstraction interface (operation()) is implemented in terms of (by delegating to) the Implementor interface (imp.operationImp()).
The UML sequence diagram shows the run-time interactions: The Abstraction1 object delegates implementation to the Implementor1 object (by calling operationImp() on Implementor1), which performs the operation and returns to Abstraction1.

Class diagram

Bridge UML class diagram.svg

Abstraction (abstract class)
defines the abstract interface
maintains the Implementor reference.
RefinedAbstraction (normal class)
extends the interface defined by Abstraction
Implementor (interface)
defines the interface for implementation classes
ConcreteImplementor (normal class)
implements the Implementor interface
Bridge in LePUS3 (legend) Bridge pattern in LePUS3.1.gif
Bridge in LePUS3 (legend)

Example

C#

Bridge pattern compose objects in tree structure. It decouples abstraction from implementation. Here abstraction represents the client from which the objects will be called. An example implemented in C# is given below

// Helps in providing truly decoupled architecturepublicinterfaceIBridge{voidFunction1();voidFunction2();}publicclassBridge1:IBridge{publicvoidFunction1(){Console.WriteLine("Bridge1.Function1");}publicvoidFunction2(){Console.WriteLine("Bridge1.Function2");}}publicclassBridge2:IBridge{publicvoidFunction1(){Console.WriteLine("Bridge2.Function1");}publicvoidFunction2(){Console.WriteLine("Bridge2.Function2");}}publicinterfaceIAbstractBridge{voidCallMethod1();voidCallMethod2();}publicclassAbstractBridge:IAbstractBridge{publicIBridgebridge;publicAbstractBridge(IBridgebridge){this.bridge=bridge;}publicvoidCallMethod1(){this.bridge.Function1();}publicvoidCallMethod2(){this.bridge.Function2();}}

The Bridge classes are the Implementation that uses the same interface-oriented architecture to create objects. On the other hand, the abstraction takes an instance of the implementation class and runs its method. Thus, they are completely decoupled from one another.

Crystal

abstractclassDrawingAPIabstractdefdraw_circle(x:Float64,y:Float64,radius:Float64)endclassDrawingAPI1<DrawingAPIdefdraw_circle(x:Float,y:Float,radius:Float)"API1.circle at #{x}:#{y} - radius: #{radius}"endendclassDrawingAPI2<DrawingAPIdefdraw_circle(x:Float64,y:Float64,radius:Float64)"API2.circle at #{x}:#{y} - radius: #{radius}"endendabstractclassShapeprotectedgetterdrawing_api:DrawingAPIdefinitialize(@drawing_api)endabstractdefdrawabstractdefresize_by_percentage(percent:Float64)endclassCircleShape<Shapegetterx:Float64gettery:Float64getterradius:Float64definitialize(@x,@y,@radius,drawing_api:DrawingAPI)super(drawing_api)enddefdraw@drawing_api.draw_circle(@x,@y,@radius)enddefresize_by_percentage(percent:Float64)@radius*=(1+percent/100)endendclassBridgePatterndefself.testshapes=[]ofShapeshapes<<CircleShape.new(1.0,2.0,3.0,DrawingAPI1.new)shapes<<CircleShape.new(5.0,7.0,11.0,DrawingAPI2.new)shapes.eachdo|shape|shape.resize_by_percentage(2.5)putsshape.drawendendendBridgePattern.test

Output

API1.circle at 1.0:2.0 - radius: 3.075 API2.circle at 5.0:7.0 - radius: 11.275 

C++

#include<iostream>#include<string>#include<vector>classDrawingAPI{public:virtual~DrawingAPI()=default;virtualstd::stringDrawCircle(floatx,floaty,floatradius)const=0;};classDrawingAPI01:publicDrawingAPI{public:std::stringDrawCircle(floatx,floaty,floatradius)constoverride{return"API01.circle at "+std::to_string(x)+":"+std::to_string(y)+" - radius: "+std::to_string(radius);}};classDrawingAPI02:publicDrawingAPI{public:std::stringDrawCircle(floatx,floaty,floatradius)constoverride{return"API02.circle at "+std::to_string(x)+":"+std::to_string(y)+" - radius: "+std::to_string(radius);}};classShape{public:Shape(constDrawingAPI&drawing_api):drawing_api_(drawing_api){}virtual~Shape()=default;virtualstd::stringDraw()const=0;virtualfloatResizeByPercentage(constfloatpercent)=0;protected:constDrawingAPI&drawing_api_;};classCircleShape:publicShape{public:CircleShape(floatx,floaty,floatradius,constDrawingAPI&drawing_api):Shape(drawing_api),x_(x),y_(y),radius_(radius){}std::stringDraw()constoverride{returndrawing_api_.DrawCircle(x_,y_,radius_);}floatResizeByPercentage(constfloatpercent)override{returnradius_*=(1.0f+percent/100.0f);}private:floatx_,y_,radius_;};intmain(intargc,char**argv){constDrawingAPI01api1{};constDrawingAPI02api2{};std::vector<CircleShape>shapes{CircleShape{1.0f,2.0f,3.0f,api1},CircleShape{5.0f,7.0f,11.0f,api2}};for(auto&shape:shapes){shape.ResizeByPercentage(2.5);std::cout<<shape.Draw()<<std::endl;}return0;}

Output:

API01.circle at 1.000000:2.000000 - radius: 3.075000 API02.circle at 5.000000:7.000000 - radius: 11.275000 

Java

The following Java program defines a bank account that separates the account operations from the logging of these operations.

// Logger has two implementations: info and warning@FunctionalInterfaceinterfaceLogger{voidlog(Stringmessage);staticLoggerinfo(){returnmessage->System.out.println("info: "+message);}staticLoggerwarning(){returnmessage->System.out.println("warning: "+message);}}abstractclassAbstractAccount{privateLoggerlogger=Logger.info();publicvoidsetLogger(Loggerlogger){this.logger=logger;}// the logging part is delegated to the Logger implementationprotectedvoidoperate(Stringmessage,booleanresult){logger.log(message+" result "+result);}}classSimpleAccountextendsAbstractAccount{privateintbalance;publicSimpleAccount(intbalance){this.balance=balance;}publicbooleanisBalanceLow(){returnbalance<50;}publicvoidwithdraw(intamount){booleanshouldPerform=balance>=amount;if(shouldPerform){balance-=amount;}operate("withdraw "+amount,shouldPerform);}}publicclassBridgeDemo{publicstaticvoidmain(String[]args){SimpleAccountaccount=newSimpleAccount(100);account.withdraw(75);if(account.isBalanceLow()){// you can also change the Logger implementation at runtimeaccount.setLogger(Logger.warning());}account.withdraw(10);account.withdraw(100);}}

It will output:

info: withdraw 75 result true warning: withdraw 10 result true warning: withdraw 100 result false 

PHP

interfaceDrawingAPI{functiondrawCircle($x,$y,$radius);}classDrawingAPI1implementsDrawingAPI{publicfunctiondrawCircle($x,$y,$radius){echo"API1.circle at $x:$y radius $radius.\n";}}classDrawingAPI2implementsDrawingAPI{publicfunctiondrawCircle($x,$y,$radius){echo"API2.circle at $x:$y radius $radius.\n";}}abstractclassShape{protected$drawingAPI;publicabstractfunctiondraw();publicabstractfunctionresizeByPercentage($pct);protectedfunction__construct(DrawingAPI$drawingAPI){$this->drawingAPI=$drawingAPI;}}classCircleShapeextendsShape{private$x;private$y;private$radius;publicfunction__construct($x,$y,$radius,DrawingAPI$drawingAPI){parent::__construct($drawingAPI);$this->x=$x;$this->y=$y;$this->radius=$radius;}publicfunctiondraw(){$this->drawingAPI->drawCircle($this->x,$this->y,$this->radius);}publicfunctionresizeByPercentage($pct){$this->radius*=$pct;}}classTester{publicstaticfunctionmain(){$shapes=array(newCircleShape(1,3,7,newDrawingAPI1()),newCircleShape(5,7,11,newDrawingAPI2()),);foreach($shapesas$shape){$shape->resizeByPercentage(2.5);$shape->draw();}}}Tester::main();

Output:

API1.circle at 1:3 radius 17.5 API2.circle at 5:7 radius 27.5

Scala

traitDrawingAPI{defdrawCircle(x:Double,y:Double,radius:Double)}classDrawingAPI1extendsDrawingAPI{defdrawCircle(x:Double,y:Double,radius:Double)=println(s"API #1 $x$y$radius")}classDrawingAPI2extendsDrawingAPI{defdrawCircle(x:Double,y:Double,radius:Double)=println(s"API #2 $x$y$radius")}abstractclassShape(drawingAPI:DrawingAPI){defdraw()defresizePercentage(pct:Double)}classCircleShape(x:Double,y:Double,varradius:Double,drawingAPI:DrawingAPI)extendsShape(drawingAPI:DrawingAPI){defdraw()=drawingAPI.drawCircle(x,y,radius)defresizePercentage(pct:Double){radius*=pct}}objectBridgePattern{defmain(args:Array[String]){Seq(newCircleShape(1,3,5,newDrawingAPI1),newCircleShape(4,5,6,newDrawingAPI2))foreach{x=>x.resizePercentage(3)x.draw()}}}

Python

"""Bridge pattern example."""fromabcimportABCMeta,abstractmethodNOT_IMPLEMENTED="You should implement this."classDrawingAPI:__metaclass__=ABCMeta@abstractmethoddefdraw_circle(self,x,y,radius):raiseNotImplementedError(NOT_IMPLEMENTED)classDrawingAPI1(DrawingAPI):defdraw_circle(self,x,y,radius):returnf"API1.circle at {x}:{y} - radius: {radius}"classDrawingAPI2(DrawingAPI):defdraw_circle(self,x,y,radius):returnf"API2.circle at {x}:{y} - radius: {radius}"classDrawingAPI3(DrawingAPI):defdraw_circle(self,x,y,radius):returnf"API3.circle at {x}:{y} - radius: {radius}"classShape:__metaclass__=ABCMetadrawing_api=Nonedef__init__(self,drawing_api):self.drawing_api=drawing_api@abstractmethoddefdraw(self):raiseNotImplementedError(NOT_IMPLEMENTED)@abstractmethoddefresize_by_percentage(self,percent):raiseNotImplementedError(NOT_IMPLEMENTED)classCircleShape(Shape):def__init__(self,x,y,radius,drawing_api):self.x=xself.y=yself.radius=radiussuper(CircleShape,self).__init__(drawing_api)defdraw(self):returnself.drawing_api.draw_circle(self.x,self.y,self.radius)defresize_by_percentage(self,percent):self.radius*=1+percent/100classBridgePattern:@staticmethoddeftest():shapes=[CircleShape(1.0,2.0,3.0,DrawingAPI1()),CircleShape(5.0,7.0,11.0,DrawingAPI2()),CircleShape(5.0,4.0,12.0,DrawingAPI3()),]forshapeinshapes:shape.resize_by_percentage(2.5)print(shape.draw())BridgePattern.test()

See also

Related Research Articles

A visitor pattern is a software design pattern that separates the algorithm from the object structure. Because of this separation new operations can be added to existing object structures without modifying the structures. It is one way to follow the open/closed principle in object-oriented programming and software engineering.

In computer programming, lazy initialization is the tactic of delaying the creation of an object, the calculation of a value, or some other expensive process until the first time it is needed. It is a kind of lazy evaluation that refers specifically to the instantiation of objects or other resources.

In class-based programming, the factory method pattern is a creational pattern that uses factory methods to deal with the problem of creating objects without having to specify the exact class of the object that will be created. This is done by creating objects by calling a factory method—either specified in an interface and implemented by child classes, or implemented in a base class and optionally overridden by derived classes—rather than by calling a constructor.

In software engineering, the composite pattern is a partitioning design pattern. The composite pattern describes a group of objects that are treated the same way as a single instance of the same type of object. The intent of a composite is to "compose" objects into tree structures to represent part-whole hierarchies. Implementing the composite pattern lets clients treat individual objects and compositions uniformly.

In object-oriented programming, the decorator pattern is a design pattern that allows behavior to be added to an individual object, dynamically, without affecting the behavior of other objects from the same class. The decorator pattern is often useful for adhering to the Single Responsibility Principle, as it allows functionality to be divided between classes with unique areas of concern as well as to the Open-Closed Principle, by allowing the functionality of a class to be extended without being modified. Decorator use can be more efficient than subclassing, because an object's behavior can be augmented without defining an entirely new object.

In computer programming, the interpreter pattern is a design pattern that specifies how to evaluate sentences in a language. The basic idea is to have a class for each symbol in a specialized computer language. The syntax tree of a sentence in the language is an instance of the composite pattern and is used to evaluate (interpret) the sentence for a client. See also Composite pattern.

In software design and engineering, the observer pattern is a software design pattern in which an object, named the subject, maintains a list of its dependents, called observers, and notifies them automatically of any state changes, usually by calling one of their methods.

Multiple dispatch or multimethods is a feature of some programming languages in which a function or method can be dynamically dispatched based on the run-time (dynamic) type or, in the more general case, some other attribute of more than one of its arguments. This is a generalization of single-dispatch polymorphism where a function or method call is dynamically dispatched based on the derived type of the object on which the method has been called. Multiple dispatch routes the dynamic dispatch to the implementing function or method using the combined characteristics of one or more arguments.

In object-oriented (OO) and functional programming, an immutable object is an object whose state cannot be modified after it is created. This is in contrast to a mutable object, which can be modified after it is created. In some cases, an object is considered immutable even if some internally used attributes change, but the object's state appears unchanging from an external point of view. For example, an object that uses memoization to cache the results of expensive computations could still be considered an immutable object.

A method in object-oriented programming (OOP) is a procedure associated with an object, and generally also a message. An object consists of state data and behavior; these compose an interface, which specifies how the object may be used. A method is a behavior of an object parametrized by a user.

In mathematics and computer science, a higher-order function (HOF) is a function that does at least one of the following:

In object-oriented programming, in languages such as C++, and Object Pascal, a virtual function or virtual method is an inheritable and overridable function or method for which dynamic dispatch is facilitated. This concept is an important part of the (runtime) polymorphism portion of object-oriented programming (OOP). In short, a virtual function defines a target function to be executed, but the target might not be known at compile time.

In computer science, a tagged union, also called a variant, variant record, choice type, discriminated union, disjoint union, sum type or coproduct, is a data structure used to hold a value that could take on several different, but fixed, types. Only one of the types can be in use at any one time, and a tag field explicitly indicates which one is in use. It can be thought of as a type that has several "cases", each of which should be handled correctly when that type is manipulated. This is critical in defining recursive datatypes, in which some component of a value may have the same type as that value, for example in defining a type for representing trees, where it is necessary to distinguish multi-node subtrees and leaves. Like ordinary unions, tagged unions can save storage by overlapping storage areas for each type, since only one is in use at a time.

<span class="mw-page-title-main">Method overriding</span> Language feature in object-oriented programming

Method overriding, in object-oriented programming, is a language feature that allows a subclass or child class to provide a specific implementation of a method that is already provided by one of its superclasses or parent classes. In addition to providing data-driven algorithm-determined parameters across virtual network interfaces, it also allows for a specific type of polymorphism (subtyping). The implementation in the subclass overrides (replaces) the implementation in the superclass by providing a method that has same name, same parameters or signature, and same return type as the method in the parent class. The version of a method that is executed will be determined by the object that is used to invoke it. If an object of a parent class is used to invoke the method, then the version in the parent class will be executed, but if an object of the subclass is used to invoke the method, then the version in the child class will be executed. This helps in preventing problems associated with differential relay analytics which would otherwise rely on a framework in which method overriding might be obviated. Some languages allow a programmer to prevent a method from being overridden.

The curiously recurring template pattern (CRTP) is an idiom, originally in C++, in which a class X derives from a class template instantiation using X itself as a template argument. More generally it is known as F-bound polymorphism, and it is a form of F-bounded quantification.

C++11 is a version of the ISO/IEC 14882 standard for the C++ programming language. C++11 replaced the prior version of the C++ standard, called C++03, and was later replaced by C++14. The name follows the tradition of naming language versions by the publication year of the specification, though it was formerly named C++0x because it was expected to be published before 2010.

In software engineering, a fluent interface is an object-oriented API whose design relies extensively on method chaining. Its goal is to increase code legibility by creating a domain-specific language (DSL). The term was coined in 2005 by Eric Evans and Martin Fowler.

In object-oriented computer programming, a null object is an object with no referenced value or with defined neutral (null) behavior. The null object design pattern, which describes the uses of such objects and their behavior, was first published as "Void Value" and later in the Pattern Languages of Program Design book series as "Null Object".

In object-oriented design, the chain-of-responsibility pattern is a behavioral design pattern consisting of a source of command objects and a series of processing objects. Each processing object contains logic that defines the types of command objects that it can handle; the rest are passed to the next processing object in the chain. A mechanism also exists for adding new processing objects to the end of this chain.

In software engineering, the Twin pattern is a software design pattern that allows developers to model multiple inheritance in programming languages that do not support multiple inheritance. This pattern avoids many of the problems with multiple inheritance.

References

  1. 1 2 Gamma, Erich; Helm, Richard; Johnson, Ralph; Vlissides, John (1994). Design Patterns: Elements of Reusable Object-Oriented Software . Addison-Wesley. p.  151. ISBN   0-201-63361-2.
  2. "The Bridge design pattern - Problem, Solution, and Applicability". w3sDesign.com. Retrieved 2017-08-12.
  3. "The Bridge design pattern - Structure and Collaboration". w3sDesign.com. Retrieved 2017-08-12.