2

I want to find out what part of "loosely coupled" code it is im failing to understand.

Say I have an object Person im using in both my client and server code. If I change a certain field in Person from type int to int[]. What I've found is that there will be a huge ripple effect in my code almost everywhere I've used the field or class caused by that ONE change.

What principle am I not aware of/adhering to?

8
  • 2
    To effectively answer this, we have to understand what that "certain field" was, and why was it changing from a single value to multiple values. Was it a design oversight? Was it based on an assumption that this field can't possibly have more than one value? We would like to know what assumptions you had, and how that assumption was broken by new use cases.
    – rwong
    Commented Feb 16, 2017 at 21:04
  • On the other hand, if that certain "field" was only used for a purpose inside the software, and did not have any business meaning, then this would be something that can be discussed under the banner of "coupling".
    – rwong
    Commented Feb 16, 2017 at 21:05
  • Well, your first guess was right. It was a change from "this can only hold one value" to "this can hold multiple values"
    – BiGGZ
    Commented Feb 16, 2017 at 21:10
  • You first issue is having a "Person" class at all - it already looks like you're baking in excessive generality. Design around narrow behavior in preference to broad entities. And of course, if a class affects everything else in the program, it's definitely doing too much. Commented Feb 16, 2017 at 21:14
  • 2
    Possible duplicate of I changed one method signature and now have over 25,000 errors. What now?
    – gnat
    Commented Feb 16, 2017 at 21:17

5 Answers 5

12

Like many other things in software development, the degree of coupling in software systems is a tradeoff between competing objectives.

Consider this constructor method that takes a Customer as a parameter.

public CustomerProcessor(Customer customer)

As you can see, you're tightly-bound to the Customer class. You can inject it from somewhere else, but you're limited to that specific type.

Now consider this constructor:

public CustomerProcessor(ICustomer customer)

Now we're specifying an Interface instead of a Class. We can pass whatever object we wish to this constructor method, so long as that object conforms to the ICustomer interface (including a mock or stub, for testing purposes).

But what if we did this?

public CustomerProcessor(string customer)

What's in customer? A JSON string? An XML string? Just a name? It could be anything. How it's handled depends on what code is in the CustomerProcessor object, but if your CustomerProcessor object was actually passed into some other class as an ICustomerProcessor, you could have a family of CustomerProcessors that could work out what the format is and correctly parse the string automatically, including a change from int to int[]. Now you're even more loosely coupled.

What does this cost you? Compile-time type safety, of course. That, and significantly increased complexity. It also costs you performance.

So why would you ever do this? Well, there are valid use cases. Consider a system of sensors. Each sensor has a value or a set of values that it emits. There are hundreds of different sensor types. Would you write a class for each new one? Some systems like this merely pass such information as strings along a data bus. Each string has a sensor type embedded, so that the sensor can be properly identified, and its data correctly decoded from the string.


As to your ripple effect, changing a member of a type should only affect that code that actually touches your int. When you make the decision to change from an int to an int[], you commit to refactoring all of the code in your system that touches that int. That's by-design. It has more to do with type systems than it does with the degree of coupling between software modules; since you're changing your fundamental declaration of what a customer is, the type system is going to complain when your old code doesn't meet the new expectations.

7
  • Your examples explained a lot as i proceed to refactor...considering your edit....there really is no way around it after all?
    – BiGGZ
    Commented Feb 16, 2017 at 21:13
  • 1
    When you declare a member in Customer of type int, you're basically entering into a contract with the rest of your code. Changing the type from int to int[] breaks that contract. Commented Feb 16, 2017 at 21:15
  • In an ideal world, which admittedly we don't live in, nobody would know that the field was an int. That knowledge should be encapsulated and "known" by as few consumers as possible.
    – user949300
    Commented Feb 16, 2017 at 23:07
  • "var" can help when a method is just getting a object and passing it on.
    – Ian
    Commented Feb 20, 2017 at 16:15
  • @ian: Type inference sets the type in a var, so it doesn't matter. You can't use var in an interface. Commented Feb 21, 2017 at 15:08
6

If you're exposing the specific type of object instance variables, you're not doing much by way of information hiding and/or encapsulation. Those are core approaches to minimizing the ripple effect in modular and object-oriented code.

Philosophically, objects are not just a struct or record data type. They're intended to hide their internal state representations. Their state is ideally exposed only through well-defined methods and interfaces.

So much for the theory. In practice, objects are often used like fancy structs. Languages like JavaScript, Perl, and Python give direct access to instance values and attributes, and developers often use that direct access, eschewing the virtues of pure, highly-decoupled designs for lower boilerplate code and faster design turns. They also start coding early, sans clear architectures. Some Agile methodologies actively encourage this rush-right-in-and-start-prototyping ethos (see e.g. YAGNI).

Those things aren't necessarily bad. Indeed, on balance, those are popular and effective languages, they're very successfully used in the field, and Agile has helped fix some things that were wrong with pre-Agile methods. But, while there are advantages, there are also are genuine tradeoffs to "code early" and "do the simplest thing that can work." Discovering that you've chosen the wrong data type or structure--in your case, a scalar int where you needed a composite int[]--and then having to ripple changes throughout your code as you accomodate your new understanding...well, that is the price to be paid.

Practically speaking, that is not a change in data type that traditional purist OO responses would really help. If you had a method returning an int that later was going to return an int[], well, that would necessitate ripple changes through your code just as much changing an instance variable. Getters and setters and all the message passing under the sun would not have avoided that ripple effect. The concept behind the data changed, and other code is going to have to change to reflect it.

More thinking through your data architecture up front might have told you you were actually going to need a list not a single value. You need to strike a balance between Agile and Architected. But any mode of development that lauds either exploratory prototyping or refactoring is going to experience some ripple changes. Thinking ahead and employing good encapsulation can reduce and mitigate, but not eliminate, rewriting code as you refactor.

2
  • 1
    Good answer. See also the somewhat extreme and controversial Allen Holub article. Note in particular the subtitle: "Make your code more maintainable by avoiding accessors" (emphasis mine)
    – user949300
    Commented Feb 16, 2017 at 23:04
  • 3
    That is a very good article. Not only in the particulars of getters/setters, but in the general design sense of "everything has tradeoffs; choose yours." Commented Feb 17, 2017 at 1:39
4

You might be violating the Separation of Concerns principle: https://en.wikipedia.org/wiki/Separation_of_concerns

Consider some of the following with respect to your code and the overall design of your application:

  1. Are you duplicating functionality on your client and server? i.e. is there really any need to send that particular data/object over your communication channel?

  2. Do you find yourself often violating the Law of Demeter in your code?

  3. How well does your code adhere to the Single Responsibility Principle (SRP); particularly for classes or methods which use that int[]? Are there any classes or methods which depend upon that field despite doing a bunch of things that don't involve it?

  4. Does your server expose its internal data unnecessarily? If so, consider whether you could build a cut-down interface using separate DTOs (Data Transfer Objects), or to create a more granular interface.

  5. On the subject of your Client/Server interface, it's often desirable to have separation between "Commands" and "Queries"

    • Related: https://martinfowler.com/bliki/CQRS.html
    • In a Nutshell, CQRS is simply about separating those instructions which might be CRUD or change the state of something, away from "Read only" instructions, such as 'GetData' or 'QueryData' (i.e. server instructions which you'd expect to be 'pure' with no side-effects).
  6. Is your client doing too much? Particularly if your client is a GUI application it may be a good idea to keep it as lightweight as possible and aim to minimise client-side logic.

  7. How modular is your application overall? Is your application predominantly built from a handful of "swiss army knife" type classes which do a whole mixed bag of stuff?

    • Do you have long methods which combine a lot of different things? (logic, error handling, exception handling, persistence, object creation, resource cleanup, validation..)
    • Do you have classes with multiple methods which don't really "belong" together (e.g. Methods for reading/writing data generally do not belong in the same classes as methods which perform validation or calculations on that data)

Overall: - Consider the requirements for your application as a whole and look for opportunities to refactor and abstract different behaviours away from each other.

  • Look at the requirements for clues to class names. A Person class doesn't sound very helpful because that's more like the name of an entity (i.e. something representing your application's data). Classes which represent behaviour might be more helpfully named if they allude to the requirement they fulfill - e.g. PersonSerialiser, PersonValidator, PersonSalaryCalculator, etc.

There's no silver bullet to reducing coupling - refactoring existing code isn't a quick task; if you are already in a situation where particular fields are spread all over the place seek opportunities to encapsulate that field and reduce its exposure.

Sometimes it's possible to have fairly localised refactoring, but if you're suffering from acute "spaghetti code" you could end up finding yourself in a rabbit-hole redesigning big chunks of the application.

1
  • Thanks a lot, in particular for point no. 5. These effects usually occur around my entity classes, not the Utility classes. Those usually remain largely unchanged, and when they do there is almost no ripple effect
    – BiGGZ
    Commented Feb 16, 2017 at 21:11
3

Loose coupling is about minimizing technical dependencies. You made your server and client modules independent of each other by giving them their own data and making them unaware of eachother. If you then change your problem domain model, it will effect both server and client and you will have more work to do than you would have had with a tightly coupled system because that would have used shared code. There is no paradox here.

Loose coupling is good in the sense that breaking one part will not break other parts. But it requires a protocol. And if you change that, everything falls apart after all and/or will need rework.

2

In general you should be better at hiding the internals of Person the rest of the code should not really know what is going on on the inside. That is encapsulation. Let the Person object do the work, "tell don't ask".

But if the rest of the code needs access to information in a field or the value returned from a method and that is changed from a simple value to a list then there will be a ripple effect, this is what happens when you refactor, it can't be helped.

But by designing better and by not jumping the gun and placing a field/method for eg. a phone number in the ´Person` class before you have decided whether there is one phone number or a list of phone numbers (that would be YAGNI (you aren´t going to need it)) you can reduce the effect considerable.

But refactoring will always occur and can be quite a task.

1
  • "... before you have decided whether there is one phone number or a list of phone numbers". This is usually the case when i need to refactor...Thanks for the insight
    – BiGGZ
    Commented Feb 16, 2017 at 21:08

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.