API design — in search of excellence

Common design principles

Modularisation: divide and conquer.

* My Scala library does: JSON coding.* The library has a package org.my.library.coder.json that contains all related to JSON encoding.* The package has a trait Encoder that describes methods to encode JSON.* The trait has a method: Encoder.encode[T] that provides encoding objects to JSON.

Naming: a classic nightmare.

  • Short
  • Self-explanatory in a context — so, if you have, for example, an interface org.example.codec.json.Encoder, you don’t have to repeat some part of its name creating a method calling it encodeJson(). In this case, just encode()is good enough
  • Locally unique, to avoid using well-known common names from standard libraries, frameworks to prevent confusion
  • Follows mainstream/defacto/standard development coding styles for your computer language and environment (frameworks, company standards). For example, I’d rather not use “camelCase” for Scala/Java class names, because it is “PascalCase” is a much more common in those languages.
  • Minimising usage of obscure and unknown abbreviations — so, JSON is ok, when HLLL isn’t (of course you might have some corporate abbreviations and they mostly fine if you implement some internal software).

Minimise “conceptual weight” of your API

Encapsulate implementation details

  • access directives from your computer languages (like private/public/protected, import/export, etc).

Be the first user of your own API

import com.example.weather.ForecastServiceval service = new ForecastService()// I want a default implementation of prediction for current date time and some default period
service.predict()
// I want to specify some period of time for prediction
val now = LocalDateTime.now()
service.predict( untilDate = now.plusDays(1) )
// I want to specify temperature units
service.predict(
untilDate = now.plusDays(1),
temperatureUnits = TemperatureUnits.Celcius
)
// Now I want to work with the results and error handling
// Here we deciding to go with standard Either(Right,Left) from
// Scala as a monad for prediction results, so this is also is an
// API decision
service.predict() match {
case Right(prediction) => {
// get an average value of whole period of time and units
assert ( prediction.avg > .0 )
assert (
prediction.temperatureUnits == TemperatureUnits.Celcius
)
assert (
prediction.untilDate != null
)
// get avg by some period of time (for simplicity sake
// we don't specify period units like avg per hour or others)
prediction.values.foreach { pv =>
assert ( pv.avg > .0 )
}
}
case Left(err) => { // error handling
fail(err)
}
}
class A {}A a1 = new A();
A a2 = new A();
a1.equals(a2) // false
class A {  @Override
public boolean equals(Object obj) {
// ???
}
}
  • there is a hidden (yet well-known and documented) contract, that you also have to implement an additional method called hashCode as well
interface Eq<T> {
boolean equalsTo(T other);
}
interface Hashable {
int toHashCode();
}
class HashMap<K,V> 
// where K — is a type of key, and V is a type of value
class HashMap <K extends Hashable & Eq<K>, V>

Be consistent: don’t break your own rules

  • Make the previous rule obsolete, declare old definitions deprecated and provide some way to migrate for legacy code
Circle.draw(canvas, x, y, r)// use the same ordering as before
Rect.draw(canvas, x, y, w, h)
// don't do this, for instance
Rect.draw(x, y, w, h, canvas)
UserProfile (
id : String,
locale : String,
address : String
)
UserProfile(
id : UserId,
locale : Locale,
address : Address
)
case class UserId ( value : String ) extends AnyVal
newtype UserId = UserId String
Locale(
language : Language,
country : Country
)
UserProfile(
id : String,
locale : Locale,
address : Address
)

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store