A good motivating example for default methods is in the Java standard library, where you now have
list.sort(ordering);
instead of
Collections.sort(list, ordering);
I don't think they could have done that otherwise without more than one identical implementation of List.sort
.
First, let's rename the repository and interface, and then we can talk about why creating an additional layer of abstraction (the interface) is beneficial.
The CassandraRepository<T>
interface has a name problem. It has the word "Cassandra" in it, which ties it to a database vendor.
Let's rename this to simply Repository<T>
instead.
public interface Repository<T>
{
void save(T object);
void delete(T object);
}
Now, the AbstractGenericRepository<T>
class has a naming problem too. This is where you should begin coupling your repository classes to a database vendor.
Let's rename this to CassandraRepository<T>
and have it implement the interface:
abstract class CassandraRepository<T> implements Repository<T>
{
private final Session sesion;
Mapper<T> entityMapper;
CassandraRepository(final Session session) {
this.session = session;
final MappingManager manager = new MappingManager(this.session);
this.entityMapper = manager.mapper(getRepositoryClass());
}
protected abstract Class<T> getRepositoryClass();
}
The ReportRepository
class also has a naming problem. It is tied to a database vendor, yet you need specific methods for reports. Rename this class to CassandraReportRepository
and have it implement a new interface: `ReportRepository':
public class CassandraReportRepository extends CassandraRepository<Report> implements ReportRepository
{
...
}
public interface ReportRepository extends Repository<Report>
{
ReportResult run(Report reportToRun);
}
The CassandraReportRepository inherits from an abstract class coupled to a particular database vendor, and this is apparent through how these things are named.
This next bit is why you define an interface for an abstract class:
public class ReportService
{
private final ReportRepository repository;
public ReportService(ReportRepository repository) {
this.repository = repository;
}
// Methods that use this.repository
}
The ReportService has a dependency on the ReportRepository interface, not the database vendor specific abstract class (or even the database vendor specific concrete class. This allows you to isolate this service class in a unit test, pass in a mock report repository, and test the behavior of the service class by itself.
This is one of the main benefits of defining an interface for an abstract class. Loose coupling lends itself to easily testable code. If you decide to switch database vendors, then code should only need to be refactored within your data access layer.
You can define a new class called, say, MongoDbRepository<T>
. You would create a new class called MongoDbReportRepository that implements the ReportRepository
interface. Since the ReportRepository
interface did not change, any object that depends on this interface will also not need to change.
Best Answer
Single method interfaces and functions (or functions type definitions) are almost the same. Function type definitions are just anonymous interfaces. Interfaces and function type definitions both serve the same purpose. And together with their implementation they could be described as mathematical duals. An object (or in this case a single method interface with implemention) is data (state) with behavior and a function is behavior with data, called a closure when data is partially applied. In C# e.g. the compiled IL code is similar in both cases (at least when it is reverse engineered). This and the correspondence to the concept of duals is described by Mark Seemann here. Note that this holds for C# and might be different in other languages or with other compilers.
In object oriented languages the intent of interfaces is to decouple the code and to be able to inverse the control (as in IoC). This gives you lots of benefits like reduced dependencies and therefore maintainability, evolvability, testability etc. In functional programming the same thing is achieved by using functions type definitions. In some languages you can do both.
Major differences is that interfaces are a bit more verbose with some boiler plate code. Functions are more concise. I'm not sure if all IoC Containers can handle functions as parameters. Also and even more important the way of programming might become very different when you go for one or the other depending on the context. If it's not convenient (meaning not idiomatic) in the language to use type definitions e.g. it can make things complicated when working in a team. So a good advice is probably to check the language's coding guide lines and conventions. (I don't know Dart so I can't help you here.)