سؤال

بعد القراءة "ما هو الحد الجيد/الجيد للتعقيد السيكلوماتي؟"، أدرك أن العديد من زملائي كانوا منزعجين جدًا من هذا الجديد ضمان الجودة سياسة مشروعنا:لا أكثر 10 التعقيد السيكلوماتي لكل وظيفة.

معنى:ما لا يزيد عن 10 عبارات "if" و"else" و"try" و"catch" وغيرها من عبارات تفرع سير عمل التعليمات البرمجية.يمين.كما شرحت في"هل تختبر الطريقة الخاصة؟"، فإن مثل هذه السياسة لها العديد من الآثار الجانبية الجيدة.

لكن:في بداية مشروعنا (200 شخص - 7 سنوات)، كنا سعداء بتسجيل الدخول (ولا، لا يمكننا بسهولة تفويض ذلك إلى نوع ما)الجانب برمجة"نهج السجلات).

myLogger.info("A String");
myLogger.fine("A more complicated String");
...

وعندما تم إطلاق الإصدارات الأولى من نظامنا، واجهنا مشكلة كبيرة في الذاكرة ليس بسبب التسجيل (الذي تم إيقافه في وقت ما)، ولكن بسبب معلمات السجل (السلاسل)، والتي يتم حسابها دائمًا، ثم تم تمريرها إلى وظائف 'info()' أو 'fine()'، فقط لتكتشف أن مستوى التسجيل كان 'OFF'، وأنه لم يتم إجراء أي تسجيل!

لذلك عادت إدارة الجودة وحثت المبرمجين لدينا على القيام بالتسجيل المشروط.دائماً.

if(myLogger.isLoggable(Level.INFO) { myLogger.info("A String");
if(myLogger.isLoggable(Level.FINE) { myLogger.fine("A more complicated String");
...

ولكن الآن، مع مستوى التعقيد السيكلوماتي العشرة "الذي لا يمكن نقله" لكل حد من الوظائف، فإنهم يجادلون بأن السجلات المختلفة التي يضعونها في وظيفتهم تعتبر عبئًا، لأن كل "if(isLoggable())" يتم احتسابه على أنه +1 تعقيد سيكلوماتيك!

لذا، إذا كانت الوظيفة تحتوي على 8 "if" و"else" وما إلى ذلك، في خوارزمية واحدة مقترنة بإحكام وغير قابلة للمشاركة بسهولة، و3 إجراءات سجل مهمة...إنهم ينتهكون الحد على الرغم من أن السجلات الشرطية قد لا تكون كذلك حقًا جزء من التعقيد المذكور لتلك الوظيفة ...

كيف يمكنك معالجة هذا الوضع؟
لقد رأيت تطورين مثيرين للاهتمام في مجال البرمجة (بسبب هذا "التعارض") في مشروعي، ولكنني أريد فقط أن أحصل على أفكارك أولاً.


شكرا لكم على كل الإجابات.
يجب أن أصر على أن المشكلة لا تتعلق بـ "التنسيق"، ولكنها تتعلق بـ "تقييم الوسيطة" (التقييم الذي يمكن أن يكون مكلفًا للغاية، قبل استدعاء الطريقة التي لن تفعل شيئًا)
لذلك عندما كتب فوق "سلسلة"، كنت أقصد في الواقع aFunction()، حيث تقوم aFunction() بإرجاع سلسلة، وتكون بمثابة استدعاء لطريقة معقدة لجمع وحساب جميع أنواع بيانات السجل ليتم عرضها بواسطة المسجل ...أم لا (ومن هنا جاءت المشكلة، و فرض لاستخدام التسجيل المشروط، ومن هنا جاءت المشكلة الفعلية للزيادة المصطنعة في "التعقيد السيكلوماتي"...)

لقد حصلت الآن على "متغير نقطة الوظيفة التي قدمها البعض منكم (شكرًا لك جون).
ملحوظة:يُظهر اختبار سريع في Java6 أن ملفي وظيفة فارارجس يقوم بتقييم وسيطاته قبل استدعائه، لذلك لا يمكن تطبيقه على استدعاء الوظيفة، ولكن على "كائن استرداد السجل" (أو "مجمع الوظيفة")، والذي سيتم استدعاء toString() عليه فقط إذا لزم الأمر.فهمتها.

لقد نشرت الآن تجربتي حول هذا الموضوع.
سأترك الأمر حتى يوم الثلاثاء القادم للتصويت، ثم سأختار إحدى إجاباتكم.
مرة أخرى، شكرا لك على جميع الاقتراحات :)

هل كانت مفيدة؟

المحلول

في Python، تقوم بتمرير القيم المنسقة كمعلمات إلى وظيفة التسجيل.يتم تطبيق تنسيق السلسلة فقط في حالة تمكين التسجيل.لا يزال هناك حمل لاستدعاء دالة، ولكن هذا ضئيل مقارنة بالتنسيق.

log.info ("a = %s, b = %s", a, b)

يمكنك القيام بشيء كهذا لأي لغة ذات وسيطات متغيرة (C/C++، C#/Java، إلخ).


هذا ليس مخصصًا حقًا عندما يكون من الصعب استرجاع الوسيطات، ولكن عندما يكون تنسيقها إلى سلاسل مكلفًا.على سبيل المثال، إذا كان الكود الخاص بك يحتوي بالفعل على قائمة أرقام، فقد ترغب في تسجيل تلك القائمة لتصحيح الأخطاء.تنفيذ mylist.toString() سوف يستغرق بعض الوقت دون أي فائدة، حيث سيتم التخلص من النتيجة.لذلك تمر mylist كمعلمة لوظيفة التسجيل، والسماح لها بالتعامل مع تنسيق السلسلة.بهذه الطريقة، لن يتم تنفيذ التنسيق إلا إذا لزم الأمر.


نظرًا لأن سؤال OP يذكر Java على وجه التحديد، فإليك كيفية استخدام ما ورد أعلاه:

يجب أن أصر على أن المشكلة لا تتعلق بـ "التنسيق"، ولكنها تتعلق بـ "تقييم الوسيطة" (التقييم الذي يمكن أن يكون مكلفًا للغاية، قبل استدعاء الطريقة التي لن تفعل شيئًا)

الحيلة هي أن يكون لديك كائنات لن تؤدي عمليات حسابية باهظة الثمن إلا عند الحاجة إليها بشدة.يعد هذا أمرًا سهلاً في لغات مثل Smalltalk أو Python التي تدعم عمليات lambda والإغلاقات، ولكنه لا يزال قابلاً للتنفيذ في Java مع القليل من الخيال.

لنفترض أن لديك وظيفة get_everything().سيقوم باسترداد كل كائن من قاعدة البيانات الخاصة بك إلى القائمة.من الواضح أنك لا تريد استدعاء هذا إذا تم تجاهل النتيجة.لذلك بدلاً من استخدام استدعاء تلك الوظيفة مباشرة، يمكنك تحديد فئة داخلية تسمى LazyGetEverything:

public class MainClass {
    private class LazyGetEverything { 
        @Override
        public String toString() { 
            return getEverything().toString(); 
        }
    }

    private Object getEverything() {
        /* returns what you want to .toString() in the inner class */
    }

    public void logEverything() {
        log.info(new LazyGetEverything());
    }
}

في هذا الرمز، الدعوة إلى getEverything() يتم تغليفه بحيث لا يتم تنفيذه فعليًا إلا عند الحاجة إليه.سيتم تنفيذ وظيفة التسجيل toString() على معلماته فقط في حالة تمكين التصحيح.بهذه الطريقة، سوف يعاني الكود الخاص بك فقط من العبء الزائد لاستدعاء دالة بدلاً من العبء الكامل getEverything() يتصل.

نصائح أخرى

مع أطر التسجيل الحالية، أصبح السؤال محل نقاش

لا تتطلب أطر التسجيل الحالية مثل slf4j أو log4j 2 بيانات الحماية في معظم الحالات.يستخدمون عبارة سجل ذات معلمات بحيث يمكن تسجيل الحدث دون قيد أو شرط، ولكن تنسيق الرسالة يحدث فقط في حالة تمكين الحدث.يتم تنفيذ إنشاء الرسالة حسب الحاجة بواسطة المسجل، وليس بشكل استباقي بواسطة التطبيق.

إذا كان عليك استخدام مكتبة تسجيل قديمة، فيمكنك القراءة للحصول على مزيد من المعلومات الأساسية وطريقة لتحديث المكتبة القديمة باستخدام الرسائل ذات المعلمات.

هل تصريحات الحراسة تزيد من التعقيد حقًا؟

فكر في استبعاد بيانات حراس التسجيل من حساب التعقيد السيكلوماتي.

يمكن القول أنه نظرًا لشكلها الذي يمكن التنبؤ به، فإن عمليات التحقق من التسجيل الشرطي لا تساهم حقًا في تعقيد التعليمات البرمجية.

يمكن للمقاييس غير المرنة أن تجعل المبرمج الجيد يصبح سيئًا.احرص!

على افتراض أن أدواتك لحساب التعقيد لا يمكن تصميمها لهذه الدرجة، فإن النهج التالي قد يقدم حلاً بديلاً.

الحاجة إلى التسجيل المشروط

أفترض أنه تم تقديم بيانات الحماية الخاصة بك لأن لديك رمزًا مثل هذا:

private static final Logger log = Logger.getLogger(MyClass.class);

Connection connect(Widget w, Dongle d, Dongle alt) 
  throws ConnectionException
{
  log.debug("Attempting connection of dongle " + d + " to widget " + w);
  Connection c;
  try {
    c = w.connect(d);
  } catch(ConnectionException ex) {
    log.warn("Connection failed; attempting alternate dongle " + d, ex);
    c = w.connect(alt);
  }
  log.debug("Connection succeeded: " + c);
  return c;
}

في Java، تقوم كل عبارة من عبارات السجل بإنشاء ملف جديد StringBuilder, ، ويستدعي toString() طريقة على كل كائن متصل بالسلسلة.هؤلاء toString() الأساليب، بدورها، من المرجح أن تخلق StringBuilder حالات خاصة بهم، واستدعاء toString() أساليب أعضائها، وما إلى ذلك، عبر رسم بياني كبير محتمل.(قبل Java 5، كان الأمر أكثر تكلفة، منذ ذلك الحين StringBuffer تم استخدامه، وتتم مزامنة جميع عملياته.)

يمكن أن يكون هذا مكلفًا نسبيًا، خاصةً إذا كان بيان السجل موجودًا في مسار تعليمات برمجية يتم تنفيذه بشكل كبير.وكما هو مذكور أعلاه، فإن تنسيق الرسالة باهظ الثمن يحدث حتى إذا كان المسجل ملزمًا بتجاهل النتيجة لأن مستوى السجل مرتفع جدًا.

وهذا يؤدي إلى إدخال بيانات الحراسة من النموذج:

  if (log.isDebugEnabled())
    log.debug("Attempting connection of dongle " + d + " to widget " + w);

مع هذا الحارس، تقييم الحجج d و w ويتم تنفيذ تسلسل السلسلة فقط عند الضرورة.

حل للتسجيل البسيط والفعال

ومع ذلك، إذا كان المسجل (أو المجمّع الذي تكتبه حول حزمة التسجيل التي اخترتها) يأخذ منسقًا ووسائط للمنسق، فيمكن تأخير إنشاء الرسالة حتى يتم التأكد من أنه سيتم استخدامه، مع إزالة عبارات الحماية وبياناتها التعقيد السيكلوماتي.

public final class FormatLogger
{

  private final Logger log;

  public FormatLogger(Logger log)
  {
    this.log = log;
  }

  public void debug(String formatter, Object... args)
  {
    log(Level.DEBUG, formatter, args);
  }

  … &c. for info, warn; also add overloads to log an exception …

  public void log(Level level, String formatter, Object... args)
  {
    if (log.isEnabled(level)) {
      /* 
       * Only now is the message constructed, and each "arg"
       * evaluated by having its toString() method invoked.
       */
      log.log(level, String.format(formatter, args));
    }
  }

}

class MyClass 
{

  private static final FormatLogger log = 
     new FormatLogger(Logger.getLogger(MyClass.class));

  Connection connect(Widget w, Dongle d, Dongle alt) 
    throws ConnectionException
  {
    log.debug("Attempting connection of dongle %s to widget %s.", d, w);
    Connection c;
    try {
      c = w.connect(d);
    } catch(ConnectionException ex) {
      log.warn("Connection failed; attempting alternate dongle %s.", d);
      c = w.connect(alt);
    }
    log.debug("Connection succeeded: %s", c);
    return c;
  }

}

الآن، لا شيء من المتتالية toString() سوف تحدث المكالمات مع تخصيصات المخزن المؤقت الخاصة بهم إلا إذا كانت ضرورية!يؤدي هذا إلى إزالة نتائج الأداء التي أدت إلى عبارات الحراسة بشكل فعال.إحدى العقوبات الصغيرة، في Java، ستكون التعبئة التلقائية لأي وسيطات من النوع البدائي تقوم بتمريرها إلى المسجل.

يمكن القول إن الكود الذي يقوم بالتسجيل أصبح أكثر نظافة من أي وقت مضى، حيث اختفى تسلسل السلسلة غير المرتب.يمكن أن يكون أكثر نظافة إذا تم إضفاء الطابع الخارجي على سلاسل التنسيق (باستخدام ملف ResourceBundle)، والتي يمكن أن تساعد أيضًا في صيانة البرنامج أو توطينه.

مزيد من التحسينات

لاحظ أيضًا أنه في Java، أ MessageFormat يمكن استخدام الكائن بدلاً من "التنسيق" String, ، مما يمنحك إمكانات إضافية مثل تنسيق الاختيار للتعامل مع الأرقام الأساسية بشكل أكثر دقة.قد يكون البديل الآخر هو تنفيذ إمكانية التنسيق الخاصة بك والتي تستدعي بعض الواجهات التي تحددها من أجل "التقييم"، بدلاً من الواجهة الأساسية toString() طريقة.

في اللغات التي تدعم تعبيرات لامدا أو كتل التعليمات البرمجية كمعلمات، سيكون أحد الحلول لذلك هو إعطاء ذلك لطريقة التسجيل.يمكن لهذا الشخص تقييم التكوين وفقط إذا لزم الأمر استدعاء/تنفيذ كتلة lambda/code المتوفرة.لم تحاول ذلك بعد، رغم ذلك.

نظريا هذا ممكن.لا أرغب في استخدامه في الإنتاج بسبب مشكلات الأداء التي أتوقعها مع هذا الاستخدام المكثف لكتل ​​lamdas/code للتسجيل.

ولكن كما هو الحال دائما:إذا كنت في شك، فاختبره وقم بقياس التأثير على حمل وحدة المعالجة المركزية والذاكرة.

شكرا لكم على كل إجاباتك!يا رفاق الصخور :)

الآن تعليقاتي ليست مباشرة مثل تعليقاتك:

نعم لاجل مشروع واحد (كما في "برنامج واحد يتم نشره وتشغيله من تلقاء نفسه على منصة إنتاج واحدة")، أفترض أنه يمكنك تنفيذ كل الأمور التقنية معي:

  • كائنات "Log Retriever" المخصصة، والتي يمكن تمريرها إلى غلاف المسجل، فقط من الضروري استدعاء toString()
  • تستخدم جنبا إلى جنب مع قطع الأشجار وظيفة متغيرة (أو مصفوفة Object[] عادية!)

وهذا ما أوضحه @John Millikin و@erickson.

ومع ذلك، أجبرتنا هذه المشكلة على التفكير قليلاً في "لماذا قمنا بتسجيل الدخول في المقام الأول؟"
يتكون مشروعنا في الواقع من 30 مشروعًا مختلفًا (يضم كل منها 5 إلى 10 أشخاص) منتشرة على منصات إنتاج مختلفة، مع احتياجات اتصالات غير متزامنة وبنية ناقلة مركزية.
كان التسجيل البسيط الموضح في السؤال جيدًا لكل مشروع في البداية (قبل 5 سنوات)، ولكن منذ ذلك الحين، علينا أن نتقدم.دخول مؤشرات الأداء الرئيسية.

بدلاً من مطالبة المسجل بتسجيل أي شيء، نطلب من كائن تم إنشاؤه تلقائيًا (يسمى KPI) تسجيل حدث.إنها مكالمة بسيطة (myKPI.I_am_signaling_myself_to_you())، ولا تحتاج إلى أن تكون مشروطة (مما يحل مشكلة "الزيادة الاصطناعية في التعقيد السيكلوماتي").

يعرف كائن KPI هذا من يتصل به، وبما أنه يعمل من بداية التطبيق، فهو قادر على استرداد الكثير من البيانات التي كنا نحسبها مسبقًا على الفور عندما كنا نقوم بالتسجيل.
بالإضافة إلى أنه يمكن مراقبة كائن KPI بشكل مستقل وحساب/نشر معلوماته عند الطلب على ناقل نشر واحد ومنفصل.
بهذه الطريقة، يمكن لكل عميل أن يطلب المعلومات التي يريدها بالفعل (مثل، "هل بدأت العملية الخاصة بي، وإذا كانت الإجابة بنعم، منذ متى؟")، بدلاً من البحث عن ملف السجل الصحيح والبحث عن سلسلة مشفرة...

في الواقع ، فإن السؤال "لماذا كنا نقوم بتسجيل الدخول بالضبط في المقام الأول؟" جعلنا ندرك أننا لم ننزلق فقط للمبرمج واختبارات وحدته أو تكامله ، ولكن بالنسبة لمجتمع أوسع بكثير بما في ذلك بعض العملاء النهائيين أنفسهم.كان يجب أن تكون آلية "إعداد التقارير" الخاصة بنا مركزية وغير متزامنة على مدار الساعة طوال أيام الأسبوع.

إن خصوصية آلية مؤشرات الأداء الرئيسية هذه هي خارج نطاق هذا السؤال.يكفي أن نقول إن معايرتها الصحيحة هي إلى حد بعيد المشكلة غير الوظيفية الأكثر تعقيدًا التي نواجهها.وما زال النظام يركع على ركبتيه من وقت لآخر!ومع ذلك، تمت معايرته بشكل صحيح، فهو منقذ للحياة.

مرة أخرى، شكرا لك على جميع الاقتراحات.وسوف نأخذها في الاعتبار بالنسبة لبعض أجزاء نظامنا عندما لا يزال التسجيل البسيط قائمًا.
لكن النقطة الأخرى من هذا السؤال هي توضيح مشكلة محددة لك في سياق أكبر بكثير وأكثر تعقيدًا.
اتمنى انك احببتها.قد أطرح سؤالاً حول مؤشرات الأداء الرئيسية (التي، صدق أو لا تصدق، ليست محل شك حول SOF حتى الآن!) في وقت لاحق من الأسبوع المقبل.

سأترك هذه الإجابة للتصويت حتى يوم الثلاثاء المقبل، ثم سأختار إجابة (ليست هذه الإجابة بوضوح؛))

ربما يكون هذا بسيطًا جدًا، ولكن ماذا عن استخدام "طريقة الاستخراج" لإعادة البناء حول شرط الحماية؟رمز المثال الخاص بك لهذا:

public void Example()
{
  if(myLogger.isLoggable(Level.INFO))
      myLogger.info("A String");
  if(myLogger.isLoggable(Level.FINE))
      myLogger.fine("A more complicated String");
  // +1 for each test and log message
}

يصبح هذا:

public void Example()
{
   _LogInfo();
   _LogFine();
   // +0 for each test and log message
}

private void _LogInfo()
{
   if(!myLogger.isLoggable(Level.INFO))
      return;

   // Do your complex argument calculations/evaluations only when needed.
}

private void _LogFine(){ /* Ditto ... */ }

في C أو C++، سأستخدم المعالج المسبق بدلاً من عبارات if للتسجيل الشرطي.

قم بتمرير مستوى السجل إلى المُسجل واتركه يقرر ما إذا كان سيتم كتابة بيان السجل أم لا:

//if(myLogger.isLoggable(Level.INFO) {myLogger.info("A String");
myLogger.info(Level.INFO,"A String");

تحديث:آه، أرى أنك تريد إنشاء سلسلة السجل بشكل مشروط دون عبارة شرطية.ويفترض في وقت التشغيل بدلا من وقت الترجمة.

سأقول فقط أن الطريقة التي حللنا بها هذه المشكلة هي وضع رمز التنسيق في فئة المسجل بحيث يتم التنسيق فقط في حالة تجاوز المستوى.يشبه إلى حد كبير sprintf المدمج.على سبيل المثال:

myLogger.info(Level.INFO,"A String %d",some_number);   

يجب أن يفي بالمعايير الخاصة بك.

نص بديل http://www.scala-lang.org/sites/default/files/newsflash_logo.png

سكالا لديه شرح @elidable() يسمح لك بإزالة الأساليب باستخدام علامة المترجم.

مع سكالا REPL:

ج:> سكالا

مرحبًا بك في إصدار Scala 2.8.0.final (Java HotSpot(TM) 64-Bit Server VM، Java 1.6.0_16).نوع في تعبيرات ليكون لهم تقييمها.اكتب : مساعدة لمزيد من المعلومات.

scala> استيراد scala.annotation.elidable استيراد scala.annotation.elidable

scala> استيراد scala.annotation.elidable._ استيراد scala.annotation.elidable._

scala> @elidable(FINE) def logDebug(arg :String) = println(arg)

تصحيح السجل:(الوسيلة:سلسلة) الوحدة

سكالا> logDebug("اختبار")

سكالا>

مع إليسيت

C:>سكالا -Xelide-أقل من 0

مرحبًا بك في إصدار Scala 2.8.0.final (Java HotSpot(TM) 64-Bit Server VM، Java 1.6.0_16).نوع في تعبيرات ليكون لهم تقييمها.اكتب : مساعدة لمزيد من المعلومات.

scala> استيراد scala.annotation.elidable استيراد scala.annotation.elidable

scala> استيراد scala.annotation.elidable._ استيراد scala.annotation.elidable._

scala> @elidable(FINE) def logDebug(arg :String) = println(arg)

تصحيح السجل:(الوسيلة:سلسلة) الوحدة

سكالا> logDebug("اختبار")

اختبارات

سكالا>

أنظر أيضا سكالا يؤكد التعريف

التسجيل المشروط هو الشر.يضيف فوضى غير ضرورية إلى التعليمات البرمجية الخاصة بك.

يجب عليك دائمًا إرسال الكائنات التي لديك إلى المسجل:

Logger logger = ...
logger.log(Level.DEBUG,"The foo is {0} and the bar is {1}",new Object[]{foo, bar});

ثم لديك java.util.logging.Formatter الذي يستخدم messageFormat لتسوية foo وbar في السلسلة المراد إخراجها.سيتم استدعاؤه فقط إذا قام المُسجل والمعالج بالتسجيل على هذا المستوى.

لمزيد من المتعة، يمكن أن يكون لديك نوع من لغة التعبير لتتمكن من التحكم الدقيق في كيفية تنسيق الكائنات المسجلة (قد لا يكون toString مفيدًا دائمًا).

بقدر ما أكره وحدات الماكرو في C/C++، في العمل لدينا #تعريفات للجزء if، والتي إذا كانت false تتجاهل (لا تقيم) التعبيرات التالية، ولكن إذا كانت true تُرجع دفقًا يمكن توصيل الأشياء إليه باستخدام ' <<' عامل.مثله:

LOGGER(LEVEL_INFO) << "A String";

أفترض أن هذا من شأنه أن يزيل "التعقيد" الإضافي الذي تراه أداتك، كما يلغي أي حساب للسلسلة، أو أي تعبيرات يتم تسجيلها إذا لم يتم الوصول إلى المستوى.

هنا حل أنيق باستخدام التعبير الثلاثي

logger.info(logger.isInfoEnabled() ؟"بيان السجل موجود هنا..." :باطل)؛

النظر في وظيفة استخدام التسجيل ...

void debugUtil(String s, Object… args) {
   if (LOG.isDebugEnabled())
       LOG.debug(s, args);
   }
);

ثم قم بإجراء المكالمة مع "الإغلاق" حول التقييم الباهظ الثمن الذي تريد تجنبه.

debugUtil(“We got a %s”, new Object() {
       @Override String toString() { 
       // only evaluated if the debug statement is executed
           return expensiveCallToGetSomeValue().toString;
       }
    }
);
مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top