3
$\begingroup$

Does it make sense to introduce methods even before objects? What we are attempting to do is to get students out of the "everything in main()" mode as soon as possible.

Suppose one starts out with a simple program to compute the roots of a quadratic equation. One can imagine the main() method (very early in the course) looking something like:


   Scanner src = new Scanner(System.in);
   System.out.print("Enter coefficient a");
   double a = src.nextDouble();
   // similar pairs of lines for b and c
   double rad = Math.sqrt(b*b - 4*a*c);
   System.out.println("1st solution is" + (-b + rad)/(2*a));
   // print second root

Now we want to introduce methods to demonstrate decomposition into methods invoked from main()t each of which does some parts of the work. One can imagine method signatures like:

    double getCoefficient(String prompt, Scanner src) {...}
    double computeRad(double a, double b, double c) { }
    void   printRoot(String label, double a, double b, double rad) {
            // called twice, once with rad, once with -rad }

If we want to call these methods from main() the methods must be declared static. Alternatively one can create an instance of the class and invoke the (non-static) methods via the instance. But, we really haven't discussed objects yet (or at least objects the student writes). Our group is split between the static vs non-static approach. The other split involves passing both objects and primitives to methods as this can get into the discussion of pass-by-reference. However, the latter discussion can be brought up later when revisiting methods in future work.

If we go the static route, then later we must point out that many/most methods are non static. If we go the new X() route, one ends up talking about constructors, even if we use a default constructor.

Where and how do you introduce method in your first programming course? Is the proposed structure too advanced? How might you alter it?

Thanks

UPDATE 1:

I mostly agree with Objects early, but I wanted to do methods even before conditionals. I have students use Objects before creating their own classes. Thus the many small methods. Yes, it is encapsulated in a class, but I don't need to deal with constructors yet. My style of programming was dramatically changed when I took MIT's SICP at HP in the 1980's.

To flush out my ideas I created the following: QuadEqn. The assignment consists of several parts. Part A is designed to be completed before conditionals are introduced. Parts B, C, and D come later, perhaps as lab short exercises rather that full blown assignments. I have run this by our undergrad TA's but would like to solicit additional feedback as well.

Again, thanks

UPDATE 2:

Thanks for all the valuable feedback! I have not decided on using a methods first approach, but am simply trying it out to see how it would come together.

$\endgroup$
3
  • $\begingroup$ Certainly you can do that. But a deeper question is whether you actually should teach students to program first with static methods. My experience is that it makes it more difficult for them to ever really understand the essence of an OO language like Java and how to use it. See, for example: cseducators.stackexchange.com/a/2883/1293 $\endgroup$
    – Buffy
    Sep 19, 2019 at 0:06
  • $\begingroup$ I am, of course, a strong advocate of starting with objects and leaving lower level things for later. But you can't do it naively and just follow the language. Here are some ideas about resources for doing it well: cseducators.stackexchange.com/q/4025/1293. The big idea is how do you move effectively from a problem to a solution. Matching the parts implied in the program to the objects that represent them. Few problems in the real world are naturally expressed in terms of ints. More can be naturally represented as objects. $\endgroup$
    – Buffy
    Sep 19, 2019 at 12:30
  • $\begingroup$ Note that the book Karel J Robot has the students write classes and methods prior to introducing conditionals or iteration. In fact it has almost no use of variables (other than references to objects) and no use of int variables and such. Polymorphism comes before iteration, in fact. $\endgroup$
    – Buffy
    Nov 5, 2019 at 0:55

7 Answers 7

5
$\begingroup$

The two general approaches to teaching OO that I've seen are "objects first" and "objects last". Objects first takes the notion that, since we are teaching OO, we should start with instances, APIs, method calls, and inheritance. (See Greenfoot as an excellent start to an objects-first curriculum if you are interested in exploring this approach.) Eventually, you start to delve further into the nitty-gritty of the language, adding in arrays, loops, mods, etc, etc, etc.

Objects last follows the historical developments more directly, teaching imperative programming without OO first in order to highlight some of the thought process that went into developing OOP in the first place.

If you are taking an objects-last approach to your course (and it sounds like you are), then methods naturally fall before objects, and need to be included. Methods were an important development for both code clarity and code reuse. Though it's true that everything will need to be static at first, you can either leave this in as a temporary magical incantation, or make note of it as something exciting that will come later, and that about two-thirds of the way through the course, you will flip everything on its head, and those static markers will largely go away.

$\endgroup$
1
  • $\begingroup$ One major pedagogical problem with objects-late is that students practice the essential parts of OO programming the least. If you start with objects then everything else you do gets to reinforce those early lessons on object structure. Doing it the other way doesn't permit that same level of reinforcement. Learning is by reinforcement and feedback, not by having information logically presented. $\endgroup$
    – Buffy
    Nov 6, 2019 at 19:41
4
$\begingroup$

I second Ben. I and csabinho's views above. While there are semantic differences between methods, procedures, functions and subroutines that are paradigm-dependent, at a high level they can all be considered as ways of organising related code into reusable and more manageable chunks. Pretty much all programming paradigms support this, even scripting languages. So I don't think they should tied to objects and only introduced along with objects.

I understand your concern around static and the fact that methods are inherently tied to objects in Java, but I don't think this is problematic, students have to deal with so much "boilerplate" when learning to program in Java I think they're used to just overlooking things until they need to understand them. The fact that most methods are non-static can be introduced along with objects. There is also the reduced cognitive load advantage of objects being a new concept on its own, without methods also being new (no new pun intended).

Structuring code into reusable chunks with methods/functions/procedures/subroutines is so fundamental to good software design that I think it should be introduced as early as possible. Otherwise student's learn bad design habits that have to be unlearned/untaught later. It also ends up making life easier for students in the long run. Working on huge main methods is every professional engineer's nightmare, we shouldn't expect students to have to cope with it!

This is anecdotal obviously, but I've seen methods introduced late, when students have been dealing with main methods 100s of lines long for months. There were some visible "a-ha" moments when they realised how much easier it was to work with code structured into methods, and many of them reported how much easier they found it. static didn't pose any mayor difficulties.

$\endgroup$
3
$\begingroup$

In my opinion methods/functions should be introduced really early as they prevent the duplication of code. In general, as least according to my experiences, they seem to be introduced quite late, mostly as a final topic in first grade of engineering classes or at the start of the second grade. The use of static shouldn't be problematic and can even be useful, if you build on that and point out the differences when introducing objects and instances.

Additionally I would really point out scopes, as you might use the same names for parameters as you have used as variable names in main. That's a topic that is often forgotten, or at least not talked about enough, but would be really important to be understood by all students.

$\endgroup$
2
  • 1
    $\begingroup$ IMHO they should be taught even before any consideration of avoiding code duplication, as a way to structure the thought, giving significant names to user defined actions (groups of instructions). Important in the process of writing code step by step. If you chose plain Java for teaching to beginners, the static keyword is already present for the unavoidable main(). $\endgroup$ Sep 29, 2019 at 15:22
  • $\begingroup$ Well, I'm quite sure that you should at least know some control structures to see how useful functions/methods are to avoid code duplication. Before that point you only declared and initialized some variables, so I can't even imagine how to demonstrate the use of functions/methods in a proper way. $\endgroup$
    – csabinho
    Sep 30, 2019 at 21:17
2
$\begingroup$

Methods and objects

Does it make sense to introduce methods even before objects?

At a base level, your suggestion that methods only make sense in the context of objects relies on the idea that methods are only used with objects, which is not the case. Functional programming very much eschews objects (to a reasonable degree) and very much relies on functions instead, proving the point that functions and objects are not married.

To static or not to static

If we go the static route, then later we must point out that many/most methods are non static.

Much to the chagrin of my teachers and senior developers, I do not distinguish functions from methods as they are the exact same thing to me. To me, staticness (or not) is irrelevant to define the purpose of a function.

But you might be on the other side here and very much separate methods and functions.

If you think they are separate definitions, then you are in luck. When you later introduce objects, you talk about methods. Before, when you were dealing with statics, you would've only been talking about functions, so there's less of a collision and you don't need to go back on your words after introducing objects.

If you don't think they are separate definitions, then you are in luck. Since staticness then isn't a core part of the definition of "method" and "function", then you don't need to change your students understanding of methods/functions just because you start using non-static methods.
Instead, focus a lesson on static vs non-static to get the point across that they should be used in different scenarios.

Generally speaking, students think statically before they learn to think object(ive)ly, so it makes sense for you to stick to that order.

A practical example

Where and how do you introduce method in your first programming course?

Functions teach two important skills: abstraction and the avoidance of repetition. They are both very easy to demonstrate, but need to be demonstrated in different ways:

  1. Ask your students to perform a particular calculation (e.g. count the letters in a string)
  2. Ask your students to perform this calculation ten times (e.g. on a string[10]). They will probably copy/paste code. Let them. Don't correct them.

The third step is different based on what you want to teach them. If you're teaching them to avoid repetition:

  1. Ask your students to perform this calculation one million times (e.g. on a string[1000000]). They will immediately sigh at the thought of having to copy/paste this another 999,990 times. (Though this is a personal thought, I believe that any student who mindlessly starts doing it without complaints does not have the right mindset to become a developer)

If you're teaching them about abstraction:

  1. Ask your students to change the calculation logic (e.g. only count vowels). If they don't mind making the same change ten times, either get them to increase the repetition before making a change, or simply require them to make change after change, making the calculation increasingly complicated.

In either case, the same will happen: your students will be unhappy about all the stupid brute force copy/pasting. When they've reached the point of discontent, it's time for you to demo something to them:

  1. Solve task 1 using a function for the calculation. Agree with them that the function isn't truly necessary here.
  2. Solve task 2 when you already have the existing function. Point out how little time it took you.
  3. Solve task 3 (whichever you picked) and have them realize that your solution means that they could either execute the logic any arbitrary number of times without needing to change anything (other than the input array size), or only needed to change the function body once instead of changing the copy/pasted logic X times.

To summarize

Teaching follows a strict pattern of incentivization. Unincentivized students don't care about your lesson, whereas incentivized students take it to heart. I've always been a fan of this quote:

If you want to build a ship, don't drum up people to collect wood and don't assign them tasks and work, but rather teach them to long for the endless immensity of the sea.
--Antoine de Saint-Exupery

If your students balk at the idea of a function and think of it as "pointless extra effort", then they clearly haven't experienced the horrors of programming in a world without functions.

Rather than expect them to take your word for it, make them experience the issues with not using functions. Make them hungry for a better way to do the horrible task they have been assigned.

And when they're hungry enough, show them the better way. Suddenly, they will love and accept functions as opposed to groaning at them.

$\endgroup$
7
  • $\begingroup$ You seem to fail to understand some really fundamental things. One minor one (not the worst) is that functions (in all paradigms) are not primarily there so that you don't need to repeat yourself. That is just a by-product. A good Pascal program has a lot of functions that are called exactly once. $\endgroup$
    – Buffy
    Nov 5, 2019 at 11:40
  • $\begingroup$ @Buffy: Which is why I separately mention the purposes of abstraction and DRY. The example you're highlighting (one-reference-methods) is one of abstraction (most commonly encountered for separation of concerns) $\endgroup$
    – Flater
    Nov 5, 2019 at 11:43
  • $\begingroup$ And if you really conflate static and non-static methods you have a really fundamental misunderstanding of how a language like Java actually runs. The chagrin of your teachers may be well justified. Learn to use your tools. $\endgroup$
    – Buffy
    Nov 5, 2019 at 11:43
  • 1
    $\begingroup$ @Buffy: I didn't claim that there's no real-world difference between statics and non-statics during runtime. I said that my core definition of what a function/method is (i.e. a small compartmentalized algorithm) is independent of whether that function happens to be static or not. "should I make this static" and "should I use a function/method" are two questions that are independent of eachother. Both should be evaluated but their answers are not related (one's answer does not suggest the other's answer) $\endgroup$
    – Flater
    Nov 5, 2019 at 11:44
  • $\begingroup$ Then, I question whether you understand scope. These sorts of things can't really be glossed over without giving students a burden of misinformation that they will eventually have to fight their way past. $\endgroup$
    – Buffy
    Nov 5, 2019 at 11:46
2
$\begingroup$

IMHO, the response by @Ben I. and the advocacy for OOP first by @Buffy capture the essence of your problem. You have to choose whether to go imperative first or OOP from the outset. Once you make the choice, the other matters resolve naturally. However, there are considerations that affect your choice. Incidentally, you did not tell us if your students are completely new to programming or have had some previous exposure. This is one key consideration in the choice of direction.

Other considerations are: what is the thrust of your curriculum? How much time do you have to cover the curriculum’s content? One semester? Two? How many contact hours do you get in a week? Will the time available afford you the luxury to start imperative and come later to OOP as we do traditionally? Or you have to hit the ground running OOP to cover much before time lapses, as @Buffy would have loved?

If your students are stark new to programming, starting them with objects (from my experience teaching many beginner classes), may be problematic later. Creating objects and classes requires so much background knowledge you end up having to explain too many things in one lesson, generating cognitive overload. If you manage somehow, to get them to learn the higher abstractions, sooner or later, you have to come back down from the gang plank to do lower level things like control structures, arrays etc. Trouble is: it is more difficult for someone who has learnt to drive an automatic car to learn to drive a car with manual gear.

However, if your students have had some programming knowledge or are running another parallel course in the semester under consideration, by all means go OOP outrightly, Now! For, whatever they could not learn with your OOP Java, will be made up by the past exposure or the parallel course.

So for complete beginners, go imperative first. Teach static methods soon after covering the elementaries and control structures + if possible, arrays. I appreciate yours and @Buffy’s concern about the proactive inhibition, earlier knowledge of static methods may cause when you get to classes/objects, and the subsequent difficulty unlearning. However, the benefits of teaching method/functions early in a first programming course overwhelm any “side effect” of it.

It teaches the student disciplined and good structuring of code and also as you noted, it weans them off the “everything-in-main()” bad habit early. One significant benefit of this I learnt by experimenting different order of topics with different beginner streams I have taken is: the students that have written functions/methods in earlier lessons learn to write a class/object faster and easier. They transfer learning of the naming rules and layout of functions/methods etc. to the writing of classes/object. Makes your teacher’s life easier.

Secondly, a beginner that has not written any aggregate entity like function/method or struct (C++) with many statements in the block can get overwhelmed at first. So learning methods first, facilitates writing class/objects and, especially a class with large number of statements and structural complexity.

Besides, static methods are part of class/object. Teaching it before teaching class reduces the cognitive load when you subsequently come to teach class since hopefully, you won’t have to explain that part again. You can focus on other things like why privating/who publicking, Messrs. constructors/destructors et.al.

It also helps the students build more sophisticated programs early. They can play with controls structures and method calls to create more interesting things even before knowing how to use class/objects.

As per the concerns about static methods not being thread safe etc., I understand from Java 8 onward some of these issues have been addressed. For example, we can have static methods in interfaces too.

Finally, a caveat: if you choose to go imperative first, let them at least learn control structures (branching & looping constructs) & scope before teaching them methods as @csabinho had pointed out. Scope may be taught in the course of teaching methods when you explain perishing of variables as you go out of scope, out of the fence, though. So make your choice in light of your situation and that of your students.

PS: I looked up your QuadEqn exercise. It was good. I like the annotation especially. But leave the part where they pass parameters to main () till the end. Else the cognitive payload will kill the smooth learning I expect from your exercise.

$\endgroup$
2
$\begingroup$

I've hesitated to provide an answer for this since it would be, necessarily, very long. But I've spent more than 20 years on this basic question and suppose I need to bite the bullet and try to clear up misconceptions that should have been dispelled generally years ago. My focus has long been on how to teach programming effectively, as well as programming languages in general.

And for starters, you don't teach programming by teaching a lot of syntax. Nor is the difference between languages primarily their syntax. Not if they represent different paradigms, at least.

I apologize in advance for the denseness of this. I hope it is accurate. My writing in general has been described as accurate but too dense. Because of this, it may be hard to grok the implications of some of it without reflection.

When we teach a first course in programming, per se, the course has to answer the fundamental question "What is a program?". Giving a concise definition to a beginner will just lose them, but, by the end of the course, they should have a good understanding of an answer as well as guidance in How To Program. If the course leaves them confused about that you have done a disservice and they will, mentally, need to start over to get an understanding of what is a program and how do we construct one.

Everything we do in that first course should be directed to that end. What is a program? How do we create one?

Sadly, too many people think of a programming language as a bunch of stuff thrown together, features, that can be used in any way you like. While that is literally true, it leads to terrible, unmaintainable, impossible to understand, cruft. We need to do better and our students need to have proper guidance.

In fact, language designers don't think that way at all.

However, there are different conceptions about what a program is, and those different conceptions (paradigms) lead us to different answers to the question "What is a program?" and the question "How do we create one?" Especially, how do we create a beautiful, useful, maintainable, understandable, program?

My university once declined to hire a very smart guy because he admitted that the program he created as the basis of his dissertation was so complex that he couldn't really understand it and was afraid to modify and extend it because it was so fragile. A naive understanding of programs and programming will leave you in exactly that place. It's creator has lost control of the creation. A Frankenstein Program.

There are others, but the three main paradigms are Functional, Procedural, and Object-Oriented. I will try to explain each of these in turn and also explain why one shouldn't try to mix them with beginners (and mix them only very carefully for experts - C++, say). Each of these paradigms has a different answer to the question "What is a program?" They also give guidance about how to construct a program within that paradigm.

Note that for all of these paradigms, the descriptions and the definitions are highly recursive, even when the programs written in them are not.

And note that once you have answered the question What is a Program for any paradigm, you can write any program at all. All the paradigms are Turing Complete hence equivalent in power. If you master any of the paradigms, and have adequate language tools, then you don't need any other paradigm to be able to write programs. Some may be more suitable for some kinds of problems, but all are capable.

Functional Programming

Since this paradigm is less important to the current question I won't expand it so deeply. But a functional program in something like Scheme, or even Standard ML, is an expression that, when evaluated, gives the answer to a problem. Normally that expression is a function, expressed usually as a list, and the evaluation usually results in another list.

But that brings us to the question of what is a function and an expression. An expression could be simple, like a value (42), or it could be a list, or it could be the result of applying a function to some arguments, which are, themselves, expressions. Note the recursion.

The question of How to Program is, then, you create an expression to represent the solution. If the expression requires function evaluations then those functions are, themselves, written to evaluate expressions that are smaller problems within the larger problem. When the overall function is evaluated it starts a cascade of evaluations of functions solving the smaller problems and then the original function constructs a solution from the solutions of the sub problems.

And no, that description isn't suitable for a novice. They need seasoning and practice so that the definition starts to emerge in a way that they can eventually (end of the course) understand.

But note that this is recursive. An expression is composed from expressions, many of which are the results of function invocations.

There is more that can be said, and I could probably refine this quite a bit, but that is really the essence of functional programming. What is a functional program? It is an expression. How do I create a program? By creating an expression that represents the answer I want, probably by writing functions that are themselves compositions of functions.

But note that this "definition" means nothing to a novice. Gibberish. They learn it by writing simple expressions and simple functions, then progressing to more complex situations in which an expression needs to be composed from the invocations of several functions. If you look at a well-formatted Scheme program (i.e. properly indented) the indentation shows the structure.

Procedural Programming (aka imperative)

Procedural programming was probably best defined by Edsger Dijkstra for the Algol family of languages, such as Algol, Pascal, and Modula-2. In this paradigm a Program is a procedure that when executed produces a solution to a problem. How do you create a program (procedure)? If the problem is simple enough, you just write down the solution as the body of the procedure.

But mostly, trying to do that results in a mess since the problem is too hard to understand (and solve) in a single pass without decomposition. Therefore, in order to write that procedure (solve that problem) you factor out sub problems that, collectively, have three characteristics:

  1. The sub problem is a problem in its own right. The nature of the problem and what would constitute a solution is known.

  2. The sub-problem is solvable with a procedure, in theory.

  3. There is a known way to recompose the solutions to the sub problems into a solution of the overall problem.

Then, the sub problems need procedures to solve them. To do that, you proceed recursively decomposing those problems as needed into smaller problems with the above three characteristics. The recursion stops when you can actually solve all the smallest problems with (simple) procedures. Then you recompose those procedures into the solution you initially desired.

This is why procedures can be nested in languages like Pascal and why C isn't ideal as a Procedural Programming Language. Additionally, the ability to nest functions implies (in these languages, at least) that variables defined at one level are visible at lower levels (scoping), but not elsewhere. This disciplined approach to information hiding/revealing is very powerful. In particular it is used to reduce coupling and increase cohesion in programs. (I can expand this if needed.)

What is a Procedural Program? It is a tree of procedures where each node in the tree represents the solution of a sub problem, with lower nodes representing the decomposition of a problem and leaves of the tree represent simply written procedures needing no further decomposition.

How do you create a Procedural Program? You construct the tree from the top down, decomposing as needed to extract simpler problems and then re-composing solutions to solve the overall problem.

Again. This explanation means nothing to a novice. They learn it from practice. Start with relatively simple problems/programs that don't require decomposition and progress to more complicated ones to get practice with both the decomposition of problems and the recomposition of solutions.

And none of what I have said has anything to do with if-statements and while-statements and integer variables and such. That isn't the essence. Those are used only for structure at the very lowest level of the program. If you try to define the overall (complex) program using only these you will fail all of the criteria of what makes a program good. In particular, as little as two pages of such code can be unintelligible even to its creator. (I have evidence of this from a very important - mission critical - project at a major US company. And, well, student projects, of course.)

So, to think as a procedural programmer, you think in terms of procedures (some of which are functions) and, if the problem is difficult, imagine all of the simpler problems that, if they could be solved, would represent the solution you desire. Solve the simpler problems with procedures.

Note 1.

In both procedural and functional paradigms the text of the program is tree-like. Namely nested structures. But the running program is also tree-like. A procedure or function can invoke its "child" nodes generating a run-time tree of execution. Call down the structure. Return up the structure. The tree is traversed (in time) as the program executes. And in both of these paradigms, the program is thought of as a process, not a thing. It answers a "how does it work?" question, not a "what is it?" sort of question. And most of the structure of such a program is a description of an algorithm. The overall program does something. It's parts do some part of that. The structure models the process. This will be different in object-oriented programming.

Note 2.

Until recently procedural programming in the above model was impossible in Java, since functions couldn't be nested. The introduction of Lambdas in Java 8 has changed that, but for a different purpose. And I'm pretty doubtful anyone has yet used lambdas in this way. And if you do, you come to something closer to a functional program than a procedural one. But if you want to solve a large and complex problem with static methods in Java, the structure is flat, not nested. All of those functions are at the same level in the program. This is actually the programming model of FORTRAN. One function that solves part of the problem of another can't be nested inside that other function.

Note 3.

In a procedural program, at any time that a procedure is called, the actual text of the program will tell you precisely which code will be executed. But in a functional program this may not be the case. This is because functions are, themselves, values that can, for example, be stored in lists and passed around as arguments to other functions and then invoked. In other words, you may need to actually execute a functional program to determine what it will do next. We shall see this again with object-oriented programs, but in a different way.

Object-Oriented Programming

While Simula 67 was technically the first OO language, the slightly later Smalltalk better represents what we think of today as an object oriented language. (C++ derives from Simula and is actually a multi-paradigm language, or paradigm-agnostic language, not, specifically an OO language.) The following doesn't describe Javascript, since it came from yet another trend that started with the language Self. Javascript has objects, but no classes.

What is an object oriented program? It is the orchestration of a web of interacting encapsulated values (objects). The web of interactions produces the solution to the problem at hand. The interactions are modeled as services that an object provides to other objects.

How do you write an OO program? You first analyze the internal structure of the problem and determine its "parts" you model these parts as faithfully as necessary for your purposes (objects). You also define the web of interactions, partly externally (function main), but mostly internally as determined by the needs of the objects.

Example: If you want to solve problems about traffic flow (self driving), you model the transportation system to some necessary level of detail. A transportation system has parts: it has roads and bridges, cars, busses, intersections, signals, detours, ... These "parts" are modeled by objects, which are, in turn, described by classes. There is only one "system" but it has several "cars", etc. The cars interact, on occasion, with each other and with other objects,

Example: A student information system has parts: courses, students, instructors, records, rules and exceptions, ... The parts interact: students enroll (a service) in classes, etc.

Example: A calculator has parts: one or more displays, keys, internal stacks, ... The parts interact. Key presses (a service) cause changes in the display (sometimes). ....

What, then, is an object? An object is an encapsulated actor that is comprised of both data and behavior. The behavior models services that the object can provide. The data can only be acted on by requesting a service of it. In a complex program, the "data" within an object is made up of other (lower level) objects, that provide services to the containing object. (Note the recursion here).

What services can an object provide? These services are defined by an interface, that might be stated explicitly or not. Each service is modeled as a protocol for a procedure or function (collectively called methods). In smalltalk derived languages (Java, Python, ...) the protocols are defined in classes, that also specify the data used by objects created from the class.

Objects are created dynamically as the program runs, not statically from the program syntax. A class is a description of a certain type of object, but the objects are distinct. Most classes can be used to create many, independent, objects of the same type, but individually encapsulated. Objects (via their services) can create other objects, both internal to their own structure and externally.

Communication (the web of interactions) in an OO system is via messages. A message requests a specific object to perform a specific service. Some services return data (functional). Some update internal structures of the objects or request further services of other objects without returning data (procedural). While possible to do both, it is discouraged as it makes the program more complex and harder to maintain. It isn't forbidden, but novices should be discouraged until they have more experience. (A saw can be used as a hammer, but the novice is discouraged.)

It is a feature of OO programming that the text of a program cannot predict, precisely, the code to be executed when a message is sent. This is similar to the situation in Functional programming where functions are values. Objects from different classes can be sent the same messages, provided that the classes implement the same interface (In Java you need explicit implementation of a defined interface, but not in Python.) The implication of this is that when a reference variable "points to" some object that is defined by an interface, the code to be executed is only "known" to the object - and the object doesn't exist in the text of the program but only at runtime. And, the implication of that is that you program to interfaces, not to specific class structures. The same is true in subclassing. Objects from the subclass implement (at least) the interface of the superclass, but may do so in different ways. (See the notes on polymorphism, below.)

---- Complete for now, but I'll think some more. -----

Good and Bad OO Programs

Here are some rules for creating good programs. I'll focus on Java. In Python and Ruby they would be a bit different. The rules in those other languages still hold, but those languages give you a bit less support for them since they are dynamically typed where Java is statically (compile time) typed.

True experts can break these rules, but normally consider them. But in the first course, insist on them and teach students that dangers lie outside the safe zone provided by the rules. "Here there be monsters!"

Don't write big classes: classes should be small with only a few methods. They should be single purpose. Do one thing well on request.

Don't write long methods: methods should be short with very low Cyclomatic Complexity. I start to get itchy when a method is more than three statements or when I need nested statements. The complexity of an application isn't in the individual methods (or classes) but in the interactions between objects. This skill, factoring a complex program into small and simple parts, is one of the most important and hardest to develop. It needs to be practiced.

Write a lot of classes: This is a consequence of the first two rules. A system is made up of a lot of parts, but the parts, themselves are simple.

Use subclassing very sparingly and never deeply. Novices tend to break this rule, thinking that subclassing is the Big Idea of OO programming. It actually isn't. There is much more power in using composition and polymorphism than in subclassing.

Write classes whose internal data is mostly other objects, not language primitives: Complex parts are made from simpler parts. This is the purpose of inner classes. You can hide the structure of an object by hiding the structure of its parts. This is encapsulation and data hiding done well. A car object has an engine object. An engine object has piston objects. A piston has bushing objects. Complex things made up of simpler things. But a car (as a whole) doesn't need to "know" about pistons. Only the engine does. And certainly the transportation system can ignore pistons. This is programming by composition. Composition is really the Big Idea of OO.

Write a lot of interfaces, even if only one class will implement a given interface: The interface documents your intent. Understanding the intent, and programming only to that intent, is what makes complexity manageable.

If you create a subclass, don't also extend its implicit interface: That is, don't add additional public methods. You will be forced at some point to use static polymorphism to resolve things if you do. If you need new "features" then extend the formal interface instead and have the subclass implement that interface. (I've found this to be one of the most important measures of code comprehensibility.) A circle isn't a subclass of (special case of) a point. Use subclassing for specialization, not extension.

Most reference variables should be typed with an interface, not a class: This also helps you avoid the trap of being forced into static polymorphism (and warned, at least) when you fall into it.

Use only public and private for class features. The others are for experts. Protected, in Java, is a land mine. All data elements, other than constants, should be private. Among the methods, only the service-defining ones are public. Protected is intended for large teams building complex software over several packages. Unspecified visibility increases the possibility of linking between elements of a class, complicating maintenance and extension. Just. Say. No.

Use static methods sparingly and only for their intended purpose. A method that tells you the number of cars created by the Car class is static. A method that tells you the color of an individual car is not static and returns information separately by each individual object.

Don't build classes speculatively, thinking that you might want them later. (Reuse is overrated). Classes are there for purpose-build (bespoke) objects, not for general principles. Don't build the Car class in your transportation system model until you know you need it. Don't commit much to its interface until you know how it will be used and what actual services it must provide. A mutator to change the color might be completely superfluous. Build what you need, when you need it.

Achieve polymorphism in a program, not via subclassing as such, but by judiciously replacing one object with another. Both have the same interface, but each performs some process in a different way. See the Strategy and Decorator Design Patterns.

A note on OO libraries

One would like to use the libraries supplied with a language as models for good programming in the language. Unfortunately it doesn't work out so well. Library code breaks many of the rules I suggest above as you are probably aware. There are two aspects.

First is the the goals of a library writer are very different from those of an application programmer. The former is providing general purpose services for programmers, rather than purpose built solutions for users. The modeling problem, in particular, is simpler.

And second, the library writers are experts who interact with other experts to define the libraries. When they break a rule they do so very carefully and after long thought and testing.

Class nesting and using protected features are often used in libraries. But so are lots of interfaces and extensions of interfaces. But these are expert-only tools.

“Do not meddle in the affairs of wizards, for they are subtle and quick to anger.”

Polymorphism: static and dynamic

Selection statements in a programming language are a form of polymorphism. They provide different actions, depending on circumstances. But this is static polymorphism. Almost all programmers are familiar with it, even if not by that name.

Dynamic polymorphism, however, is something that happens only at run time. If a variable references an object, then a message to the object names the variable, not the object directly. If the variable refers to different objects of different classes (but the same interface) then different things can occur when the same message statement is re-executed. And the text of the program doesn't normally reveal this. In Java, however, you can subvert the system by forcing static polymorphism using something like the instanceof operator. Through it you can learn the specific class of an object (usually) and you can then know the precise code that will be fired in a message. But this usage represents a failure. You should never need to write code that does this, and if you really need it then your program is broken in other ways. Perhaps you used subclassing naively. This sort of programming is a code smell that indicates something sick or wounded elsewhere. Let your program heal to avoid it. Let dynamic polymorphism be your friend. If you need to fight it, you need to rethink how you are using your tools. Hammer, meet Saw.

And don't confuse polymorphism with subclassing.

How Abstraction fits this scheme

Abstraction (simple names for complex phenomena) are vital to all three paradigms. But different languages have more or fewer ways to do it. Procedural abstraction (naming a complex process) is about all that is available in Pascal (or C). But class based abstraction (naming of things) can be a very powerful adjunct. So powerful, in fact, that it becomes the main tool. Model the things. Give them good names. And, the rules I've suggested (lots of small classes and methods) means that you create abstractions for many things in an OO program when built as described above. You become a namer of things. Which, itself, is a kind of wizard.

What emerges from programming OO in the way described here, when mastered, is programs that are expressed directly in the language of the model - which is closely related to the language of the problem and in terms of our intent. The opposite is programs expressed mostly in terms of language primitives which, at best, are mere surrogates for our intent.

Pedagogy

Given my insistence that students learn from repetition (reinforcement) and feedback, if I want to teach them to program in Java, I have to do objects first. Otherwise they get too little practice on the most important things. But I never start with an empty class and an empty main method. Students in a course I'd be willing to teach will start with a substantial amount of scaffolding that lets them focus on the important things, rather than low level syntactic details.

Students can work to extend an existing system. With care, a simulation can be created that allows the students to program in a Turing Complete environment while emphasizing the essence of "What is a program?" and "How do I build it?". The Greenfoot programming system provides a fairly rich set of starting points. The Greenroom site, linked from there, is the specific teacher resource. (I know the developers and have contributed to Greenfoot, but have no connection.)

Conclusions

If you are using an object-oriented programming language, learn (and teach) how to do it well. Don't use it for things (other paradigms) for which it is ill suited. Teach students in the first course some paradigm, but only one. paradigm, so that they have a meaningful answer to the two questions: What is a program? How do I create one?

So, for a formal answer to the question posed, no, I wouldn't do that. It is actually more like C programming. If I wanted to teach C and its concepts (and have done), I'd use C. C is a better C, anyway.

$\endgroup$
9
  • 1
    $\begingroup$ If I had a blog. Final version ought to be a Pulitzer. $\endgroup$ Nov 6, 2019 at 8:57
  • $\begingroup$ Question: How you done software development in the workplace? While this is pretty great conceptually, it smacks over overly theoretical "pure OOP" philosophy that one sees in academia but not so much in real engineering work, IME. $\endgroup$ Nov 6, 2019 at 19:22
  • 1
    $\begingroup$ @DanielR.Collins, it depends on what you mean. I've never had a boss telling me what or how to write. But I've consulted to companies on their software process, which can be appalling. I mostly produce educational software (large and complex) with something over 20,000 users. And, yes, I eat my own dog food. (Actually, the last time I had such a boss was waaaaay back on a FORTRAN project.) Also. I'm focused on the first course here. I also normally use Agile software practice in conjunction with the things I recommend here. Unit tests, frequent delivery, and all that. $\endgroup$
    – Buffy
    Nov 6, 2019 at 19:31
  • 1
    $\begingroup$ @Buffy Thanks so much for your many detailed comments. This answer takes me back to MIT 6.001 SICP that I took at HP. It literally changed my whole career as I learned a whole new way of looking at programs. $\endgroup$ Nov 6, 2019 at 20:52
  • $\begingroup$ What a tour de force! This is possibly the most substantial (and possibly the most important) answer I have seen on CSEd so far. I regret only that I have but one upvote to give. $\endgroup$
    – Ben I.
    Nov 7, 2019 at 0:39
0
$\begingroup$

Does it make sense to introduce methods even before objects?

Short answer: Yes. Without arguing that it's necessarily better, it's at least a reasonable approach; it's common enough that major textbooks are published following that strategy. For example, the Gaddis Starting Out with Java books have two variants, for whichever approach you prefer:

In the former, writing methods is introduced in Chapter 5 (before writing classes), and indeed, all of the examples in that chapter necessarily have the static keyword. It works sufficiently well enough that this particular textbook is now on its 7th edition.

I'll point out that Gaddis also has an analogous pair of textbooks for C++. At my institution we do use the "control structures through objects" version (largely with a procedural C-like first semester, and then an object-oriented second semester, using the single textbook for the year-long sequence).

Also I will recall that circa 2002 or so I was at another institution where I proposed this approach for our Java sequence, and other faculty argued that no, that wasn't logically possible when teaching Java. But here we are almost 20 years later and that's exactly what my current department does.

One more thing to consider: Among my friends who are practicing software engineers, one (in his late 20's) a few weeks ago complained bitterly to me that the pure-OOP philosophy with Java taught throughout his undergraduate program was almost entirely a waste, as he uses none of it for his work in web services. But functional thought is something he certainly uses daily, so he argued there should have been more of that in his education.

$\endgroup$

Your Answer

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

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