In es2015, if I have base class to represent a List
that looks like this:
class List {
constructor(data){
this.data = data
}
sortBy(attribute){
return this.data.sort((a,b) => {
return (a[attribute] < b[attribute]) ? 1 : -1;
})
}
get count() { return this.data.length }
}
Then I might want to subclass that base class with a less generic kind of data, namely, if I am an elf, toys:
class ToyList extends List {
constructor(toys){
super(toys);
this.toys = toys;
}
}
At this point ToyList
is no different from List
, except for the name. But if you look at an instantiation of ToyList
, it has both data
and toys
properties. These refer to the same array, in terms of conceptualizing the point of a ToyList
, data
doesn’t make much sense.
If I make a ToyList
, I have both .data
and a .toys
attributes:
tl = new ToyList(['truck', 'plane', 'doll'])
Object { data: Array[3], toys: Array[3] }
Then my tl
has both a data
and a toys
attribute. They’re both references to the same array, but what I would like is for the subclass to only have the toys
reference.
Here’s another example which utilizes a method on the base class:
class Todos extends List {
constructor(todos){
super(todos);
this.todos = todos;
}
get byPriority(){
return this.todos.sortBy('priority')
}
}
var thingsToDo = [
{task: 'wash the dog', priority: 10},
{task: 'do taxes', priority: 1},
{task: 'clean the garage', priority: 0}
]
var todos = new Todos(thingsToDo);
todos.byPriority
This would be nice, because then I could just refer to .byPriority
to get a sorted version of the list which is very specific to this particular kind of data. But I can’t see how I can make that happen, because
But what I get is:
TypeError: this.todos.sortBy is not a function
So to summarize, what I want is a way to refer to to base class properties with a name which is specific to the semantics of the subclass, without losing the methodology of the base class.
In es2015, if I have base class to represent a List
that looks like this:
class List {
constructor(data){
this.data = data
}
sortBy(attribute){
return this.data.sort((a,b) => {
return (a[attribute] < b[attribute]) ? 1 : -1;
})
}
get count() { return this.data.length }
}
Then I might want to subclass that base class with a less generic kind of data, namely, if I am an elf, toys:
class ToyList extends List {
constructor(toys){
super(toys);
this.toys = toys;
}
}
At this point ToyList
is no different from List
, except for the name. But if you look at an instantiation of ToyList
, it has both data
and toys
properties. These refer to the same array, in terms of conceptualizing the point of a ToyList
, data
doesn’t make much sense.
If I make a ToyList
, I have both .data
and a .toys
attributes:
tl = new ToyList(['truck', 'plane', 'doll'])
Object { data: Array[3], toys: Array[3] }
Then my tl
has both a data
and a toys
attribute. They’re both references to the same array, but what I would like is for the subclass to only have the toys
reference.
Here’s another example which utilizes a method on the base class:
class Todos extends List {
constructor(todos){
super(todos);
this.todos = todos;
}
get byPriority(){
return this.todos.sortBy('priority')
}
}
var thingsToDo = [
{task: 'wash the dog', priority: 10},
{task: 'do taxes', priority: 1},
{task: 'clean the garage', priority: 0}
]
var todos = new Todos(thingsToDo);
todos.byPriority
This would be nice, because then I could just refer to .byPriority
to get a sorted version of the list which is very specific to this particular kind of data. But I can’t see how I can make that happen, because
But what I get is:
TypeError: this.todos.sortBy is not a function
So to summarize, what I want is a way to refer to to base class properties with a name which is specific to the semantics of the subclass, without losing the methodology of the base class.
Share Improve this question edited Feb 11, 2016 at 4:10 asked Feb 11, 2016 at 1:30 user2467065user2467065 4- Well, why do you inherit from List, if you don't want the functionality that List provides? Seems like your Abstraction needs a re-evaluation !? – Thomas Commented Feb 11, 2016 at 2:00
- I have added a couple more methods to the base class to make the point clearer. Of course there would be other methods on the Collection, the sorts of things you can do with a Backbone collection, for instance (sort, search, save, whatever). My question is how to override a given property on the base class so that the derived class can have domain-specific methods and properties, and nothing else. – user2467065 Commented Feb 11, 2016 at 3:56
-
Nothing against adding additional functionality on the derived classes. But as soon as you don't apply your collection to the
data
-property all the additional functionality of the List will fail, and you'll have to wrap or re-implement them for the derived classes; like you've done withget toyCount()
instead of usingget count()
. The whole approach makes no sense, and only adds plexity. I'll add an example of a better approach. – Thomas Commented Feb 11, 2016 at 4:08 -
But what I get is: TypeError: this.todos.sortBy is not a function
sortBy is a method of your List-class, but this.todos is not a List, it's a private property of the Todos-class and of type Array, wich doesn't implement your methods. That are exactly the problems I'm talking about. You would have to re-implement the sortBy-function on Todos to sort the todos-property instead of the data-property, and so on... for every subclass you write. Take a look at my Answer, i've written an example Code, that would walk around this whole Problem. – Thomas Commented Feb 11, 2016 at 5:00
4 Answers
Reset to default 2referencing our discurrion in the ments, a better implementation (imo), extensible and avoiding the problem you asked about
var AP = Array.prototype; //just lazy
class List {
constructor(elements){
for(var i = 0, j = (elements && elements.length)|0; i<j; ++i)
this[i] = elements[i];
//use length instead of count, stay patible with the Array-methods
//will make your life easier
this.length = i;
}
length: 0
sortBy(attr){
return this.sort(function(a,b){
return (a[attribute] < b[attribute]) ? 1 : -1
});
}
//some functions have to be wrapped, to produce a List of the right type
filter(fn){
return new (this.constructor)(AP.filter.call(this, fn));
}
clone(){ return new (this.constructor)(this) }
}
//some functions can simply be copied from Array
//no need to re-implement or even wrap them.
List.prototype.sort = AP.sort;
List.prototype.push = AP.push;
List.prototype.pop = AP.pop;
the subclass
class ToyList extends List {
constructor(toys){
//maybe you want to filter the input, before you pass it to the list
//or convert it, or whatever, it's all up to you
super(toys && AP.filter.call(toys, v=>v instanceof Toy));
}
//... additional functionality
}
and an example usage
class Toy {
constructor(name){
this.name = name;
}
}
var a = new ToyList([
new Toy("foo"),
new Toy("bar"),
"not a toy",
new Toy("baz")
])
console.log(a instanceof ToyList, a);
var b = a.filter(toy => toy.name.charAt(0) === "b");
console.log(b instanceof ToyList, b);
Edit: added your Example with the Todos
class Todos extends List {
//don't even need a constructor, since I simply want to pass
//the array to the parent-constructor
//don't use getter for functionality, use methods!
byPriority(){
return this.sortBy('priority');
}
}
var thingsToDo = [
{task: 'wash the dog', priority: 10},
{task: 'do taxes', priority: 1},
{task: 'clean the garage', priority: 0}
]
var todos = new Todos(thingsToDo);
todos.byPriority()
ToyList
has bothdata
andtoys
properties. These refer to the same array, in terms of conceptualizing the point of aToyList
,data
doesn’t make much sense.
There's your actual problem: your ToyList
doesn't make sense as a subclass of List
.
If (for any reasons) your class should be similar to List
, but not have a data
property, then it's not a subclass any more. It would violate the Liskov substitution principle.
Now what are your options?
- as you already considered, you can make it a subclass in which the more specific
.toys
property is an alias for.data
. This is perfectly fine, but you can't avoid having thatdata
property there as well. - you might want to outiright scrap that
data
property and store elements directly on the object. YourList
class looks like "Array but with useful helper functions". If that was your intention, you should consider making it an actual subclass ofArray
. @Thomas's answer goes in that direction. - you might want to favor position over inheritance. You've already used the concept - your
List
instances containArray
s in theirdata
properties. If you have aWishlist
orToylist
or whatever, that deal specifically with whishes or toys and have corresponding methods for them, you can simply store aList
instance in their.toys
slot.
You actually seemed to expect yourTodoList
to work like that, given the invocation ofthis.todos.sortBy('priority')
(wherethis.todos
would be aList
). On an subclass, justthis.sortBy('priority')
would do the job. - I didn't really get how your
ToyList
is a specialisation ofList
. If there is nothing special about it but the name, maybe you actually don't need a different class alltogether. If JavaScript had generics or type variables, you'd use aList<Toy>
, but it doesn't so you can just useList
s directly.
I think you have a lot of different problems.
Your list has a problem with the definition of sortBy
, you need to take 3 cases in account, like this:
class List {
constructor(data){ this.data = data; }
sortBy(attribute){
console.log("sortBy");
return this.data.sort( (a,b) => {
if (a[attribute] < b[attribute]) return -1;
if (a[attribute] > b[attribute]) return 1;
return 0;
});
}
get count() { return this.data.length; }
}
Now you can extend the List
, and if you want to name data
as toys
then define a get
method named toys()
to return the data. It may strange to you, but if you subclass List
then you should use data
(if not, don't subclass it). There is an alternative: you can delete data
attribute and then create toys
but alas, designing a sortBy
method in List
would be difficult (or use a second parameter to name the array to sort?). So, let's use the first suggestion:
class ToyList extends List {
constructor(toys){ super(toys); }
get toys() { return this.data; }
}
Let do the same for Todos
:
class Todos extends List {
constructor(todos){ super(todos); }
get todos() { return data; }
get byPriority(){
return this.sortBy('priority');
}
}
The definition of byPriority
is a little bit weird as it has a border effect (sorting the elements). I (personally) would write it as a standard method.
Then let's make some tests:
var thingsToDo = [
{task: 'wash the dog', priority: 10},
{task: 'do taxes', priority: 1},
{task: 'clean the garage', priority: 3}
];
var tl = new ToyList(['truck', 'plane', 'doll']);
for (var i=0; i<3; i++) {
console.log(tl.toys[i]); // access the *pseudo* toys attribute
}
var todos = new Todos(thingsToDo);
var r = todos.byPriority; // access the *pseudo* byPriority attribute (border effect: sorting internal data)
for (var i=0; i<3; i++) {
console.log(todos.data[i].priority);
}
May I suggest you to have a little more read about OOP and inheritance? The point of need to subclass but removing data attribute is certainly a bad design.
IMO, a better approach, if possible, would be to treat the List class are pure virtual, Meaning you will never create an instance of that class, but only just extend from it.
Pure virtual classes are not supposed to have constructors, and the methods are to assume certain properties exist. However, you could infact use the constructor to set the name that the base class should use for the 'data' property.
class List {
constructor(keyName) { this.keyName = keyName }
sortBy(attr) { return this[this.keyName].sort(...) }
}
class ToyList extends List {
constructor('toys') {
super(toys)
this.toys = toys;
}
}