Also throwing in my two cents on the subclassing discussion. Subclassing itself isn’t bad, but it’s when you get into subclassing types you don’t own is when things get tricky. This can lead to massive headaches if/when the upstream type changes. Speaking from experience here.
A better approach is to decorate the other type, such that you can do something before/after a specific method, or add more methods, all the while not touching anything related to the upstream type itself. In this way you are insulated from internal refactors that do not change the public API, but may break your subclassing, (assuming you are decorating against the public API as well).
HOWEVER, with the way the stdlib is implemented at the moment, this doesn’t work as well as it could since everything is typed as concrete implementations instead of abstractions. I.e. you can’t do something like:
class CustomRequest
getter request : HTTP::Request
forward_missing_to @request
def self.new(method : String, path : String, headers : HTTP::Headers? = nil, body : String | Bytes | IO | Nil = nil, version : String = "HTTP/1.1") : self
new HTTP::Request.new method, path, headers, body, version
end
def initialize(@request : HTTP::Request); end
def safe? : Bool
@request.method.in? "GET", "HEAD", "OPTIONS", "TRACE"
end
end
and have it work as a drop in replacement to HTTP::Request
, even if it is 100% compatible with it. For decorators to work, you really need to have the interfaces to build against such that you can use them interchangeably. E.g. RequestInterface
. But that’s probably a conversation in its own.