2

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). polythreehas 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
    for polytwo and
    c0 + c1*x + c2*x**2 + c3*x**3
    for polythree.

  • Some methods will use the same code for both, for example get_constant_coeff will always return c0. Or discrete_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.

6
  • 2
    You got the direction wrong. Inheritance is for modeling "is a" relationship - and each polynom of degree two is also one of degree 3, with c3== zero, hence you may inherit polytwo from polythree, but not the other way round. But in this case, I would actually use only one class.
    – Doc Brown
    Aug 12, 2022 at 16:30
  • @DocBrown, I am not sure that fixing some variables in a child class is good programming practice. It might also violate the Liskov substitution principle, since if I had a set_c3 method that would be meaningless in the child class. See for example softwareengineering.stackexchange.com/questions/238176/… Aug 12, 2022 at 17:00
  • Also, please don't give an answer specific to polynomials... I even wrote in the question: "To provide a simple example, I would use two polynomial classes, please, don't give an answer specific to polynomials" Aug 12, 2022 at 17:01
  • 3
    Well, your question to some degree specific to polynomials, and it is pretty unclear what kind of "more general case" you have in mind - and I am not good in mind reading. Moreover, a potential LSP violation can be avoided by making the polynomial class immutable - which means, there will be no set_c3 method. However, as I wrote, I would probably not use two different classes here - no inheritance, no LSP violation.
    – Doc Brown
    Aug 12, 2022 at 17:27
  • 1
    ... I guess your polynom example is to some degree a variant of the Circle-Ellipse problem, a.k.a. Square-Rectangle problem. The Wikipedia article lists several ways how to deal with it - now the exercise for you will be to translate these suggestions to your polynomial classes.
    – Doc Brown
    Aug 12, 2022 at 17:43

3 Answers 3

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.

Liskov substitution principle

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.

enter image description here

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.

0

[?] 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;

-1

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.

1
  • 1
    I 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 class EmployeeWhoGetsExtraPay, where the second class is like the first one, but with the added variable ExtraPay instead of c3. Aug 12, 2022 at 16:52

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.