0

I have the following structure:

public class LogicStatement : ILogicStatement
{
    public string TestLogic { get; set; }
    public string CompareLogic { get; set; }
    public string Operator { get; set; }
    public string Expression();
    public bool Value();
}

public class Test : ITest 
{
    public int TestId { get; set; }
    public int LiteralId { get; set; }
    public string TestName { get; set; }
    public string TestText { get; set; }
    public string TestDisplayName { get; }
    **public ILogicStatement LogicStatement { get; set; }**
    public string Expression { get; set; }
    public bool Value { get; set; }
}

public class Literal : ILiteral
{
    some property members...
    **public List<ITest> Tests {get; set;}**
    some method members...
}

Note that the class Test has a member of type LogicStatement, and the class Literal has a member of type List.

Note also that all classes have properties and methods that share the same name: Expression, Value, Expression(), Value().

The value of Expression and Value (properties and methods) depend on values in the LogicStatement class.

Throughout the whole project, I use the Interface Type for to instantiate each object to adhere with Dependency Inversion. To support this, I use a factory-like design to create new instances of Test and LogicStatement.

Example:

public static class Factory
{
    public static ILogicStatement CreateLogicStatement()
    {
        return new LogicStatement();
    }
    public static ITest CreateTest()
    {
        return new Test(CreateLogicStatement());
    }
    public static List<ITest> CreateTests()
    {
        return new List<ITest>();
    }
//repeat the same for evey other class.
}

My goal is to have Expression() and Value() be calculated only once in the bottom level class (LogicStatement), and somehow get transfered to their counterpart properties in the higher level classes.

I'm getting the data from Dapper and it looks like all the nested objects are returned from the Dapper module correctly with the same nested structure I intended, and with the right values for all of their members. All of them but Expression, Expression(), Value, Value() are null.

my constructors look like this:

public LogicStatement()
{
    Expression();
    Value();
}

public Test(ILogicStatement logicStatement)
{
    _logicStatement = logicStatement;
    Expression = _logicStatement.Expression();
    Value = _logicStatement.Value();
}

public Literal(ITest test)
{
    _test = test;
    Expression = _test.Expression;
    Value = _test.Value;
}

and my main:

List<ILiteral> literals = Factory.CreateLiterals();
List<ITest> tests = Facotry.CreateTests();
List<ILogicStatement> logicStatements = Factory.CreateLogicStatements();

literals = GetDataFromDapper();

This last line seems to assign correct values to all other members on all hierarchies. But I cannot get Expression and Value to be anything other than null.

If I test LogicStatement.Expression() and LogicStatement.Value() standalone, they do return the expexted values. but starting at the first parent class Test, these properties are all null.

I think I'm doing something wrong in the way i'm instantiating my objects. Primarily because I'm not sure i understand basic best practices to write constructors.

Maybe I the desired behavior should be implemented through events, where the Test and Literal classes subscribe to changes in the Expression() and Value() methods (or rather to what calculates them). But I never used events and I'd like to know if this fundamentally can be acheived without them first.

My question: How do I make the Expression() Value() at the bottom level class "Fire up" whenever LogicStatement is instantiated, and then have the Expression and Value properties be assigned accordingly as a result.

In other words, I want the following to always be true:

test[i].Expression == literal[i].Expression == LogicStatement[i].Expression()

I'm a beginner in OOP. So any fundamental explanation is welcome.

burnsi
  • 6,194
  • 13
  • 17
  • 27
waza
  • 1

1 Answers1

0

As you are new to object oriented programming I would start with the basics and leave factories and adhering with Dependency Inversion and the interfaces away for later.

You could tell Dapper to split joined tables into multiple entities (see https://www.learndapper.com/relationships), but for learning OOP I would start doing everything manually.

Your class design does not look proper to me yet. Not sure what Expression and Value of the LogicStatement are, but if they are calculations based on the other properties, I would implement them as (just to show off with complicated words) lazy initialized cached getter properties that are invalidated in the setters of the relevant properties. That ensures you only calculate them once for as many reads you like but recalculate them on first read after one or multiple properties have been updated.

public class LogicStatement {

    private string _testLogic;
    private string _compareLogic;
    private string _operator;
    private string? _expression;
    private bool? _value;

    public LogicStatement(string testLogic, string compareLogic, string @operator) {
        _testLogic = testLogic;
        _compareLogic = compareLogic;
        _operator = @operator;
    }

    public string TestLogic {
        get {
            return _testLogic;
        }
        set {
            _testLogic = value;
            InvalidateCachedValues();
        }
    }

    public string CompareLogic {
        get {
            return _compareLogic;
        }
        set {
            _compareLogic = value;
            InvalidateCachedValues();
        }
    }

    public string Operator {
        get {
            return _operator;
        }
        set {
            _operator = value;
            InvalidateCachedValues();
        }
    }

    public string Expression {
        get {
            string? result = _expression;
            if (result is null) {
                _expression = result = BuildExpression();
            }
            return result;
        }
    }

    public bool Value {
        get {
            bool? result = _value;
            if (result is null) {
                _value = result = EvaluateValue();
            }
            return result.Value;
        }
    }

    private void InvalidateCachedValues() {
        _expression = null;
        _value = null;
    }

    private string BuildExpression() {
        //Your logic goes here
        throw new NotImplementedException();
    }

    private bool EvaluateValue() {
        //Your logic goes here
        throw new NotImplementedException();
    }

}

Sorry, it got a bit bigger with the full properties.

In the other classes I would not copy the Value and the Expression but simply remove these properties as anybody can easily access them through the LogicStatement property:

public class Test {

    public Test(int testId, int literalId, string testName, string testText, string testDisplayName, LogicStatement logicStatement) {
        TestId = testId;
        LiteralId = literalId;
        TestText = testText;
        TestDisplayName = testDisplayName;
        LogicStatement = logicStatement;
    }

    public int TestId { get; }
    public int LiteralId { get; }
    public string TestName { get; }
    public string TestText { get; }
    public string TestDisplayName { get; }
    public LogicStatement LogicStatement { get; }

}

and the Literal could look like this (I got a bit confused whether this class has one Test or a list of them, I stick to your constructor + properties that hint in the direction of a single one):

public class Literal {

    private Test _test;

    public Literal(string property1, int property2, Test test) {
        Property1 = property1;
        Property2 = property2;
        _test = test;
    }

    public string Property1 { get; }
    public int Property2 { get; }
    public string Expression => _test.LogicStatement.Expression;
    public bool Value => _test.LogicStatement.Value;

}

As you decided not to expose the Test in the Literal it makes sense to provide Expression and Value, otherwise they could also be removed (or kept for convenience).

Christoph
  • 3,322
  • 2
  • 19
  • 28
  • Thank you Christoph. Expression() and Value() are calculated based on property values in the LogicStatement class. Literal only has one Test (I had a typo) but above Literal there are higher level classes that include List (I only showed 3 out of 6 levels). Follow up: 1. Assuming I want to keep the Expression and Value properties in Test (and maybe Literal and others for that matter). what is the correct way of implementing that? 2. In a constructor like the one you suggested for Literal, where is the initial initialization of the passed properties happen (the values from Dapper)? – waza Jan 27 '23 at 13:48
  • 1. Similair to how it's done in `Literal`. 2. You have to start with the `LogicStatement` and not the `Literal`. I would consider mapping the database values to a flat class that only contains properties that are mapped 1:1 to database fields instead and then assemble the nested classes manually using the values from it (assuming everything comes flat from the database from a single query). The other option is to try to tell Dapper to do it for you with `splitOn` (still starting with the `LogicStatement`). – Christoph Jan 27 '23 at 16:42
  • I do use SplitOn. all properties other than Expression and Value get populated correctly. the properties that help calculate Expression and Value in LogicStatement are also populated correctly. it's just these two members that are stuck to null. To be clear, Expression and Value in LogicStatement are not queried from the db, but should be calculated when the objects are instantiated. The first calculation that assigns non Nulls to Expression and Value never kicks off. So currently I have these properties null in all objects, even when the information required to calculate them is there. – waza Jan 27 '23 at 17:25
  • Then just use my logic with lazy initialized getter properties (if the values of the other properties cannot be changed you don't even have to implement them as full properties) and you reached your goal. – Christoph Jan 28 '23 at 21:44