When should I use an abstract class vs a regular class? More specifically, I'm trying to understand when constructor parameters should be preferred over abstract members.
For example:
sealed trait Client
abstract class BaseService {
def client: Client
}
class Service extends BaseService {
val client = ???
}
vs
sealed trait Client
class BaseService(client: Client) {}
class Service extends BaseService(client = ???)
As far as I can tell, both are valid.
As @Luis Miguel said, use abstract classes when you want to declare a common set of methods that need to be implemented by the sub-classes and/or share some limited functionality that will be used by all child classes.
Below, I'm listing some reasons why I considered you should pass your dependencies to the constructor rather than defining them in your classes/base-classes.
(IMHO) It is better to give the constructor the dependencies it needs to function properly AKA dependency injection.
When you declare a dependency inside your class or in your constructor, you are tightly coupling your Service
with that specific implementation of the dependency, which is not ideal and is considered an antipattern.
Injecting the dependencies gives you greater flexibility as you are not coupled to a specific implementation. This is true as long as your code relies on an interface/trait/abstract-class (consequently avoiding tight coupling).
When your class relies on an interface/trait/abstract-class can be very powerful as you can be passing a mock, a no-op, or different strategies of the client. So make sure you "Program to interfaces, not implementations".