1

I'm not even sure how to ask this, but here goes:

Let's say I have a Web application that incorporates Teachers, Courses and Students in such a way that I can do things like this (pseudocode):

teacherQuery = new TeacherQuery;

teacher = teacherQuery.getById(teacherId); // teacher is a Teacher object

foreach (course in teacher.getCourses()) {
    course.printName();
    foreach (student in course.getStudents()) {
        student.printName();
    }
}

studentQuery = new StudentQuery;
students = studentQuery.getAllForCourse(courseId); // students is a 
collection of Student objects

foreach (student in students) {
    foreach (teacher in student.getTeachers()) {
        // getTeachers() returns of a Collection of unique Teachers for all the
        // Courses for the Student.
        teacher.printName();
    }
}

There are a lot of ways to handle this, but my question is about change.

Let's say that once we have the Teacher/Course/Student application working, the client says they want to let the students form study groups, such that each Student can have multiple StudyGroups, and each StudyGroup can have multiple Students, but each StudyGroup belongs to only one Course.

I've been chewing this over, and it feels like inevitably adding StudyGroups (or the equivalent) means making changes to a whole bunch of classes. Apart from creating StudyGroup and StudyGroupQuery, you're going to have to add getStudyGroups() to Student and Course and maybe even Teacher, and you're going to have to add getAllForStudyGroup() to StudentQuery and maybe some other stuff.

And, more to the point, every time you add a new table, you're going to have to add new methods to every class that has a relation to that table.

One approach would be to paramaterize it, so that instead of teacher.getStudyGroups() you have:

genericQuery.get('StudyGroups').by('Teacher').with('id',teacher.getId()).through('Courses').unique()

...or something along those lines, but you're not going to want to copy and paste that every place you need to list the StudyGroups overseen by a given Teacher (are you?) so that means you're still going to want to create getStudyGroupsByTeacher() someplace, which means that you're going to have to edit that "someplace" when it's time to add getTextbooksByTeacher().

I'm tempted to conclude that sometimes there are changes that just tell the Open/Closed principle to go to hell, and that's all there is to it. Am I missing something?

(A couple obligatory notes: The code above is just an example, and I'm not literally looking for a way to incorporate study groups into an existing course application. I am interested in hearing about architecture, but in the context of "What architecture best allows for incorporating new types of entities/tables?" not "What is the best architecture for a Web application?")

4 Answers4

4

It's not your changes that told the open/closed principle to go to hell. It was the design. OCP says it's a better design that allows change to come, not from rewriting code, but from adding new code.

Apart from creating StudyGroup and StudyGroupQuery, you're going to have to add getStudyGroups() to Student and Course and maybe even Teacher, and you're going to have to add getAllForStudyGroup() to StudentQuery and maybe some other stuff.

Well, no. No you don't.

You don't have to add knowledge of study groups to all things related to study groups. You could simply let the study group know about what it is composed of, give it the behaviors you need, and let it figure out how to do them. This is sometimes called "Tell, don't ask"

studyGroup.printStudents();
studyGroup.printTeachers();
studyGroup.printCourse();

Thus the StudyGroup fully encapsulates it's contents so nothing else has to take on the responsibility of doing it's work for it. Put the method where the data is and you don't have to fling the data around. And this means you don't have to touch the other classes.

candied_orange
  • 102,279
  • 24
  • 197
  • 315
  • Wouldn't it be unintuitive that you can do studyGroup.printStudents() but you can't do student.printStudyGroups()? It seems like that would require future users of the system to know the order that entities were added to the system. – Lore Sjöberg Sep 26 '17 at 21:00
  • I prefer my objects to live in a [Directed Acyclic Graph](http://www.elegantcoding.com/2011/08/object-graph.html) rather then have to worry about getting caught in an infinite loop when traversing or risk creating inconsistent relationships because they live in two places. So far my future users haven't minded. They like cats that have a limited number ways to skin. But, if you insist that you want a bidirectional relationship I can show you how to add that without violating OCP. Use [the visitor pattern](http://butunclebob.com/ArticleS.UncleBob.IuseVisitor). – candied_orange Sep 26 '17 at 21:45
  • The bi-directionality is part of it, but my concern is more that, if I understand correctly, your approach always puts the latest entity at the top of the graph. So if I add Textbook, then I have to do textbook.printCourses() rather than course.printTextbooks(). That's a case where unidirectionality is arguably fine, but the direction is the opposite of what you'd want. – Lore Sjöberg Sep 26 '17 at 22:07
  • The latest entry doesn't have to be at the top. It just has to do the pointing. – candied_orange Sep 26 '17 at 22:15
  • I'm not sure I understand the distinction. How would I implement course.printTextbooks() without altering Course? – Lore Sjöberg Sep 26 '17 at 22:26
  • The thing that points is the thing that knows. Anyway, if that's what you insist on doing you do it by writing `TextBookCourse` so that it implements or extends `Course`. – candied_orange Sep 26 '17 at 23:04
3

I think you've misunderstood the purpose of the open/closed principle (which I'm going to abbreviate OCP for the rest of this). The OCP doesn't mean that if you ever have to add an entity that no classes or other objects are allowed to change. It says that an algorithm / function which works on multiple kinds of objects shouldn't have to change every time a new type of object is added.

A good example of OCP would be printing objects as strings. Say you had a method called PrintThing(object o). One way to write it would be like this

void PrintThing(object o)
{
    if (o is int)
    {
        //do some stuff here to convert an int to a string then print it
    }
    else if (o is Foo)
    {
        //Convert relevant information from a Foo object to a string representation
    }
    else if (o is ...)
    //...
}

This is going to get out of hand really fast. Every time you add a new type, you have to add another if block to that method. Built-in types, system types, library types, etc. etc. That is going to become an unmaintainable nightmare fast.

Many languages (C# for instance) make it so that every single object has a ToString method. For most objects, it just uses the default which isn't particularly interesting, though it can be overridden when needed. Thus you could write PrintThing as follows:

void PrintThing(object o)
{
    Console.Write(o.ToString());
}

Now no matter what I throw at PrintThing it will be able to handle it (since object defines a ToString method and everything inherits from object.

So my function doesn't need modification every time it needs to handle a new type. This is what OCP is going for. That your code / functions / algorithms shouldn't require change every time a new type is added. It doesn't at all mean that code might not need to change to handle new functionality.

Becuzz
  • 4,815
  • 1
  • 21
  • 27
2

I've been chewing this over, and it feels like inevitably adding StudyGroups (or the equivalent) means making changes to a whole bunch of classes. Apart from creating StudyGroup and StudyGroupQuery, you're going to have to add getStudyGroups() to Student and Course and maybe even Teacher, and you're going to have to add getAllForStudyGroup() to StudentQuery and maybe some other stuff.

One option is... just don't do that. Here's how you can implement StudyGroups:

  1. Create StudyGroup and StudyGroupQuery classes.
  2. Don't add any methods to Student, Course, Teacher, or StudentQuery.
  3. Implement operations like "get study groups for student" in whichever one of your new classes makes the most sense.

What class does a "get study groups for student" method belong in? The most thorough solution would probably be to have a StudyGroupRepository class which represents a data source that can provide information about study groups.

But that might be overkill. You may already have a StudentRepository class which provides information about students. Do you plan to ever have a database which has students but doesn't support study groups? If not, it would make sense to just go ahead and modify your StudentRepository class to contain the "get study groups for student" method. The open/closed principle doesn't say that you shouldn't modify existing code in order to extend it; it just says that code shouldn't be designed in such a way that it has to be modified in order to extend it.

Tanner Swett
  • 1,359
  • 8
  • 15
  • First off, maybe I'm hung up too much on symmetry, but I can imagine use cases for both getStudentsForStudyGroup() and getStudyGroupsForStudent(). It seems potentially aggravating that every time you want to get X for Y, you'd have to remember whether to use RepositoryX.getXForY() or RepositoryY.getXForY. In general, I go with the idea that RepositoryX should always return either X or collections of X. – Lore Sjöberg Sep 26 '17 at 21:07
  • 1
    Yeah, that's a problem with having separate RepositoryX and RepositoryY classes. In C#, I would probably make heavy use of extension methods here: the "get study groups for student" method would be a method on StudyGroupRepository, and then I would create an extension method allowing users to use the shortcut syntax `myStudent.GetStudyGroups()` instead of `StudyGroupRepository.GetStudyGroupsFor(myStudent)`. – Tanner Swett Sep 26 '17 at 21:19
1

The key thing to remember that goes hand in hand with the Open Closed Principle is the Single Responsibility Principle. This means that rather than perpetually enhance your classes with new features that requires they be changed, you design the initial version of the classes with limited responsibility, test those changes, and the seal those classes.

You can then enhance these classes by subclassing them, and adding an additional layer of responsibility without modifying the base classes. Your super classes are now Open for Extension, but they are Closed for Modification.

Haim Zamir
  • 111
  • 1
  • I think subclassing might not be a good idea in this case. First, the subclass inherits the functionality of the base class so that it bloats up easily (violating the SRP). Second, it creates very deep and difficult to understand class hierarchies. – larsbe Sep 27 '17 at 08:43