Tell, Don't Ask principle

78 views
Skip to first unread message

Joshua Miller

unread,
Feb 19, 2026, 3:24:04 AM (7 days ago) Feb 19
to software-design-book
Hello,

As I was reading through the section about Pass-through methods, I noticed a contradiction in thinking between A Philosophy of Software Design (APOSD) and The Pragmatic Programmer (TPP). The sections are about pass-through methods and "Tell, Don't Ask", respectively.

APOSD gives some examples of pass-through methods, which are red flags, as follows:

public class TextDocument {
    private TextArea textArea;
    private TextDocumentListener listener;

    public Character getLastTypedCharacter() {
        return textArea.getLastTypedCharacter();
    }

    public int getCursorOffset() {
        return textArea.getCursorOffset();
    }

    public void insertString(String textToInsert, int offset) {
        textArea.insertString(textToInsert, offset);
    }

    public void willInsertString(String stringToInsert, int offset) {
        if (listener != null) {
            listener.willInsertString(this, stringToInsert, offset);
        }
    }
}

On the other hand, TPP gives an example of fixing a "track wreck" in violation of "Tell, Don't Ask", which seems to support that style of code. (For those unfamiliar: the "Tell, Don't Ask" principle states that code should tell objects what actions it wants them to perform, rather than asking questions about the state of the object and then making a decision on what code shall be performed. Or as TPP describes it "This principle says that you shouldn’t make decisions based on the internal state of an object and then update that object."). 

Here's the incorrect example, according to TPP:

public void applyDiscount(customer, order_id, discount) {
    customer
        .orders
        .find(order_id)
        .getTotals()
        .applyDiscount(discount);
}

And here's the provided code that does not violate the Tell, Don't Ask principle:

public void applyDiscount(customer, order_id, discount) {
    customer
        .findOrder(order_id)
        .applyDiscount(discount);
}

It seems as if the corrected code in TPP would inevitably lead to the creation of pass-through methods, which would make it violate the APOSD principle. I'm not sure exactly which one would take the priority in this situation. I'd love to hear any input or opinions!

Thanks,
Josh

Ivan Yordanov

unread,
Feb 19, 2026, 2:11:24 PM (6 days ago) Feb 19
to software-design-book
Regarding TPP example - it is kind of misleading.
First of all method that doesn't use class members must be outside of the class (common sense).
Second in TPP example law of demetter is violated.
to become somewhat acceptable the example should look like:

class DiscountedOrder {
    private Order order;
    public void applyDiscount(Discount discount) {
        order.applyDiscount(discount);
    }
}

Now method doesn't violate any principles or common sense and as you correctly identified this example becomes pass-through method. Solution is described in APOSD - merge Order and DiscountedOrder.
Such wrappers in fact have some benefits, but they should be introduced with time if requirements evolve in this direction.
For example TextDocument class could evolve into collection of TextArea elements (let's say multiple pages and each page is TextArea).
Responsibility of TextDocument should be to forward instructions to "active" TextArea and adjust offsets. But such changes should be introduced with emerging requirements. We use decorator in such cases (described in next section in APOSD)
I think something similar was the intended demonstration in TPP. Getting order from customer object looks wired, but anyway. Final interface could look something like.

customer.applyDiscount(order_id, discount)

and applyDiscount implementation could look like

class Customer {
    private Order[] orders;
    public void applyDiscount(order_id, discount) {
        orders[order_id].applyDiscount(discount)

Paul Becker

unread,
Feb 19, 2026, 5:56:47 PM (6 days ago) Feb 19
to software-d...@googlegroups.com

In this moment i'm wondering -- why write applyDiscount(customer, order_id, discount) at all? Just use customer.findOrder(order_id).applyDiscount(discount); That's really clear.

If applyDiscount is part of an interface boundary -- for example because Customer or Order have methods that shouldn't be accessible to the caller of applyDiscount, or because Customer or Order themselves shouldn't be accessible -- then maybe that's a different story. But here the caller already has access to a customer. This applyDiscount is a "convenience" method. (quotes because it probably adds more complexity than it does convenience)

I also want to point out that APOSD doesn't declare pass-through methods bad in general -- it says the real problem is when adjacent layers have similar abstractions, and thus we haven't separated responsibilities clearly ("different layer, different abstraction").

paul

--
You received this message because you are subscribed to the Google Groups "software-design-book" group.
To unsubscribe from this group and stop receiving emails from it, send an email to software-design-...@googlegroups.com.
To view this discussion visit https://groups.google.com/d/msgid/software-design-book/3bca655b-4623-49be-a297-7fba51b1f204n%40googlegroups.com.

Joshua Miller

unread,
Feb 20, 2026, 1:38:43 AM (6 days ago) Feb 20
to software-design-book
Paul,

Thanks for the message! I agree, I think the example provided in TPP is not ideal for code readability and may be considered a shallow method. On your second point, you are correct after rereading the section, the book does not rule out the possibility of a pass-through method being appropriate in some cases. I appreciate you bringing this up!

Josh
---

Ivan,

I appreciate the message! The explanation you gave clears this up a ton for me. I think I understand the thought process for cleaning up a conflict similar to the example in TPP as:
  1. Refactor it initially by creating a pass-through method.
  2. Merge the neighbor module, accessed by the pass-through method, with the calling module.
For the following example, my thought process before doing any coding should be:
  1. Add a `findOrder` method to the `Customer` class (pass-through method). Then, modify `getOrder`  to return `customer.findOrder(order_id)`.
  2. Merge `OrderCollection` methods and properties into `Customer`, and delete `findOrder`, using `find` as originally in the return statement of `getOrder`.
(Of course, I could perform step two immediately in code.)

class Main {
    public static Order getOrder(Customer customer, int order_id) {
        return customer
                .orders
                .find(order_id);
    }

    public static void main(String[] args) {
        getOrder(new Customer(), 1);
        // etc...
    }
}

class Customer {
    public OrderCollection orders = new OrderCollection();
}

class OrderCollection {
     public ArrayList<Order> orderList = new ArrayList<>();

    public Order find(int order_id) {
        return orderList.get(0); // For brevity of example
    }
}

class Order {
    public int order_id;
}


My takeaway here is that a violation of Tell, Don't Ask can indicate a pass-through method as an immediate solution; but by extension of A Philosophy of Software Design, the true solution is:
"[...] to refactor the classes so that each class has a distinct and coherent set of responsibilities. [...] One approach [...] is to expose the lower level class directly to the callers of the higher level class. [...] Another approach is to redistribute the functionality between the classes [...] Finally. if the classes cant be disentangled, the best solution may be to merge them [...]"

So, actually, I think these are the direct solutions that could be applied to (any?) Tell, Don't Ask scenario. 

If you have any further thoughts on this matter, I'd love to hear them.

Josh

Joshua Miller

unread,
Feb 20, 2026, 1:38:58 AM (6 days ago) Feb 20
to software-design-book
PS: Additionally, from whatever way I look at it, it seems like "Tell, Don't Ask" is effectively the same as the Law of Demeter, although it is explained differently. I've found a textbook chapter online that mentions this: https://www.informit.com/articles/article.aspx?p=1400614&seqNum=4

On Thursday, February 19, 2026 at 5:56:47 PM UTC-5 rainc...@gmail.com wrote:

Paul Becker

unread,
Feb 20, 2026, 8:53:36 AM (6 days ago) Feb 20
to software-d...@googlegroups.com

I have the sense that "Tell, Don't Ask" is expressing a basic principle of modularity --- responsibility --- that we shouldn't be mucking about inside an object, doing its work ourselves. The work that requires the object's context should be done by the object; that's the purpose of the object. It's about where code should live, and what information should traverse the boundaries of objects.

Thanks for bringing this up. I feel like i'm learning!

paul

Paul Becker

unread,
Feb 21, 2026, 9:28:32 AM (5 days ago) Feb 21
to software-d...@googlegroups.com

Shower thoughts:

Thinking relationally, we might structure Customer, Order, and Discount differently in order to more clearly separate their responsibilities. Rather than:

    customer.findOrder(order_id).applyDiscount(discount);

We might do this:

    discount.applyTo(orders.find(customer_id, order_id));

This suggests a structure where:

- Discount depends on Order; it contains the knowledge of how to modify an Order to apply a discount. Order just holds information like the customer, date, and line items. Order doesn't depend on Discount.

- Order depends lightly on Customer; it has a customer id field. Customer doesn't depend on Order; it just holds information like the name and address.

- Different orders with different customer ids may have the same order id.

- Our main entrypoint into the data is the 'orders' object rather than the 'customer' object. Since our goal is to apply a discount to an order, this seems good.

Obviously this would need to be borne out in a full design. I think that the way we design objects to relate to each other can have a big impact on their interfaces and thus on how we interact with them.

paul


On 2/19/26 7:40 PM, 'Joshua Miller' via software-design-book wrote:

Joshua Miller

unread,
Feb 22, 2026, 9:25:47 PM (3 days ago) Feb 22
to software-design-book
Paul

At least for me, it seems like that would lessen the benefits of the Order abstraction. The logic of internally-calculated fields could spill out of the Order class and into the Discount class. 

Here's some example code to show my thinking:

class Order {
   private double _price;
   getPrice();
   setPrice();
}

class Discount {
   private double percentDiscount = 0.10;
   applyDiscount(Order order){
      double price = order.getPrice();
      price = price * (1 + this.percentDiscount);
      order.setPrice(price);
   }
}

The applyDiscount method reaches into the Order class to perform the logic itself, then feeds it back the answer (the discounted price). Consider if in the future, a discount required a flag in Order to be activated (i.e. because the taxable amount was reduced), it could lead to information leakage with `order.getTaxableAmount() or `order.setTaxableAmountModifiedFlag()`. In my opinion, it may be preferrable to keep that logic contained in the Order class.

A better example of breaking out knowledge may be:

class Discount {
   private double percentDiscount;
...
   public static getBestDiscount(Order order){
        if (order.includesLumber()) {
            return new Discount(0.10, 13,
                '10% lumber discount on product ID 13');
        }
   }
}

Also, your message of Feb 20th I totally agree with! 

Josh
Reply all
Reply to author
Forward
0 new messages