We provide client libraries for our customers in 5 programming languages: Ruby, Python, PHP, C# and Java. Each library is built to help our customers make requests, parse responses and retrieve data from our gateway. We hope they make integrating with Braintree simple and intuitive.
Maintaining 5 client libraries means writing essentially the same functionality in 5 languages. In many cases this just means differences in syntax. However, some features are complicated enough to warrant a slightly different approach in each library.
One example is transaction searching. Because searching can be a bit complex, we decided to create a Domain Specific Language (DSL) in each of the five libraries.
We wanted transaction searching that was both easy to read and deep enough to perform complicated queries. Specifically, we wanted to allow searching on 3 different types of fields:
A collection of resources matching all criteria will be returned.
The code samples below assume the user wants to search for transactions meeting the following criteria:
In Ruby, the search
method yields a search object to a block. This object contains the necessary methods for building search criteria. The block is then executed and the request is built from the result. Ruby also overloads the ==
, !=
, >=
and <=
operators on the text and range search fields. We think this improves readability and reduces syntatic noise.
collection = Braintree::Transaction.search do |search| search.order_id.starts_with "a2d" search.customer_website.ends_with ".com" search.billing_first_name == "John" search.status.in( Braintree::Transaction::Status::Authorized, Braintree::Transaction::Status::Settled ) search.amount.between "10.00", "20.00" end collection.each do |transaction| puts transaction.id end
search
block variableOur Python solution uses a different approach because the language lacks a block syntax and multi-line lambdas. The search
method expects a list of objects representing search criteria. Each of these objects is built inline using readable method names. The search method is then able to iterate over the provided objects to build the search request.
Like Ruby, this implementation overloads the operators ==
, !=
, >
and <
for operations on the text and range fields.
collection = Transaction.search([ TransactionSearch.order_id.starts_with("a2d"), TransactionSearch.customer_website.ends_with(".com"), TransactionSearch.billing_first_name == "John", TransactionSearch.status.in_list([ Transaction.Status.Authorized, Transaction.Status.Settled ]), TransactionSearch.amount.between("10.00", "20.00") ]) for transaction in collection.items: print transaction.id
TransactionSearch
class nameThe PHP implementation is similar to the Python solution described above but feels less readable. Again, the search
method expects a list of search criteria objects and these objects are created inline during the method call. However, the ::
syntax for class method invocation along with the ->
operator for instance method invocation make the code a bit more noisy.
$collection = Braintree_Transaction::search(array( Braintree_TransactionSearch::orderId()->startsWith('a2d'), Braintree_TransactionSearch::customerWebsite()->endsWith('.com'), Braintree_TransactionSearch::billingFirstName()->is('John'), Braintree_TransactionSearch::status()->in(array( Braintree_Transaction::AUTHORIZED, Braintree_Transaction::SETTLED )), Braintree_TransactionSearch::amount()->between('10.00', '20.00') )); foreach($collection as $transaction) { print_r($transaction->id); }
Braintree_TransactionSearch
class name#
In C# we used a fluent interface as the basis for our DSL because it is the idiomatic choice given the language. The fluent interface is used to build a TransactionSearchRequest
object that represents the criteria of the search. Method chaining is used to specify the search criteria in pairs: the name of the field to search on followed by the restrictions on the value of that field. These name/restriction pairs are also chained together.
Notice that now there are two steps to performing the search. We need to build the search criteria before actually preforming the search. C# does allow us to avoid parentheses after the field names through the use of properties.
The TransactionSearchRequest
is then passed to the search
method which returns a collection.
TransactionSearchRequest searchRequest = new TransactionSearchRequest(). OrderId.StartsWith("a2d"). CustomerWebsite.EndsWith(".com"). BillingFirstName.Is("John"). Status.IncludedIn( TransactionStatus.AUTHORIZED, TransactionStatus.SETTLED ). Amount.Between(10.00M, 20.00M); ResourceCollection<Transaction> collection = gateway.Transaction.Search( searchRequest ); foreach (Transaction transaction in collection) { Console.WriteLine(transaction.Id); }
The Java DSL is also based on a fluent interface. Again, the fluent interface is used to build aTransactionSearchRequest
via method chaining.
In Java we see an extra set of parentheses after the search field name because of the lack of properties. We feel this detracts from the readability of the code.
TransactionSearchRequest searchRequest = new TransactionSearchRequest(). orderId().startsWith("a2d"). customerWebsite().endsWith(".com"). billingFirstName().is("John"). status().in( Transaction.Status.AUTHORIZED, Transaction.Status.SETTLED ). amount().between(new BigDecimal("10.00"), new BigDecimal("20.00")); ResourceCollection<Transaction> collection = gateway.transaction().search( searchRequest ); for (Transaction transaction : collection) { System.out.println(transaction.getId()); }
In general, there were 3 types of DSL implementations: fluent interface, readable inline method arguments and blocks. We found fluent interfaces the most effective approach for the static languages. For Python and PHP, readable inline method arguments seemed to be the most idiomatic. The Ruby community tends to prefer block based DSLs and we chose this approach for our Ruby implementation.
It was an incredibly valuable experience to solve the same problem across 5 different languages. Although the results are quite different, we think each solution is robust and readable. And, more importantly, working in these languages helped us to appreciate the strengths and weaknesses of each from a relatively unbiased perspective.
If you’re a developer working in one the these languages, we’d love to hear your feedback. How would you approach this problem in your language of choice?