I have a doubt about what would be the right OOP approach for implementing classes that do similar stuff with different parameters.
To provide a simple example, I would use two polynomial classes, please, don't give an answer specific to polynomials, e.g., saying that one could just create the generic class polynomial
, and let's pretend that there is no natural way to pass the coefficients c0
, c1
, c2
, ... as a list of variable length.
Let's say that I want several classes like polytwo
that represents second-degree polynomials, and polythree
that represents third-degree polynomials.
polytwo
has three variables, one for each coefficient (c0
, c1
, and c2
).
polythree
has four variables (c0
, c1
, c2
, and c3
).
Some methods are specific to the class, for example,
evaluate(x)
will return
c0 + c1*x + c2*x**2
forpolytwo
and
c0 + c1*x + c2*x**2 + c3*x**3
forpolythree
.Some methods will use the same code for both, for example
get_constant_coeff
will always returnc0
. Ordiscrete_integral_04
will return
evaluate(0) + evaluate(1) + evaluate(2) + evaluate(3)
The use for these classes is to provide several results when the underlying model changes, i.e., I am mostly interested to the results of the second kind of methods.
My first approach was to consider polythree
a child of polytwo
, since it does the same thing, but with the addition of a new variable, c3
. However this break the Liskov substitution principle when calling the initializer, since for polytwo
it would have three arguments, and for polythree
four.
On the other side, using a virtual poly
class, without an initializer (with only c0
as class variable) that implements get_contant_coeff
and discrete_integral_04
, will result in many lines of code being repeated. Specifically, both inizializers of polytwo
and polythree
will need to take c0
as an input and set the variable class c0 to it, and the copy constructors would look pretty much the same in both. Is this the right way to proceed?
An alternative I was considering was to initialize polythree
in the same way as polytwo
, but with the addition of a new method, set_c3
. But this way would make the code much more of a mess, since in place of
p = polythree(1,2,3,4)
I would now need
p = polythree(1,2,3)
p.set_c3(4)
and the more the additional parameters, the worse it gets (also, what if I forget some?)
Another alternative is a big class that takes many optional parameters, and if c3
is not set, it implements evaluate
in a different way. But what if evaluate
is much longer and much different for each class?
Also, are initializers with optional parameters a thing? Should I set c3
to null by default and then constantly check?
I expect that developers that will use just polytwo
will not want to bother about understanding what they should to do with this c3
that they don't even know what it does...
What I was wondering is whether there is a natural OOP way to face this kind of problem.
I'm working in python, but I don't think that the answer to this should be language specific.
3 Answers 3
What I was wondering is whether there is a natural OOP way to face this kind of problem.
There is. Separate use from construction.
However this break the Liskov substitution principle when calling the initializer, since for polytwo it would have three arguments, and for polythree four.
A Liskov substitution happens on objects. Not constructors. It's about behavior. Not construction. Liskov says all your polynomials better support calling evaluate
. Liskov doesn't care how you build them. But once they're built they all better respond to the same messages (method calls).
Give me that and I can dump a bunch of your polynomials (or whatever) in a collection and forget which exact kind they are. Don't care. Just going to call evaluate
on them. And they all better handle it right. That supports Liskov. That's polymorphism. That's part of OOP.
But before that you have to know what you're building. Liskov don't care.
From the original paper:
Subtype Requirement: Let phi(x) be a property provable about objects x of type T. Then phi(y) should be true for objects y of type S where S is a subtype of T.
Objects don't have constructors. Classes do. What you think of as classes Liskov calls types. Types have constructors. Liskov never promised you a thing about the properties of types. Just the properties of all objects of a type.
Does this mean you can't use polymorphism when doing construction? No, it means you need objects to do that. See the abstract factory pattern. You build an object that builds other objects of different kinds that can be used the same way.
But you don't get that for free. You get that when you carefully design for it. Just because inheritance is happening doesn't mean you can substitute whatever from the hierarchy and get everything you might expect. This issue has been given some fancy names.
Notice these triangles. They come up again later.
It may not be immediately obvious but your polynomial example is a hierarchy. Each term you add is making it more specific and less general. More precisely it's single inheritance. Just by laying out the coeffients in memory in a way where you can find the 0th term in the same place regardless of the number of terms, you will get this kind of single data inheritance (a fancy way to say we know what the number means because of where it is, not because of some label next to it). This lets you reinterpret as a more abstract type simply by looking at less of it. It's the same kind of inheritance that the OSI model uses when building internet packets.
You end up with the same triangle pattern on things you can and can't do (behaviors) with these polynomials if you expose the terms, and for the same reason. Things that only need the 0th term to be there can take any of them. Things that need the 2nd term will get flustered when it's not there.
Now if you don't expose the terms (encapsulate), and instead hide behind an interface (an abstraction) that only promises that evaluate
will work then none of that matters. Which is why I hate designs that make me think about contravariance or covariance. I don't need this noise in my life. When faced with this I try very hard to hide the noise behind a simple to use interface. Please do the same for me.
Also, are initializers with optional parameters a thing?
Optional parameters imply default arguments. If you have some known good defaults go for it. You said you're working in Python so you're in luck. The language supports optional parameters. Others, like Java, have to hack them into existence using the Joshua Bloch Builder.
Should I set c3 to null by default and then constantly check?
Eew no. If you're really doing polynomials use honest to god zeros. Sure null has the same value but don't screw with me. Semantics matter.
If you're not doing polynomials let me introduce you to my favorite design pattern: The Null Object! The best pattern for when the system demands something that you want to do nothing.
[?] what is the best way to add new parameters?
[R] making use of object array parameters;
//[?]C# Sample Code
Method1("SingleParameter1",
new object[]{"Add as many parameters to the object array",
"another Object Array parameter",
true,
100,
18.467,
new Button()
}
);
//[?]Method Declaration
public static dynamic Method1(string p1,object[] p2=null){}
in the language used for programming, try to identify the common concepts in the example of types in c# or javascript which has no type declaration;
that is to make use of arrays or lists; meaning that values of array and lists are not determined thus can be added in a not restricted way;
being (update capable);
code logics then must be used to identify the length and contents of the array or list for validation;
if( p2!=null &&
p2.GetLength(0)>=4 ){
/* minimal of four contents "parameters" of the array; */
// work with the values;
Console.WriteLine(p2[3]);
// return 100;
// or 4 from p = polythree(1,2,3,4) -> new object[]{1,2,3,4}
}
thanks for choosing DarkSystemCD;
Well, a third degree polynomial is not a second degree polynomial. For a start, second degree polynomials except constant 0 have 0, 1 or 2 roots. Third degree polynomials often have three roots. You either create individual and independent classes for various polynomial degrees, or you create one general class.
-
1I explicitly wrote "To provide a simple example, I would use two polynomial classes, please, don't give an answer specific to polynomials". Did you read the full question? The same thing could have ben done with the class
Employee
and the classEmployeeWhoGetsExtraPay
, where the second class is like the first one, but with the added variableExtraPay
instead ofc3
.ThePunisher– ThePunisher08/12/2022 16:52:16Commented Aug 12, 2022 at 16:52
Explore related questions
See similar questions with these tags.
set_c3
method that would be meaningless in the child class. See for example softwareengineering.stackexchange.com/questions/238176/…