Interview Handbook by Sayed Elmahdy العربية

Topic 16 · Design & architecture

Clean code & refactoring

Naming, functions, DRY/KISS/YAGNI, code smells, refactoring techniques, code review.

15 questionsJunior · 5Mid · 7Senior · 38 quiz

Concepts

What you will learn: Help a teammate understand and change code without guessing or breaking existing behavior. We use clear names, methods with a coherent level of detail, and checks that help refactoring preserve results. Code quality here is about the cost of change, not line count.

Before you start: Be comfortable distinguishing a method from a class, passing a value to a method, and reading its result. Refactoring changes code structure while preserving observable behavior. If basic C# is familiar, the before-and-after examples will explain the rest.

Select a term with a dotted underline for its meaning, then continue reading from the same place.

#Naming and one level of abstraction

Code is read more often than it is written. A name such as Process forces readers to inspect its implementation; CalculateInvoiceTotal communicates intent. Include units where relevant, such as timeoutSeconds, and use consistent business terminology across layers.

A method is like a chapter heading: it introduces an idea, with details delegated to the paragraphs below.
// Fragment inside an invoice service; collaborators are injected.
public async Task IssueAsync(Invoice invoice, CancellationToken ct)
{
    Validate(invoice);                       // (1)
    invoice.CalculateTotal();                // (2)
    await repository.SaveAsync(invoice, ct); // (3)
}
🧾 Code walkthrough
  1. (1) Validation has an explicit name; field-level checks belong in its implementation.
  2. (2) The invoice owns its calculation instead of exposing a loop inside orchestration.
  3. (3) Saving stays at the same abstraction level: use-case steps rather than mixed and business details.

Keep functions focused on a coherent responsibility, rather than a fixed line count. Extracting every statement can add without meaning. The Async suffix and cancellation parameter make the calling contract visible.

#Guard clauses and failing fast

Nested conditions hide the normal path. Reject invalid input early, before side effects. Expected business rejection should still be an explicit result rather than an infrastructure exception.

❌ The normal path is buried
decimal Discount(Customer customer, decimal total)
{
    if (total > 0)
    {
        if (customer.IsActive)
        {
            if (customer.IsPreferred) return total * 0.05m;
        }
    }
    return 0;
}
✅ Preserve the result with flatter branches
decimal Discount(Customer customer, decimal total)
{
    if (total <= 0) return 0;               // (1)
    if (!customer.IsActive) return 0;
    if (!customer.IsPreferred) return 0;
    return total * PreferredDiscountRate;   // (2)
}
const decimal PreferredDiscountRate = 0.05m;
🧾 Code walkthrough
  1. (1) Cases that already returned zero now exit early; test boundaries so evaluation order preserves behavior.
  2. (2) The successful path is visible, and the constant gives the number meaning.
This fragment assumes a non-null customer. At a public boundary, use ArgumentNullException.ThrowIfNull. Failing fast does not require duplicating every defensive check in every method.

#Magic numbers and useful comments

An unexplained number makes a rule hard to change. Extract a constant for a stable meaning and configuration for an operational setting. Useful comments explain a non-obvious reason or external constraint; paraphrasing the statement adds noise and drifts.

❌ Unexplained number and redundant comment
// Add 30 days.
var due = issuedAt.AddDays(30);
✅ A clear name and reason
const int InvoicePaymentTermDays = 30; // (1)
// Contract requires calendar days, including weekends.
var due = issuedAt.AddDays(InvoicePaymentTermDays); // (2)
🧾 Code walkthrough
  1. (1) The name identifies what thirty means; if terms vary by contract, store the value on the contract.
  2. (2) The comment prevents an accidental change from calendar days to business days.

Keep zero and one inline when their meaning is evident. A constant named Thirty adds nothing. Delete commented-out code and rely on version control; use an when a decision needs substantial context.

#DRY, KISS and YAGNI without overengineering

A worked example
  1. Two places calculate the same invoice tax rule. A rate change applied to only one produces inconsistency: duplicated knowledge worth extracting.
  2. Similar-looking tax and discount functions may change for different reasons. Combining them through many flags can make the design harder to understand.
  3. addresses duplicated knowledge, favors simplicity, and avoids speculative needs. Extract shared behavior when the rule is actually shared, not merely when lines look similar.

DRY gives each piece of knowledge one authoritative representation; it does not merge all similar-looking statements. KISS favors the simplest design that meets requirements. YAGNI avoids flexibility for hypothetical needs. Two similar functions may represent rules that change for different reasons.

public interface IShippingFee
{
    decimal Calculate(decimal subtotal);
}

public sealed class Checkout(IShippingFee shipping) // (1)
{
    public decimal Total(decimal subtotal) =>
        subtotal + shipping.Calculate(subtotal);    // (2)
}
🧾 Code walkthrough
  1. (1) Composition supplies a shipping policy without an inheritance hierarchy.
  2. (2) Checkout requests behavior without knowing each carrier's implementation.

Use an interface for real variation or a boundary that needs separation. A small method is enough for one simple policy with no separation requirement. Inheritance can model genuine substitutability, but using it only for reuse couples callers to base-class behavior.

#Tell-don't-ask and the Law of Demeter

When every caller inspects order state and decides how to mutate it, transition rules spread across the application. Let the object protect its . The Law of Demeter limits knowledge of collaborator internals; it is not a rule for counting dots in a pipeline.

❌ Rules and internals leak
if (order.Status == OrderStatus.Pending)
    order.Status = OrderStatus.Cancelled;
var postalCode = order.Customer.Address.PostalCode;
✅ Keep behavior with its owner
order.Cancel();                          // (1)
var destination = order.ShippingDestination; // (2)

// Inside Order:
public void Cancel()
{
    if (Status != OrderStatus.Pending)
        throw new InvalidOperationException("Order cannot be cancelled.");
    Status = OrderStatus.Cancelled;
}
🧾 Code walkthrough
  1. (1) The order protects the transition itself; its status should have a private setter.
  2. (2) A shipping snapshot belongs to the order, avoiding traversal to a customer's current address.

These are design fragments; an explicit result can replace the exception for expected rejection. need not become rich domain objects, and forwarding every property creates noise. Apply the principle when it reduces coupling or protects a business invariant.

#Code smells and their refactorings

A smell is a prompt to investigate, rather than proof of bad code. Choose a refactoring around the reason for change and establish tests first.

SmellSignal and refactoring
Long methodMixed steps: Extract Method with intention-revealing names.
Large classUnrelated responsibilities: Extract Class around cohesion.
Primitive obsessionStrings for everything: introduce validated value objects such as Money or Email.
Feature envyA method mostly uses another object's data: Move Method to that object.
Shotgun surgeryOne rule requires edits in many files: consolidate it with Move Method and Move Field.
Divergent changeOne class changes for unrelated reasons: Extract Class to separate responsibilities.
Data clumpsThe same parameters travel together: Introduce Parameter Object.
Switch statementsRepeated type switches: Replace Conditional with Polymorphism or a strategy.
Speculative generalityUnused hooks and abstractions: Inline Class, Collapse Hierarchy or remove dead code.
A small, clear switch over a closed enum may be better than a hierarchy. Smells require context rather than mechanical replacement.

#Refactoring techniques: before and after

Refactoring changes structure while preserving observable behavior. These independent, abbreviated examples use business types as application collaborators.

❌ Calculation details inside orchestration
var total = lines.Sum(x => x.Price * x.Quantity);
invoice.Total = total;
✅ Extract Method
invoice.Total = CalculateSubtotal(lines); // (1)

decimal CalculateSubtotal(IEnumerable<InvoiceLine> lines) =>
    lines.Sum(x => x.Price * x.Quantity);
❌ Data clump
SearchOrders(DateOnly from, DateOnly to, int page, int pageSize);
✅ Introduce Parameter Object
public record OrderSearch(DateOnly From, DateOnly To, int Page, int PageSize);
// Method signature becomes SearchOrders(OrderSearch query). // (2)
❌ A conditional repeated across operations
decimal Fee(string kind) => kind switch
{
    "standard" => 20m,
    "express" => 50m,
    _ => throw new ArgumentException("Unknown shipping kind.")
};
✅ Replace Conditional with Polymorphism
public interface IShipping { decimal Fee(); } // (3)
public sealed class StandardShipping : IShipping
{
    public decimal Fee() => 20m;
}
public sealed class ExpressShipping : IShipping
{
    public decimal Fee() => 50m;
}
// Resolve the validated kind at the boundary; do not silently use a default.
❌ Callers can mutate the collection
public List<OrderLine> Lines { get; } = [];
✅ Encapsulate Collection
private readonly List<OrderLine> _lines = [];
public IReadOnlyList<OrderLine> Lines => _lines.AsReadOnly(); // (4)
public void AddLine(OrderLine line)
{
    ArgumentNullException.ThrowIfNull(line);
    _lines.Add(line);
}
🧾 Code walkthrough
  1. (1) The calculation name separates how from what without changing the formula.
  2. (2) The parameter object groups related values; validate range and pagination at an appropriate boundary.
  3. (3) Each shipping type owns its behavior. Extra types pay off when branching repeats; these amounts are illustrative, and constant extraction is shown in the magic-number card.
  4. (4) The read-only wrapper prevents casting back to the mutable list. It neither freezes elements nor creates a snapshot; use values and a separate copy when required.

#Safe refactoring, technical debt and code review

Without tests, begin with characterization tests: tests that record what the program currently does, including odd edge cases, so you can tell whether refactoring changed the result. Fix bugs separately so reviewers can distinguish intended behavior changes from code restructuring.

  1. Identify the problem and improvement: a duplicated rule or difficulty adding a case.
  2. Establish behavior tests and inspect public contracts (behavior callers rely on) and side effects such as saving data or sending a message.
  3. Make a small rename or extraction, run tests and inspect the diff.
  4. Measure hot-path changes and roll out gradually when operational risk warrants it.

Technical debt is a shortcut with a future cost. Record the reason, impact, owner and repayment trigger; old code alone does not justify a rewrite.

Review checklist: correctness, edge cases, authorization, data consistency, cancellation, secret-free logs, behavior tests, naming and diff size. Distinguish blockers, suggestions and nits.

Useful comment: “If this message arrives twice, this line charges twice. Could we add a unique message ID and test redelivery?” It names a scenario, consequence and remedy without attacking the author.

#Analyzers, .editorconfig and nullable warnings

Conventions enforced only by review consume attention. Let the compiler and analyzers catch repeatable mistakes. These settings suit a 10 project; adopt rules incrementally in existing code rather than suppressing every warning.

<!-- In the project file -->
<PropertyGroup>
  <Nullable>enable</Nullable>
  <AnalysisLevel>10.0</AnalysisLevel>
  <WarningsAsErrors>$(WarningsAsErrors);nullable</WarningsAsErrors>
</PropertyGroup>
# .editorconfig
root = true
[*.cs]
indent_style = space
indent_size = 4
dotnet_diagnostic.CA2200.severity = warning
csharp_style_var_for_built_in_types = false:suggestion

Nullable enables null-state analysis, while WarningsAsErrors makes the nullable warning group fail the build. CA2200 flags rethrows that lose stack information. A style suggestion does not necessarily fail ; configure severity and build enforcement when that is required.

Older practiceCurrent approach
Scattered null checks and manual formatting reviewNullable analysis, analyzers and shared .editorconfig
Silencing warnings with an unexplained !Fix the nullability contract or establish it through validation

Tools cannot identify a missing business rule. dotnet format --verify-no-changes can check agreed formatting in CI.

Interview questions

Check your reasoning

Why does this choice fit? When would it fail? What changes under higher load or a dependency failure?

16.1What makes code clean?Junior
Model answer

I judge clean code by how safely another engineer can understand and change it. I look for meaningful names, cohesive functions, explicit dependencies and visible failure paths. I also want tests around business rules and boundaries, because readability alone does not establish correctness. I do not apply a fixed method length or introduce patterns automatically. A short function with hidden side effects can be harder to maintain than a longer, straightforward calculation.

16.2How do you choose good names?Junior
Model answer

I choose names that express domain intent and make the call site readable. For example, paymentTimeoutSeconds is clearer than timeout, and ReserveStock is clearer than Process. I use the same term for the same concept throughout the application and distinguish different concepts even when their current implementation matches. I follow C# conventions such as the Async suffix and avoid abbreviations that require local knowledge. If naming is difficult, I check whether the responsibility itself is unclear.

16.3How small should a function be?Junior
Model answer

I keep a function focused on one coherent operation and roughly one level of abstraction. An orchestration method can validate an order, reserve inventory and save it, while detailed calculations live behind meaningful names. I do not use a universal line limit because a clear mapping or algorithm may need more lines. I extract a method when it introduces a useful concept or isolates complexity, and avoid splitting code so aggressively that reading it requires constant .

16.4Why use guard clauses?Junior
Model answer

I use guard clauses to handle invalid input and exceptional cases near the beginning of a method, leaving the normal path easy to follow. They reduce indentation and make preconditions visible. During refactoring, I preserve evaluation order, null handling and side effects, because flattening conditions can accidentally change behavior. I also distinguish invalid programming input from expected business rejection. The former may justify an argument exception, while the latter often deserves an explicit result.

16.5When should you write a comment?Junior
Model answer

I write comments for information the code cannot communicate well, such as a regulatory constraint, a deliberate trade-off or a workaround linked to an issue. I avoid comments that simply repeat the statement, and I rename or extract confusing code before explaining it in prose. Public documentation can describe contracts and failure behavior. I remove commented-out implementations because version control already preserves history, and I update useful comments whenever the relevant behavior changes.

16.6What does DRY actually mean?Mid
Model answer

I interpret as avoiding multiple authoritative representations of the same knowledge. If the same tax rule is implemented in three places, a policy should own it. However, similar-looking code can represent independent rules, such as a loyalty discount and an invoice adjustment. Combining them can create coupling and complicated flags. I first ask whether the pieces must change together, then extract a shared concept when that relationship is real rather than deduplicating purely by appearance.

16.7How do KISS and YAGNI affect your design?Mid
Model answer

I use to choose the simplest design that satisfies the actual functional and operational requirements. stops me from building extension points for scenarios nobody currently needs. For example, I would not introduce a plugin framework for one shipping calculation. I still preserve useful boundaries and meet security and reliability requirements, because those are real needs. When a second variation appears, I can refactor using evidence instead of carrying a speculative abstraction indefinitely.

16.8What is the difference between shotgun surgery and divergent change?Mid
Model answer

Shotgun surgery means one logical change requires coordinated edits across many files, often because a rule is scattered. Divergent change means one class changes for several unrelated reasons, often because it owns too many responsibilities. I address the first by moving related behavior and data together, and the second by extracting cohesive responsibilities. I verify the actual change history before restructuring, because class size or file count alone does not prove either problem.

16.9When would you replace a conditional with polymorphism?Mid
Model answer

I consider polymorphism when the same type-based branching appears in several operations and each variant has meaningful behavior. A shipping policy can then own its price and delivery rules, reducing coordinated switch changes. I keep construction or selection at a boundary and reject unsupported values explicitly. For one small switch over a stable enum, introducing a hierarchy may add more complexity than it removes. I choose based on repetition and change patterns rather than treating every switch as a defect.

16.10What are tell-don't-ask and the Law of Demeter?Mid
Model answer

Tell-don't-ask encourages me to request behavior from the object that owns a rule instead of reading its state and reproducing the rule elsewhere. The Law of Demeter limits dependence on a collaborator's internal object graph. For example, order.Cancel can protect valid transitions, and an order shipping snapshot avoids traversing customer internals. I apply these ideas to reduce coupling and protect , while keeping simple data transfer and query projections straightforward.

16.11Why prefer composition over inheritance?Mid
Model answer

I prefer composition when behavior varies independently, such as shipping, discounts or payment providers. Injecting a collaborator lets me replace one policy without inheriting unrelated state or protected implementation details. It also makes dependencies visible and tests more focused. Inheritance remains useful when there is a genuine substitutable relationship and a stable base contract. I avoid deep hierarchies created only for code reuse, because base-class changes can unexpectedly affect every derived class.

16.12How do you refactor code that has no tests?Senior
Model answer

I first identify the behavior and contracts that must remain stable, including failure paths and side effects. I add characterization tests around those boundaries, using a small seam if necessary to isolate a dependency. Then I make one structural change at a time and run the relevant tests. I separate discovered bugs from refactoring so reviewers can see intentional behavior changes. For high-risk paths, I also compare production signals and use a gradual rollout with a clear rollback path.

16.13How do you prioritize technical debt?Senior
Model answer

I prioritize technical debt by its effect on delivery, reliability and risk rather than by how old or unattractive the code looks. I record the shortcut, its interest cost, the affected area and a practical repayment trigger. Repeated incidents or changes that consistently touch many files are useful evidence. I favor small improvements alongside related feature work, and propose a larger investment only when its expected benefit and migration risk justify the cost.

16.14How do you give effective code review feedback?Senior
Model answer

I review the intended behavior first, then correctness, security, consistency and test coverage before style. My comments describe a concrete scenario and consequence, and suggest an action or ask a focused question. I label whether something blocks approval or is optional, and explain the trade-off when several solutions are reasonable. I leave mechanical formatting to tooling. If a discussion becomes lengthy, I resolve it directly and record the decision so the author has a clear next step.

16.15What can analyzers and nullable reference types enforce?Mid
Model answer

I use analyzers to catch repeatable defects and shared conventions, and nullable reference types to make nullability part of the compile-time contract. I can promote nullable warnings to build errors and share formatting or diagnostic severity through .editorconfig. This reduces review noise, but it does not prove the business logic is correct. The null-forgiving operator only suppresses analysis; it does not validate input at runtime. External input still needs validation, and behavior still needs appropriate tests.

Test yourself

Quiz answers and completed-topic marks are saved in this browser; they do not sync across devices.

Score: 0 / 0

Two similar calculations change for unrelated business reasons. What is safest?

Similar syntax does not establish shared knowledge; premature merging creates coupling.

Which comment adds useful information?

It records a business constraint that the expression cannot explain.

One pricing change requires edits in twelve unrelated files. Which smell?

One scattered rule needs a cohesive owner.

A class changes for shipping, billing and email requirements. Which smell?

Independent responsibilities share one class; Extract Class is a candidate.

What defines a refactoring?

An intentional behavior change is a functional change that should be reviewed separately.

What does a read-only collection wrapper guarantee?

The wrapper restricts access; it does not freeze elements or the source.

What is the best first step before risky refactoring without tests?

Establish evidence of behavior before changing structure.

What does the null-forgiving operator ! do?

It adds no runtime protection; validate when null can arrive.