I am developing external library. Assume I have different implementations of logger class, I create abstract class ILogger
which those classes can then implement.
I also have various implementations of log objects that I want to adhere to ILog
abstract class so I expose it as well.
The problem is when my ILogger
uses ILog
as argument to one of its methods. I would assume that any of my implemented logger classes (that implement ILogger
) would accept any arguments that are log classes (which implement ILog
).
See my constrained example below:
abstract class ILog {
const LogImpl({required this.id});
final String id;
}
class Log implements ILog {
const Log({required this.id});
final String id;
}
abstract class ILogger {
void writeLog({
required LogImpl log,
required bool persist,
});
}
class Logger implements ILogger {
void writeLog({
required Log log,
required bool persist,
}) {
print('writing $log with persistence? $persist');
}
}
void main() {
final logger = Logger();
final log = Log(id: 'abcd-1234');
logger.writeLog(log: log, persist: true)
}
For this I get error:
Error: The parameter 'log' of the method 'Logger.writeLog' has type 'Log', which does not match the corresponding type, 'ILog', in the overridden method, 'ILogger.writeLog'.
Is there a way to solve this without resorting to applying generics?
The reason why my log object ILog
needs to be abstract class instead of regular class that is extended is technical. One of my serialization libraries uses source code annotation which means that this annotation cannot be part of the library (because the annotation might be different for different applications).
The program doesn't compile because it's not sound. Dart, and object oriented programming in general, is based around subtype substitutability. If your code works with an instance of a type, it works with an instance of a subtype too - the subtype can substitute for the supertype.
The Logger
's writeLog
method is not a valid override of the ILogger
's writeLog
method. The latter accepts an ILog
as argument, and for the subtype to be able to substitute for that, it needs to accept an ILog
too. However, it only accepts a Log
, which is a subtype, and not any implementation of ILog
.
One alternative is to admit that what you are writing is unsound, and tell the compiler to accept it anyway:
class Logger implements ILogger {
void writeLog({
required covariant Log log,
required bool persist,
}) {
print('writing $log with persistence? $persist');
}
}
Here the covariant
tells the compiler that you know that Log
does not satisfy the superclass parameter type of ILog
, but it's OK. You know what you're doing, and no-one will ever call this function with something which isn't a proper Log
. (And if they do anyway, it'll throw an error).
The other alternative, which is what I'd probably recommend, is to parameterize your classes on the Log
it uses:
abstract class ILogger<L extends ILog> {
void writeLog({
required L log,
required bool persist,
});
}
class Logger implements ILogger<Log> {
void writeLog({
required Log log,
required bool persist,
}) {
print('writing $log with persistence? $persist');
}
}
With that, the Logger
doesn't have to substitute for all ILogger
s, only for ILogger<Log>
, and it can do that soundly.
(Well, as soundly as the inherently unsound covariant generics allow, but the program will compile, and throw if you ever pass something which isn't a Log
instance.)
In both cases the compiler will know that the argument to a Logger
must be a Log
. In both cases, you can fool the program by casting the Logger
to the supertype ILogger
/ILogger<ILog>
, and then pass in an ILog
to writeLog
, but it takes at least a little effort to circumvent the type system.